class NHentai { constructor() { this.baseUrl = "https://nhentai.net"; this.type = "book-board"; this.version = "1.2"; this.mediaType = "manga"; } getFilters() { return { sort: { label: "Order", type: "select", options: [ { value: "date", label: "Recent" }, { value: "popular", label: "Popular: All time" }, { value: "popular-month", label: "Popular: Month" }, { value: "popular-week", label: "Popular: Week" }, { value: "popular-today", label: "Popular: Today" } ], default: "date" }, tags: { label: "Tags (separated by comma)", type: "text", placeholder: "ej. big breasts, stocking" }, categories: { label: "Categories", type: "text", placeholder: "ej. doujinshi, manga" }, groups: { label: "Groups", type: "text", placeholder: "ej. fakku" }, artists: { label: "Artists", type: "text", placeholder: "ej. shindo l" }, parodies: { label: "Parodies", type: "text", placeholder: "ej. naruto" }, characters: { label: "Characters", type: "text", placeholder: "ej. sakura haruno" }, pages: { label: "Pages (ej. >20)", type: "text", placeholder: ">20" }, uploaded: { label: "Uploaded (ej. >20d)", type: "text", placeholder: ">20d" } }; } shortenTitle(title) { return title.replace(/(\[[^]]*]|[({][^)}]*[)}])/g, "").trim(); } parseId(str) { return str.replace(/\D/g, ""); } extractJson(scriptText) { const m = scriptText.match(/JSON\.parse\("([\s\S]*?)"\)/); if (!m) throw new Error("JSON.parse no encontrado"); const unicodeFixed = m[1].replace( /\\u([0-9A-Fa-f]{4})/g, (_, h) => String.fromCharCode(parseInt(h, 16)) ); return JSON.parse(unicodeFixed); } async search({ query = "", page = 1, filters = null }) { if (query.startsWith("id:") || (!isNaN(query) && query.length <= 7 && query.length > 0)) { return [await this.getMetadata(this.parseId(query))]; } let advQuery = ""; let sortParam = ""; if (filters) { const textFilters = [ { key: "tags", prefix: "tag" }, { key: "categories", prefix: "category" }, { key: "groups", prefix: "group" }, { key: "artists", prefix: "artist" }, { key: "parodies", prefix: "parody" }, { key: "characters", prefix: "character" }, { key: "uploaded", prefix: "uploaded", noQuote: true }, { key: "pages", prefix: "pages", noQuote: true } ]; textFilters.forEach(({ key, prefix, noQuote }) => { if (filters[key]) { const terms = filters[key].split(","); terms.forEach(term => { const t = term.trim(); if (!t) return; let currentPrefix = prefix; let currentTerm = t; let isExclusion = false; if (t.startsWith("-")) { isExclusion = true; currentTerm = t.substring(1); } advQuery += ` ${isExclusion ? "-" : ""}${currentPrefix}:`; advQuery += noQuote ? currentTerm : `"${currentTerm}"`; }); } }); if (filters.sort && filters.sort !== "date") { sortParam = `&sort=${filters.sort}`; } } const finalQuery = (query + " " + advQuery).trim() || '""'; const url = `${this.baseUrl}/search/?q=${encodeURIComponent(finalQuery)}&page=${page}${sortParam}`; const { result } = await this.scrape( url, page => page.evaluate(() => document.documentElement.innerHTML), { waitSelector: ".gallery" } ); const $ = this.cheerio.load(result); return $(".gallery").map((_, el) => ({ id: this.parseId($(el).find("a").attr("href")), image: $(el).find("img").attr("data-src") || $(el).find("img").attr("src"), title: this.shortenTitle($(el).find(".caption").text()), type: "book" })).get(); } async getMetadata(id) { const url = `${this.baseUrl}/g/${id}/`; const { result } = await this.scrape( url, page => page.evaluate(() => { const html = document.documentElement.innerHTML; const script = [...document.querySelectorAll("script")] .find(s => s.textContent.includes("JSON.parse") && !s.textContent.includes("media_server") && !s.textContent.includes("avatar_url") )?.textContent || null; const thumbMatch = html.match(/thumb_cdn_urls:\s*(\[[^\]]*])/); const thumbCdns = thumbMatch ? JSON.parse(thumbMatch[1]) : []; return { script, thumbCdns }; }) ); if (!result?.script) { throw new Error("Script de datos no encontrado"); } const data = this.extractJson(result.script); const cdn = result.thumbCdns[0] || "t3.nhentai.net"; return { id: id.toString(), title: data.title.pretty || data.title.english, format: "MANGA", status: "completed", genres: data.tags .filter(t => t.type === "tag") .map(t => t.name), published: new Date(data.upload_date * 1000).toLocaleDateString(), summary: `Pages: ${data.images.pages.length}\nFavorites: ${data.num_favorites}`, chapters: 1, image: `https://${cdn}/galleries/${data.media_id}/cover.webp` }; } async findChapters(mangaId) { return [{ id: mangaId.toString(), title: "Chapter", number: 1, index: 0 }]; } async findChapterPages(chapterId) { const url = `${this.baseUrl}/g/${chapterId}/`; const { result } = await this.scrape( url, page => page.evaluate(() => { const html = document.documentElement.innerHTML; const cdnMatch = html.match(/image_cdn_urls:\s*(\[[^\]]*])/); const s = [...document.querySelectorAll("script")] .find(x => x.textContent.includes("JSON.parse") && !x.textContent.includes("media_server") && !x.textContent.includes("avatar_url") ); return { script: s?.textContent || null, cdns: cdnMatch ? JSON.parse(cdnMatch[1]) : ["i.nhentai.net"] }; }) ); if (!result?.script) throw new Error("Datos no encontrados"); const data = this.extractJson(result.script); const cdn = result.cdns[0]; return data.images.pages.map((p, i) => { const ext = p.t === "j" ? "jpg" : p.t === "p" ? "png" : "webp"; return { index: i, url: `https://${cdn}/galleries/${data.media_id}/${i + 1}.${ext}` }; }); } } module.exports = NHentai;