@yeci226/nhentai-ts
Version:
A scraper for NHentai with types
528 lines (518 loc) • 18.2 kB
JavaScript
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
};