updated marketplace and extensions
This commit is contained in:
244
book/nhentai.js
244
book/nhentai.js
@@ -1,182 +1,140 @@
|
||||
class nhentai {
|
||||
class NHentai {
|
||||
constructor() {
|
||||
this.baseUrl = "https://nhentai.net";
|
||||
this.type = "book-board";
|
||||
this.version = "1.1";
|
||||
this.mediaType = "manga";
|
||||
this.version = "1.0"
|
||||
}
|
||||
|
||||
async search(queryObj) {
|
||||
const q = queryObj.query.trim().replace(/\s+/g, "+");
|
||||
const url = q
|
||||
? `${this.baseUrl}/search/?q=${q}`
|
||||
: `${this.baseUrl}/?q=`;
|
||||
shortenTitle(title) {
|
||||
return title.replace(/(\[[^]]*]|[({][^)}]*[)}])/g, "").trim();
|
||||
}
|
||||
|
||||
const { result: data } = await this.scrape(
|
||||
url,
|
||||
async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const container = document.querySelector('.container.index-container');
|
||||
if (!container) return {results: [], hasNextPage: false};
|
||||
parseId(str) {
|
||||
return str.replace(/\D/g, "");
|
||||
}
|
||||
|
||||
const galleryEls = container.querySelectorAll('.gallery');
|
||||
const results = [];
|
||||
extractJson(scriptText) {
|
||||
const m = scriptText.match(/JSON\.parse\("([\s\S]*?)"\)/);
|
||||
if (!m) throw new Error("JSON.parse no encontrado");
|
||||
|
||||
galleryEls.forEach(el => {
|
||||
const a = el.querySelector('a.cover');
|
||||
if (!a) return;
|
||||
|
||||
const href = a.getAttribute('href');
|
||||
const id = href.match(/\d+/)?.[0] || null;
|
||||
|
||||
const img = a.querySelector('img.lazyload');
|
||||
const thumbRaw = img?.dataset?.src || img?.src || "";
|
||||
const thumb = thumbRaw.startsWith("//") ? "https:" + thumbRaw : thumbRaw;
|
||||
const coverUrl = thumb.replace("thumb", "cover");
|
||||
|
||||
const caption = a.querySelector('.caption');
|
||||
const title = caption?.textContent.trim() || "";
|
||||
|
||||
results.push({
|
||||
id,
|
||||
title,
|
||||
image: coverUrl,
|
||||
rating: null,
|
||||
type: "book"
|
||||
});
|
||||
});
|
||||
|
||||
const hasNextPage = !!document.querySelector('section.pagination a.next');
|
||||
return {results, hasNextPage};
|
||||
});
|
||||
},
|
||||
{
|
||||
waitSelector: '.container.index-container',
|
||||
timeout: 55000
|
||||
}
|
||||
const unicodeFixed = m[1].replace(
|
||||
/\\u([0-9A-Fa-f]{4})/g,
|
||||
(_, h) => String.fromCharCode(parseInt(h, 16))
|
||||
);
|
||||
|
||||
return data?.results || [];
|
||||
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 { result: data } = await this.scrape(
|
||||
`${this.baseUrl}/g/${id}/`,
|
||||
async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const title = document.querySelector('h1.title .pretty')?.textContent?.trim() || "";
|
||||
const url = `${this.baseUrl}/g/${id}/`;
|
||||
|
||||
const img = document.querySelector('#cover img');
|
||||
const image =
|
||||
img?.dataset?.src ? "https:" + img.dataset.src :
|
||||
img?.src?.startsWith("//") ? "https:" + img.src :
|
||||
img?.src || "";
|
||||
const { result } = await this.scrape(
|
||||
url,
|
||||
page =>
|
||||
page.evaluate(() => {
|
||||
const html = document.documentElement.innerHTML;
|
||||
|
||||
const tagBlock = document.querySelector('.tag-container.field-name');
|
||||
const genres = tagBlock
|
||||
? [...tagBlock.querySelectorAll('.tags .name')].map(x => x.textContent.trim())
|
||||
: [];
|
||||
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 timeEl = document.querySelector('.tag-container.field-name time');
|
||||
const published =
|
||||
timeEl?.getAttribute("datetime") ||
|
||||
timeEl?.textContent?.trim() ||
|
||||
"???";
|
||||
const thumbMatch = html.match(/thumb_cdn_urls:\s*(\[[^\]]*])/);
|
||||
const thumbCdns = thumbMatch ? JSON.parse(thumbMatch[1]) : [];
|
||||
|
||||
return {title, image, genres, published};
|
||||
});
|
||||
},
|
||||
{
|
||||
waitSelector: "#bigcontainer",
|
||||
timeout: 55000
|
||||
}
|
||||
return { script, thumbCdns };
|
||||
})
|
||||
);
|
||||
|
||||
if (!data) throw new Error(`Fallo al obtener metadatos para ID ${id}`);
|
||||
if (!result?.script) {
|
||||
throw new Error("Script de datos no encontrado");
|
||||
}
|
||||
|
||||
const formattedDate = data.published
|
||||
? new Date(data.published).toLocaleDateString("es-ES")
|
||||
: "???";
|
||||
const data = this.extractJson(result.script);
|
||||
const cdn = result.thumbCdns[0] || "t3.nhentai.net";
|
||||
|
||||
return {
|
||||
id,
|
||||
title: data.title || "",
|
||||
format: "Manga",
|
||||
score: 0,
|
||||
genres: Array.isArray(data.genres) ? data.genres : [],
|
||||
status: "Finished",
|
||||
published: formattedDate,
|
||||
summary: "",
|
||||
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: data.image || ""
|
||||
image: `https://${cdn}/galleries/${data.media_id}/cover.webp`
|
||||
};
|
||||
}
|
||||
|
||||
async findChapters(mangaId) {
|
||||
const { result: data } = await this.scrape(
|
||||
`${this.baseUrl}/g/${mangaId}/`,
|
||||
async (page) => {
|
||||
return page.evaluate(() => {
|
||||
const title = document.querySelector('#info > h1 .pretty')?.textContent?.trim() || "";
|
||||
|
||||
const img = document.querySelector('#cover img');
|
||||
const cover =
|
||||
img?.dataset?.src ? "https:" + img.dataset.src :
|
||||
img?.src?.startsWith("//") ? "https:" + img.src :
|
||||
img?.src || "";
|
||||
|
||||
const hash = cover.match(/galleries\/(\d+)\//)?.[1] || null;
|
||||
|
||||
const thumbs = document.querySelectorAll('.thumbs img');
|
||||
const pages = thumbs.length;
|
||||
|
||||
const first = thumbs[0];
|
||||
const s = first?.dataset?.src || first?.src || "";
|
||||
const ext = s.match(/t\.(\w+)/)?.[1] || "jpg";
|
||||
|
||||
const langTag = [...document.querySelectorAll('#tags .tag-container')]
|
||||
.find(x => x.textContent.includes("Languages:"));
|
||||
|
||||
const language = langTag?.querySelector('.tags .name')?.textContent?.trim() || "";
|
||||
|
||||
return {title, cover, hash, pages, ext, language};
|
||||
});
|
||||
},
|
||||
{
|
||||
waitSelector: '#bigcontainer',
|
||||
timeout: 55000
|
||||
}
|
||||
);
|
||||
|
||||
if (!data?.hash) throw new Error(`Fallo al obtener hash para ID ${mangaId}`);
|
||||
|
||||
const encodedChapterId = Buffer.from(JSON.stringify({
|
||||
hash: data.hash,
|
||||
pages: data.pages,
|
||||
ext: data.ext
|
||||
})).toString("base64");
|
||||
|
||||
return [{
|
||||
id: encodedChapterId,
|
||||
title: data.title,
|
||||
id: mangaId.toString(),
|
||||
title: "Chapter",
|
||||
number: 1,
|
||||
releaseDate: null,
|
||||
index: 0,
|
||||
index: 0
|
||||
}];
|
||||
}
|
||||
|
||||
async findChapterPages(chapterId) {
|
||||
const decoded = JSON.parse(Buffer.from(chapterId, "base64").toString("utf8"));
|
||||
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"]
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const { hash, pages, ext } = decoded;
|
||||
const baseUrl = "https://i.nhentai.net/galleries";
|
||||
if (!result?.script) throw new Error("Datos no encontrados");
|
||||
const data = this.extractJson(result.script);
|
||||
const cdn = result.cdns[0];
|
||||
|
||||
return Array.from({ length: pages }, (_, i) => ({
|
||||
url: `${baseUrl}/${hash}/${i + 1}.${ext}`,
|
||||
index: i,
|
||||
headers: { Referer: `https://nhentai.net/g/${hash}/` }
|
||||
}));
|
||||
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;
|
||||
module.exports = NHentai;
|
||||
Reference in New Issue
Block a user