UNPKG

@yeci226/nhentai-ts

Version:

A scraper for NHentai with types

528 lines (518 loc) 18.2 kB
var __accessCheck = (obj, member, msg) => { if (!member.has(obj)) throw TypeError("Cannot " + msg); }; var __privateGet = (obj, member, getter) => { __accessCheck(obj, member, "read from private field"); return getter ? getter.call(obj) : member.get(obj); }; var __privateAdd = (obj, member, value) => { if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); member instanceof WeakSet ? member.add(obj) : member.set(obj, value); }; var __privateSet = (obj, member, value, setter) => { __accessCheck(obj, member, "write to private field"); setter ? setter.call(obj, value) : member.set(obj, value); return value; }; // src/lib/Classes/List.ts import axios from "axios"; import { load } from "cheerio"; var List = class { constructor(title, id, cover, url) { this.title = title; this.id = id; this.cover = cover; this.url = url; } /** * Gets the contents of a doujin * @returns The contents of the doujin */ async getContents() { return await axios.get(this.url).then( ({ data }) => parseDoujinInfo( load(data), this.url.split("nhentai.")[1].split("/")[0] ) ).catch((err) => { throw new Error(err.message); }); } }; // src/lib/constants.ts var sites = ["nhentai.to", "nhentai.net", "nhentai.website"]; var baseURLS = { to: "https://nhentai.to", net: "https://nhentai.net", website: "https://nhentai.website" }; var imageSites = { to: "cdn.dogehls.xyz", net: /t[357].nhentai.net/, website: "cdn.dogehls.xyz" }; // src/lib/Classes/NHentai.ts import axios3 from "axios"; import { load as load3 } from "cheerio"; import { CookieJar } from "tough-cookie"; import { HttpsCookieAgent } from "http-cookie-agent/http"; // src/lib/util.ts import axios2 from "axios"; import { load as load2 } from "cheerio"; var clean = (x) => { const result = []; x.forEach((a) => { const text = a.split(/\d/g)[0].trim(); if (text !== "") result.push(text); }); return result; }; var getExtension = (type) => { switch (type) { case "g": return "gif"; case "j": return "jpg"; default: return "png"; } }; var getAPIGalleryPages = async (axios5, data) => { const $ = load2(data); const id = ($("#cover").find("a").attr("href") || "g/").split("g/")[1].split("/")[0]; return (await axios5.get( "https://nhentai.net/api/gallery/".concat(id) )).data.images.pages; }; var getPageStatus = (url) => axios2.head(url).then((res) => res.status).catch((err) => { var _a; return ((_a = err.response) == null ? void 0 : _a.status) || 500; }); // src/lib/Classes/NHentai.ts var _axios; var NHentai = class { /** * Constructs an instance of the NHentai class * @param _options Options of the NHentai class */ constructor(_options = { site: "https://nhentai.to" }) { this._options = _options; __privateAdd(this, _axios, void 0); /** * Gets a random doujin * @returns Info of the random doujin */ this.getRandom = async () => await __privateGet(this, _axios).get("".concat(this._options.site, "/random")).then( async ({ data }) => parseDoujinInfo( load3(data), this.getSiteName(), this._options.site.includes("net") ? await getAPIGalleryPages(__privateGet(this, _axios), data) : void 0 ) ).catch((err) => { throw new Error(err.message); }); /** * Explores the list of doujin * @param page Page number of the list * @returns The doujin list */ this.explore = async (page = 1) => { if (isNaN(page) || page < 1) page = 1; return await __privateGet(this, _axios).get("".concat(this._options.site, "?page=").concat(page)).then(({ data }) => parseDoujinList(load3(data), this.getSiteName())).catch((err) => { throw new Error(err.message); }); }; /** * Searches for a doujin by a query * @param query Query of the doujin to search * @param options Options for searching * @returns The result of the search */ this.search = async (query, options) => { if (!query) throw new Error("The 'query' parameter shouldn't be undefined"); let page = 1; if ((options == null ? void 0 : options.page) && options.page > 0) page = options.page; return await __privateGet(this, _axios).get("".concat(this._options.site, "/search?q=").concat(query, "&page=").concat(page)).then((res) => { const results = parseDoujinList( load3(res.data), this.getSiteName() ); if (!results.data.length) throw new Error("No search results found"); return results; }); }; /** * Searches Tag for a doujin by a query * @param query Tag of the doujin to search * @param options Options for searching * @returns The result of the search */ this.searchWithTag = async (query, options) => { if (!query) throw new Error("The 'query' parameter shouldn't be undefined"); let page = 1; if ((options == null ? void 0 : options.page) && options.page > 0) page = options.page; return await __privateGet(this, _axios).get( "".concat(this._options.site, "/tag/").concat(query).concat(page > 1 ? "?page=".concat(page) : "") ).then((res) => { const results = parseDoujinList( load3(res.data), this.getSiteName() ); if (!results.data.length) throw new Error("No tags results found"); return results; }); }; /** * Searches artist for a doujin by a query * @param query Artist of the doujin to search * @param options Options for searching * @returns The result of the search */ this.searchWithArtist = async (query, options) => { if (!query) throw new Error("The 'query' parameter shouldn't be undefined"); let page = 1; if ((options == null ? void 0 : options.page) && options.page > 0) page = options.page; return await __privateGet(this, _axios).get( "".concat(this._options.site, "/artist/").concat(query).concat(page > 1 ? "?page=".concat(page) : "") ).then((res) => { const results = parseDoujinList( load3(res.data), this.getSiteName() ); if (!results.data.length) throw new Error("No artists results found"); return results; }); }; /** * Searches parody for a doujin by a query * @param query Parody of the doujin to search * @param options Options for searching * @returns The result of the search */ this.searchWithParody = async (query, options) => { if (!query) throw new Error("The 'query' parameter shouldn't be undefined"); let page = 1; if ((options == null ? void 0 : options.page) && options.page > 0) page = options.page; return await __privateGet(this, _axios).get( "".concat(this._options.site, "/parody/").concat(query).concat(page > 1 ? "?page=".concat(page) : "") ).then((res) => { const results = parseDoujinList( load3(res.data), this.getSiteName() ); if (!results.data.length) throw new Error("No parodies results found"); return results; }); }; /** * Searches character for a doujin by a query * @param query Character of the doujin to search * @param options Options for searching * @returns The result of the search */ this.searchWithCharacter = async (query, options) => { if (!query) throw new Error("The 'query' parameter shouldn't be undefined"); let page = 1; if ((options == null ? void 0 : options.page) && options.page > 0) page = options.page; return await __privateGet(this, _axios).get( "".concat(this._options.site, "/character/").concat(query).concat(page > 1 ? "?page=".concat(page) : "") ).then((res) => { const results = parseDoujinList( load3(res.data), this.getSiteName() ); if (!results.data.length) throw new Error("No characters results found"); return results; }); }; /** * Gets the info of a doujin by its ID * @param id ID of the doujin * @returns Info of the doujin */ this.getDoujin = async (id) => { if (!id) throw new Error("The 'id' parameter shouldn't be undefined"); const valid = await this.validate(id); if (!valid) throw new Error("Invalid doujin ID"); return await __privateGet(this, _axios).get("".concat(this._options.site, "/g/").concat(id)).then( async (res) => parseDoujinInfo( load3(res.data), this.getSiteName(), this._options.site.includes("net") ? await getAPIGalleryPages(__privateGet(this, _axios), res.data) : void 0 ) ).catch((err) => { throw new Error(err.message); }); }; /** * Validates the ID of a doujin * @param id ID of the doujin to check */ this.validate = (id) => __privateGet(this, _axios).get("".concat(this._options.site, "/g/").concat(id)).then(() => true).catch(() => false); __privateSet(this, _axios, axios3); const siteName = this._options.site.replace("https://", "").replace("http://", "").replace(/\/$/, ""); if (!sites.includes(siteName)) { this._options.site = "https://nhentai.to"; } if (!this._options.site.startsWith("https://")) this._options.site = "https://".concat(this._options.site); if (this._options.site.includes("nhentai.net") && (!this._options.cookie_value || !this._options.user_agent)) throw new Error( "Assign the ".concat(!this._options.cookie_value ? "'cookie_value'" : "'user_agent'", " in the instance of the class to use this site.") ); if (this._options.cookie_value) { const jar = new CookieJar(); jar.setCookie(this._options.cookie_value, this._options.site); const httpsAgent = new HttpsCookieAgent({ cookies: { jar } }); __privateSet(this, _axios, axios3.create({ httpsAgent })); } if (this._options.user_agent) __privateGet(this, _axios).defaults.headers.common["User-Agent"] = this._options.user_agent; } /** * Helper method to extract site name from URL */ getSiteName() { const siteName = this._options.site.replace("https://", "").replace("http://", "").replace(/\/$/, ""); if (siteName.includes("nhentai.to")) return "to"; if (siteName.includes("nhentai.net")) return "net"; if (siteName.includes("nhentai.website")) return "website"; return "to"; } }; _axios = new WeakMap(); // src/lib/Classes/Pages.ts import PDFDocument from "pdfkit"; import { tmpdir } from "os"; import { writeFile, unlink, readFile, mkdir, stat } from "fs/promises"; import { createWriteStream, existsSync } from "fs"; import axios4 from "axios"; import JSZip from "jszip"; import { join } from "path"; var _title; var Pages = class { /** * * @param pages An array of URLS of the doujin pages */ constructor(pages, __title) { this.pages = pages; __privateAdd(this, _title, void 0); __privateSet(this, _title, __title); } async PDF(filename) { const pdf = new PDFDocument({ autoFirstPage: false }); const file = filename ? "".concat(filename).concat(filename.endsWith(".pdf") ? "" : ".pdf") : "".concat(tmpdir(), "/").concat(Math.random().toString(36), ".pdf"); const stream = createWriteStream(file); pdf.pipe(stream); for (const url of this.pages) { const { data } = await axios4.get(url, { headers: url.includes("cdn.dogehls.xyz") ? { Referer: "https://nhentai.to" } : {}, responseType: "arraybuffer" }); pdf.addPage(); pdf.image(new Uint8Array(data), 0, 0, { fit: [pdf.page.width, pdf.page.height] }); const index = this.pages.indexOf(url); if (index === this.pages.length - 1) pdf.end(); } await new Promise((resolve, reject) => { stream.on("finish", () => resolve(file)); stream.on("error", reject); }); if (filename) return file; const buffer = await readFile(file); await unlink(file); return buffer; } async zip(filename) { const zip = new JSZip(); const folder = zip.folder(__privateGet(this, _title)); for (const url of this.pages) folder.file( "".concat(this.pages.indexOf(url) + 1, ".").concat(url.split(".")[url.split(".").length - 1]), new Uint8Array( (await axios4.get(url, { headers: url.includes("cdn.dogehls.xyz") ? { Referer: "https://nhentai.to" } : {}, responseType: "arraybuffer" })).data ), { binary: true } ); const buffer = await zip.generateAsync({ type: "nodebuffer" }); if (filename) { await writeFile( "".concat(filename).concat(filename.endsWith(".zip") ? "" : ".zip"), new Uint8Array(buffer) ); return "".concat(filename).concat(filename.endsWith(".zip") ? "" : ".zip"); } return buffer; } /** * Downloads the pages of a doujin and saves all of it in a folder * @param folderName The name of the folder in which all of the pages should be saved */ async download(folderName) { if (!folderName) throw new Error( "No folder name provided to save the downloaded doujin pages" ); if (!existsSync(folderName)) await mkdir(folderName, { recursive: true }); const isDirectory = (await stat(folderName)).isDirectory(); if (!isDirectory) throw new Error( "Expected a directory for saving the downloads, but recieved a file." ); for (const url of this.pages) await writeFile( join( folderName, "".concat(this.pages.indexOf(url) + 1, ".").concat(url.split(".")[url.split(".").length - 1]) ), new Uint8Array( (await axios4.get(url, { responseType: "arraybuffer" })).data ) ); } }; _title = new WeakMap(); // src/Parser/list.ts var parseDoujinList = ($, site) => { const data = []; const baseURL = baseURLS[site]; const currentPage = Number($(".pagination").find(".page.current").text()); const totalPages = Number( ($(".pagination").find("a.last").attr("href") || "").split("page=")[1] ); const pagination = currentPage === 0 ? null : { currentPage, hasNextPage: totalPages > currentPage, totalPages }; $(".gallery").each((i, el) => { const contentElements = $(el).find("a"); const slug = contentElements.attr("href"); const id = slug ? slug.split("g/")[1].replace("/", "") : ""; const url = "".concat(baseURL, "/g/").concat(id); const coverSlug = contentElements.find("a > img").attr("data-src") || contentElements.find("a > img").attr("src"); const cover = coverSlug ? "".concat(coverSlug.startsWith("/galleries/") ? "https://t3.nhentai.net" : "").concat(coverSlug).replace("/g/", "/galleries/").replace(imageSites[site], "t3.nhentai.net") : null; const title = $(el).find(".caption").text().trim(); data.push(new List(title, id, cover, url)); }); return { pagination, data }; }; // src/Parser/doujin.ts var parseDoujinInfo = async ($, site, api_pages) => { var _a, _b; const pages = []; const dataSrc = $(".thumb-container").first().find("a > img").attr("data-src") || ""; const gallery_id = dataSrc.includes("/galleries/") ? ((_a = dataSrc.split("/galleries/")[1]) == null ? void 0 : _a.split("/")[0]) || "" : ""; if (site === "net" && api_pages) api_pages.forEach( (page, i) => pages.push( "https://i.nhentai.net/galleries/".concat(gallery_id, "/").concat(i + 1, ".").concat(getExtension(page.t)) ) ); else for (const el of $(".thumb-container")) { const url2 = ($(el).find("a > img").attr("data-src") || "").replace( /t(?=\.)/, "" ); if (url2) { const page = url2.replace(imageSites[site], "i.nhentai.net"); pages.push(page); } } const cover = $("#cover").find("a > img").attr("data-src") || $("#cover").find("a > img").attr("src"); const href = $("#cover").find("a").attr("href") || ""; const id = href.includes("g/") ? ((_b = href.split("g/")[1]) == null ? void 0 : _b.split("/")[0]) || "" : ""; const titles = { english: $("#info").find("h1").text().trim(), original: $("#info").find("h2").text().trim() }; const baseURL = baseURLS[site]; const parodies = []; const characters = []; const tags = []; const artists = []; const groups = []; const languages = []; const categories = []; $(".tag-container.field-name").each((i, el) => { const type = $(el).text().trim().toLowerCase(); const contents = []; const push = (field) => contents.filter((content) => content !== "").forEach((content) => field.push(content)); $(el).find(".tags").find("a").each((i2, el2) => { contents.push($(el2).text().trim().split("\n")[0]); }); type.startsWith("parodies") ? push(parodies) : type.startsWith("characters") ? push(characters) : type.startsWith("tags") ? push(tags) : type.startsWith("artists") ? push(artists) : type.startsWith("groups") ? push(groups) : type.startsWith("languages") ? push(languages) : push(categories); }); const url = "".concat(baseURL, "/g/").concat(id); const images = new Pages(pages, titles.english); return { id, title: titles.english, originalTitle: titles.original, parodies: clean(parodies), characters: clean(characters), tags: clean(tags), artists: clean(artists), groups: clean(groups), languages: clean(languages), categories: clean(categories), cover: !pages.some((url2) => url2.includes("cdn.dogehls.xyz")) ? cover.replace("cdn.dogehls.xyz", "t3.nhentai.net") : null, images, url }; }; export { sites, baseURLS, imageSites, List, parseDoujinList, parseDoujinInfo, clean, getExtension, getAPIGalleryPages, getPageStatus, NHentai, Pages };