243 lines
6.9 KiB
JavaScript
243 lines
6.9 KiB
JavaScript
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; |