class NHentai { constructor() { this.baseUrl = "https://nhentai.net"; this.type = "book-board"; this.version = "1.1"; this.mediaType = "manga"; } 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 }) { if (query.startsWith("id:") || (!isNaN(query) && query.length <= 7)) { return [await this.getMetadata(this.parseId(query))]; } const url = `${this.baseUrl}/search/?q=${encodeURIComponent(query)}&page=${page}`; 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;