182 lines
5.4 KiB
JavaScript
182 lines
5.4 KiB
JavaScript
class nhentai {
|
|
constructor() {
|
|
this.baseUrl = "https://nhentai.net";
|
|
this.type = "book-board";
|
|
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=`;
|
|
|
|
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};
|
|
|
|
const galleryEls = container.querySelectorAll('.gallery');
|
|
const results = [];
|
|
|
|
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
|
|
}
|
|
);
|
|
|
|
return data?.results || [];
|
|
}
|
|
|
|
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 img = document.querySelector('#cover img');
|
|
const image =
|
|
img?.dataset?.src ? "https:" + img.dataset.src :
|
|
img?.src?.startsWith("//") ? "https:" + img.src :
|
|
img?.src || "";
|
|
|
|
const tagBlock = document.querySelector('.tag-container.field-name');
|
|
const genres = tagBlock
|
|
? [...tagBlock.querySelectorAll('.tags .name')].map(x => x.textContent.trim())
|
|
: [];
|
|
|
|
const timeEl = document.querySelector('.tag-container.field-name time');
|
|
const published =
|
|
timeEl?.getAttribute("datetime") ||
|
|
timeEl?.textContent?.trim() ||
|
|
"???";
|
|
|
|
return {title, image, genres, published};
|
|
});
|
|
},
|
|
{
|
|
waitSelector: "#bigcontainer",
|
|
timeout: 55000
|
|
}
|
|
);
|
|
|
|
if (!data) throw new Error(`Fallo al obtener metadatos para ID ${id}`);
|
|
|
|
const formattedDate = data.published
|
|
? new Date(data.published).toLocaleDateString("es-ES")
|
|
: "???";
|
|
|
|
return {
|
|
id,
|
|
title: data.title || "",
|
|
format: "Manga",
|
|
score: 0,
|
|
genres: Array.isArray(data.genres) ? data.genres : [],
|
|
status: "Finished",
|
|
published: formattedDate,
|
|
summary: "",
|
|
chapters: 1,
|
|
image: data.image || ""
|
|
};
|
|
}
|
|
|
|
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,
|
|
number: 1,
|
|
releaseDate: null,
|
|
index: 0,
|
|
}];
|
|
}
|
|
|
|
async findChapterPages(chapterId) {
|
|
const decoded = JSON.parse(Buffer.from(chapterId, "base64").toString("utf8"));
|
|
|
|
const { hash, pages, ext } = decoded;
|
|
const baseUrl = "https://i.nhentai.net/galleries";
|
|
|
|
return Array.from({ length: pages }, (_, i) => ({
|
|
url: `${baseUrl}/${hash}/${i + 1}.${ext}`,
|
|
index: i,
|
|
headers: { Referer: `https://nhentai.net/g/${hash}/` }
|
|
}));
|
|
}
|
|
}
|
|
|
|
module.exports = nhentai; |