Initial commit

このコミットが含まれているのは:
sinkaroid 2022-06-07 08:08:53 +07:00
コミット a1a8f85d9e
この署名に対応する既知のキーがデータベースに存在しません
GPGキーID: A7DF4E245FDD8159
35個のファイルの変更1243行の追加0行の削除

1
.env ノーマルファイル
ファイルの表示

@ -0,0 +1 @@
CF=cf_clearance=XZoRJIDGsHrMCuS3rFIH9jCLhoYmBHzUNs6dfVJwNqg-1654563641-0-150

41
.eslintrc.json ノーマルファイル
ファイルの表示

@ -0,0 +1,41 @@
{
"env": {
"es2021": true,
"node": true,
"commonjs": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"linebreak-style": 0,
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
],
"no-empty": "error",
"no-func-assign": "error",
"no-case-declarations": "off",
"no-unreachable": "error",
"no-eval": "error",
"no-global-assign": "error",
"@typescript-eslint/no-explicit-any": ["off"],
"indent": [
"error",
2
]
}
}

5
.gitignore vendored ノーマルファイル
ファイルの表示

@ -0,0 +1,5 @@
/node_modules
yarn.lock
/build
/playground
p.ts

21
LICENSE ノーマルファイル
ファイルの表示

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 sinkaroid.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1
Procfile ノーマルファイル
ファイルの表示

@ -0,0 +1 @@
web: npm start

49
package.json ノーマルファイル
ファイルの表示

@ -0,0 +1,49 @@
{
"name": "jandapress-cookie",
"version": "0.0.1",
"description": "Experimental doujin API with gather in mind",
"main": "build/src/index.js",
"scripts": {
"build": "rm -rf build && tsc",
"start": "node build/src/index.js",
"test": "ts-node test/test.ts",
"test:cf": "ts-node test/nhentaiCookietest.ts",
"start:prod": "npm run build && node build/src/index.js",
"start:dev": "nodemon src/index.ts",
"lint": "npx eslint . --ext .ts",
"lint:fix": "npx eslint . --fix",
"postinstall": "npm run build"
},
"keywords": [],
"author": "sinkaroid",
"license": "MIT",
"dependencies": {
"cheerio": "^1.0.0-rc.11",
"express": "^4.18.1",
"express-rate-limit": "^6.4.0",
"express-slow-down": "^1.4.0",
"http-cookie-agent": "^4.0.1",
"phin": "^3.6.1",
"pino": "^7.11.0",
"pino-pretty": "^8.0.0",
"tough-cookie": "^4.0.0"
},
"devDependencies": {
"@types/express-rate-limit": "^6.0.0",
"@types/express-slow-down": "^1.3.2",
"@types/node": "^14.14.37",
"@types/tough-cookie": "^4.0.2",
"@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.18.0",
"dotenv": "^16.0.1",
"eslint": "^7.32.0",
"nhentai-api": "^3.4.3",
"nodemon": "^2.0.15",
"npx": "^10.2.2",
"ts-node": "^10.8.1",
"typescript": "4.6.3"
},
"engines": {
"node": ">=16.9.1"
}
}

27
src/controller/hentai2read/hentai2readGet.ts ノーマルファイル
ファイルの表示

@ -0,0 +1,27 @@
import { scrapeContent } from "../../scraper/hentai2read/hentai2readGetController";
import c from "../../utils/options";
import { logger } from "../../utils/logger";
export async function getHentai2read(req: any, res: any) {
try {
const book = req.query.book || "";
if (!book) throw Error("Parameter book is required");
if (book.split("/").length !== 2) throw Error("Book must be in format 'book_example/chapter'. Example: 'fate_lewd_summoning/1'");
const url = `${c.HENTAI2READ}/${book}/`;
const data = await scrapeContent(url);
logger.info({
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent")
});
return res.json(data);
} catch (err: any) {
const example = {
"success": false,
"message": err.message
};
res.json(example);
}
}

ファイルの表示

@ -0,0 +1,22 @@
import { scrapeContent } from "../../scraper/hentai2read/hentai2readSearchController";
import c from "../../utils/options";
import { logger } from "../../utils/logger";
export async function searchHentai2read(req: any, res: any, next: any) {
try {
const key = req.query.key || "";
if (!key) throw Error("Parameter book is required");
const url = `${c.HENTAI2READ}/hentai-list/search/${key}`;
const data = await scrapeContent(url);
logger.info({
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent")
});
return res.json(data);
} catch (err: any) {
next(Error(err.message));
}
}

23
src/controller/hentaifox/hentaifoxGet.ts ノーマルファイル
ファイルの表示

@ -0,0 +1,23 @@
import { scrapeContent } from "../../scraper/hentaifox/hentaifoxGetController";
import c from "../../utils/options";
import { logger } from "../../utils/logger";
export async function getHentaifox(req: any, res: any, next: any) {
try {
const book = req.query.book || "";
if (!book) throw Error("Parameter book is required");
if (isNaN(book)) throw Error("Value must be number");
const url = `${c.HENTAIFOX}/gallery/${book}/`;
const data = await scrapeContent(url);
logger.info({
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent")
});
return res.json(data);
} catch (err: any) {
next(Error(err.message));
}
}

26
src/controller/hentaifox/hentaifoxSearch.ts ノーマルファイル
ファイルの表示

@ -0,0 +1,26 @@
import { scrapeContent } from "../../scraper/hentaifox/hentaifoxSearchController";
import c from "../../utils/options";
import { logger } from "../../utils/logger";
const sorting = ["latest", "popular"];
export async function searchHentaifox(req: any, res:any, next: any) {
try {
const key = req.query.key || "";
const page = req.query.page || 1;
const sort = req.query.sort || sorting[0];
if (!key) throw Error("Parameter key is required");
if (!sorting.includes(sort)) throw Error("Invalid short: " + sorting.join(", "));
const url = `${c.HENTAIFOX}/search/?q=${key}&sort=${sort}&page=${page}`;
const data = await scrapeContent(url);
logger.info({
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent")
});
return res.json(data);
} catch (err: any) {
next(Error(err.message));
}
}

29
src/controller/nhentai/nhentaiGet.ts ノーマルファイル
ファイルの表示

@ -0,0 +1,29 @@
import { scrapeContent } from "../../scraper/nhentai/nhentaiGetController";
import c from "../../utils/options";
import { logger } from "../../utils/logger";
//import { mock } from "../../utils/modifier";
export async function getNhentai(req: any, res: any) {
try {
const book = req.query.book || "";
if (!book) throw Error("Parameter book is required");
if (isNaN(book)) throw Error("Value must be number");
const url = `${c.NHENTAI}/api/gallery/${book}`;
const data = await scrapeContent(url);
logger.info({
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent")
});
return res.json(data);
} catch (err: any) {
const e = {
"success": false,
"message": err.message
};
res.json(e);
}
}

32
src/controller/nhentai/nhentaiRelated.ts ノーマルファイル
ファイルの表示

@ -0,0 +1,32 @@
import { scrapeContent } from "../../scraper/nhentai/nhentaiRelatedController";
import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { mock } from "../../utils/modifier";
export async function relatedNhentai(req: any, res: any) {
try {
const book = req.query.book || "";
if (!book) throw Error("Parameter book is required");
if (isNaN(book)) throw Error("Value must be number");
let actualAPI;
if (!await mock(c.NHENTAI)) actualAPI = c.NHENTAI_IP_2;
const url = `${actualAPI}/api/gallery/${book}/related`;
const data = await scrapeContent(url);
logger.info({
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent")
});
return res.json(data);
} catch (err: any) {
const e = {
"success": false,
"message": err.message
};
res.json(e);
}
}

35
src/controller/nhentai/nhentaiSearch.ts ノーマルファイル
ファイルの表示

@ -0,0 +1,35 @@
import { scrapeContent } from "../../scraper/nhentai/nhentaiSearchController";
import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { mock } from "../../utils/modifier";
const sorting = ["popular-today", "popular-week", "popular"];
export async function searchNhentai(req: any, res: any) {
try {
const key = req.query.key || "";
const page = req.query.page || 1;
const sort = req.query.sort || sorting[0];
if (!key) throw Error("Parameter key is required");
if (!sorting.includes(sort)) throw Error("Invalid short: " + sorting.join(", "));
let actualAPI;
if (!await mock(c.NHENTAI)) actualAPI = c.NHENTAI_IP_2;
const url = `${actualAPI}/api/galleries/search?query=${key}&sort=${sort}&page=${page}`;
const data = await scrapeContent(url);
logger.info({
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent")
});
return res.json(data);
} catch (err: any) {
const e = {
"success": false,
"message": err.message
};
res.json(e);
}
}

23
src/controller/pururin/pururinGet.ts ノーマルファイル
ファイルの表示

@ -0,0 +1,23 @@
import { scrapeContent } from "../../scraper/pururin/pururinGetController";
import c from "../../utils/options";
import { logger } from "../../utils/logger";
export async function getPururin(req: any, res: any, next: any) {
try {
const book = req.query.book || "";
if (!book) throw Error("Parameter book is required");
if (isNaN(book)) throw Error("Value must be number");
const url = `${c.PURURIN}/gallery/${book}/janda`;
const data = await scrapeContent(url);
logger.info({
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent")
});
return res.json(data);
} catch (err: any) {
next(Error(err.message));
}
}

26
src/controller/pururin/pururinSearch.ts ノーマルファイル
ファイルの表示

@ -0,0 +1,26 @@
import { scrapeContent } from "../../scraper/pururin/pururinSearchController";
import c from "../../utils/options";
import { logger } from "../../utils/logger";
const sorting = ["newest", "most-popular", "highest-rated", "most-viewed", "title", "random"];
export async function searchPururin(req: any, res: any, next: any) {
try {
const key = req.query.key || "";
const page = req.query.page || 1;
const sort = req.query.sort || sorting[0];
if (!key) throw Error("Parameter key is required");
if (!sorting.includes(sort)) throw Error("Invalid short: " + sorting.join(", "));
const url = `${c.PURURIN}/search/${sort}?q=${key}&page=${page}`;
const data = await scrapeContent(url);
logger.info({
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent")
});
return res.json(data);
} catch (err: any) {
next(Error(err.message));
}
}

ファイルの表示

@ -0,0 +1,31 @@
import { scrapeContent } from "../../scraper/simply-hentai/simply-hentaiGetController";
import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { mock } from "../../utils/modifier";
export async function getSimplyhentai(req: any, res: any) {
try {
const book = req.query.book || "";
if (!book) throw Error("Parameter book is required");
let actualAPI;
if (!await mock(c.SIMPLY_HENTAI)) actualAPI = c.SIMPLY_HENTAI_PROXIFIED;
const url = `${actualAPI}/${book}`;
const data = await scrapeContent(url);
logger.info({
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent")
});
return res.json(data);
} catch (err: any) {
const example = {
"success": false,
"message": err.message
};
res.json(example);
}
}

52
src/index.ts ノーマルファイル
ファイルの表示

@ -0,0 +1,52 @@
import express from "express";
import { Request, Response, NextFunction } from "express";
import scrapeRoutes from "./router/endpoint";
import { slow, limiter } from "./utils/limit-options";
import { logger } from "./utils/logger";
import { isNumeric } from "./utils/modifier";
import * as pkg from "../package.json";
const app = express();
app.get("/", slow, limiter, (req, res) => {
res.send({
success: true,
message: "Hi, I'm alive!",
endpoint: "https://github.com/sinkaroid/jandapress/blob/master/README.md#routing",
date: new Date().toLocaleString(),
version: `${pkg.version}`,
});
logger.info({
path: req.path,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent")
});
});
app.use(scrapeRoutes());
app.get("/g/:id", slow, limiter, (req, res) => {
if (!isNumeric(req.params.id)) throw Error("This path need required number to work");
res.redirect(301, `https://nhentai.net/g/${req.params.id}`);
});
app.use((req: Request, res: Response, next: NextFunction) => {
res.status(404);
next(Error(`The page not found in path ${req.url} and method ${req.method}`));
logger.error({
path: req.url,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent")
});
});
app.use((error: any, res: Response) => {
res.status(500).json({
message: error.message,
stack: error.stack
});
});
app.listen(process.env.PORT || 3000, () => console.log(`${pkg.name} running in port 3000`));

30
src/router/endpoint.ts ノーマルファイル
ファイルの表示

@ -0,0 +1,30 @@
import { Router } from "express";
import { searchHentaifox } from "../controller/hentaifox/hentaifoxSearch";
import { getHentaifox } from "../controller/hentaifox/hentaifoxGet";
import { getPururin } from "../controller/pururin/pururinGet";
import { searchPururin } from "../controller/pururin/pururinSearch";
import { searchHentai2read } from "../controller/hentai2read/hentai2readSearch";
import { getHentai2read } from "../controller/hentai2read/hentai2readGet";
import { getSimplyhentai } from "../controller/simply-hentai/simply-hentaiGet";
import { getNhentai } from "../controller/nhentai/nhentaiGet";
import { searchNhentai } from "../controller/nhentai/nhentaiSearch";
import { relatedNhentai } from "../controller/nhentai/nhentaiRelated";
import { slow, limiter } from "../utils/limit-options";
function scrapeRoutes() {
const router = Router();
router.get("/hentaifox/search", slow, limiter, searchHentaifox);
router.get("/hentaifox/get", slow, limiter, getHentaifox);
router.get("/pururin/get", slow, limiter, getPururin);
router.get("/pururin/search", slow, limiter, searchPururin);
router.get("/hentai2read/search", slow, limiter, searchHentai2read);
router.get("/hentai2read/get", slow, limiter, getHentai2read);
router.get("/simply-hentai/get", slow, limiter, getSimplyhentai);
router.get("/nhentai/get", slow, limiter, getNhentai);
router.get("/nhentai/search", slow, limiter, searchNhentai);
router.get("/nhentai/related", slow, limiter, relatedNhentai);
return router;
}
export default scrapeRoutes;

ファイルの表示

@ -0,0 +1,49 @@
import { load } from "cheerio";
import p from "phin";
import c from "../../utils/options";
interface IHentai2readGet {
title: string;
id: string;
image: string[];
}
interface IHentai2readGetPush {
data: object;
main_url: string;
current_url: string;
next_url?: string;
previus_url?: string;
}
export async function scrapeContent(url: string) {
try {
const res = await p(url);
const $ = load(res.body as Buffer);
const script = $("script").map((i, el) => $(el).text()).get();
//find 'var gData = {}' inside script
const gData = script.find(el => el.includes("var gData"));
const gDataClean: string = gData?.replace(/[\s\S]*var gData = /, "").replace(/;/g, "").replace(/'/g, "\"") || "";
const gDataJson = JSON.parse(gDataClean);
const images = gDataJson.images.map((el: any) => `https://cdn-ngocok-static.sinxdr.workers.dev/hentai${el}`);
const objectData: IHentai2readGet = {
title: gDataJson.title,
id: url.replace(c.HENTAI2READ, ""),
image: images
};
const data: IHentai2readGetPush = {
data: objectData,
main_url: gDataJson.mainURL,
current_url: gDataJson.currentURL,
next_url: gDataJson.nextURL,
previus_url: gDataJson.previousURL
};
return data;
} catch (err: any) {
throw Error(err.message);
}
}

ファイルの表示

@ -0,0 +1,44 @@
import { load } from "cheerio";
import p from "phin";
import c from "../../utils/options";
import { getId } from "../../utils/modifier";
interface IHentai2readSearch {
title: string;
cover: string;
id: string;
link: string;
message: string;
}
export async function scrapeContent(url: string) {
try {
const res = await p(url);
const $ = load(res.body as Buffer);
const title = $(".title-text").map((i, el) => $(el).text()).get();
const imgSrc = $("img").map((i, el) => $(el).attr("data-src")).get();
const id = $(".overlay-title").map((i, el) => $(el).children("a").attr("href")).get();
const idClean = id.map(el => getId(el));
const content = [];
for (const abc of title) {
const objectData: IHentai2readSearch = {
title: title[title.indexOf(abc)],
cover: `${c.HENTAI2READ}${imgSrc[title.indexOf(abc)]}`,
id: idClean[title.indexOf(abc)],
link: `${c.HENTAI2READ}${idClean[title.indexOf(abc)]}`,
message: "Required chapter number is mandatory",
};
content.push(objectData);
}
const data = {
data: content,
source: url,
};
return data;
} catch (err: any) {
throw Error(err.message);
}
}

ファイルの表示

@ -0,0 +1,56 @@
import { load } from "cheerio";
import p from "phin";
import c from "../../utils/options";
interface IHentaiFoxGet {
title: string;
id: number;
tags: string[];
type: string;
total: number;
image: string[];
}
export async function scrapeContent(url: string) {
try {
const res = await p(url);
const $ = load(res.body as Buffer);
const id = parseInt($("a.g_button")?.attr("href")?.split("/")[2] || "");
const category = $("a.tag_btn").map((i, abc) => {
return $(abc)?.text()?.replace(/[0-9]/g, "").trim();
}).get();
const imgSrc = $("img").map((i, el) => $(el).attr("data-src")).get();
const parameterImg = imgSrc[0].split("/").slice(0, imgSrc[0].split("/").length - 1).join("/");
const extensionImg = `.${imgSrc[0].split(".").slice(-1)[0]}`;
const info = $("span.i_text.pages").map((i, abc) => {
return $(abc).text();
}).get();
const pageCount = parseInt(info[0].replace(/[^0-9]/g, ""));
const image = [];
for (let i = 0; i < Number(pageCount); i++) {
image.push(`${parameterImg}/${i + 1}${extensionImg}`);
}
const titleInfo = $("div.info").children("h1").text();
const objectData: IHentaiFoxGet = {
title: titleInfo,
id: id,
tags: category,
type: extensionImg,
total: pageCount,
image: image,
};
const data = {
data: objectData,
source: `${c.HENTAIFOX}/gallery/${id}/`,
};
return data;
} catch (err: any) {
throw Error(err.message);
}
}

ファイルの表示

@ -0,0 +1,57 @@
import { load } from "cheerio";
import p from "phin";
import c from "../../utils/options";
interface IHentaiFoxSearch {
title: string;
cover: string;
id: number;
category: string;
link: string;
}
export async function scrapeContent(url: string) {
try {
const res = await p(url);
const $ = load(res.body as Buffer);
const title = $("h2.g_title").map((i, abc) => {
return $(abc).text();
}).get();
const link = $("h2.g_title").map((i, abc) => {
//return number only
return $(abc)?.children("a")?.attr("href")?.split("/")[2];
}).get();
const category = $("h3.g_cat").map((i, abc) => {
return $(abc)?.children("a")?.attr("href")?.split("/")[2];
}).get();
const imgSrc = $("img").map((i, el) => $(el).attr("data-cfsrc")).get();
const imgSrcClean = imgSrc.slice(0, imgSrc.length - 1);
const content = [];
for (const abc of title) {
const objectData: IHentaiFoxSearch = {
title: title[title.indexOf(abc)],
cover: imgSrcClean[title.indexOf(abc)],
id: parseInt(link[title.indexOf(abc)]),
category: category[title.indexOf(abc)],
link: `${c.HENTAIFOX}/gallery/${link[title.indexOf(abc)]}`,
};
content.push(objectData);
}
const data = {
data: content.filter(con => con.category !== ""),
page: Number(url.split("&page=")[1]),
sort: url.split("&sort=")[1].split("&")[0],
source: url,
};
return data;
} catch (err: any) {
throw Error(err.message);
}
}

114
src/scraper/nhentai/nhentaiGetController.ts ノーマルファイル
ファイルの表示

@ -0,0 +1,114 @@
import p from "phin";
import c from "../../utils/options";
import { getDate, timeAgo } from "../../utils/modifier";
import * as pkg from "../../../package.json";
import { CookieJar } from "tough-cookie";
import { HttpsCookieAgent } from "http-cookie-agent/http";
import { config } from "dotenv";
config();
interface INhentaiGet {
title: string;
optional_title: object;
id: number;
language: string;
tags: string[];
total: number;
image: string[];
num_pages: number;
num_favorites: number;
artist: string[];
group: string;
parodies: string;
characters: string[];
upload_date: string;
}
const jar = new CookieJar();
jar.setCookie(process.env.CF as string, "https://nhentai.net/");
export async function scrapeContent(url: string) {
try {
const res = await p({
url: url,
core: {
agent: new HttpsCookieAgent({ cookies: { jar, }, }),
},
"headers": {
"User-Agent": `${pkg.name}/${pkg.version} Node.js/16.9.1`
},
parse: "json",
});
console.log(res.statusCode);
const GALLERY = "https://i.nhentai.net/galleries";
const TYPE: any = {
j: "jpg",
p: "png",
g: "gif",
};
const dataRaw: any = res.body;
const imagesRaw = dataRaw.images.pages;
const images: string[] = Object.keys(imagesRaw)
.map((key: string) => imagesRaw[key].t);
const imageList = [];
for (let i = 0; i < images.length; i++) {
imageList.push(`${GALLERY}/${dataRaw.media_id}/${i + 1}.${TYPE[images[i]]}`);
}
//get all tags.name
const tagsRaw = dataRaw.tags;
const tags: string[] = Object.keys(tagsRaw).map((key: string) => tagsRaw[key].name);
const artistRaw = tagsRaw.filter((tag: any) => tag.type === "artist");
const artist: string[] = artistRaw.map((tag: any) => tag.name) || [];
//find "type": "language" in tagsRaw
const languageRaw = tagsRaw.find((tag: any) => tag.type === "language");
const language = languageRaw ? languageRaw.name : null;
const parodiesRaw = tagsRaw.find((tag: any) => tag.type === "parody");
const parodies = parodiesRaw ? parodiesRaw.name : null;
const groupRaw = tagsRaw.find((tag: any) => tag.type === "group");
const group = groupRaw ? groupRaw.name : null;
//get all "type": "character" in tagsRaw
const charactersRaw = tagsRaw.filter((tag: any) => tag.type === "character");
const characters: string[] = charactersRaw.map((tag: any) => tag.name) || [];
const time = new Date(dataRaw.upload_date * 1000);
const objectData: INhentaiGet = {
title: dataRaw.title.pretty,
optional_title: {
english: dataRaw.title.english,
japanese: dataRaw.title.japanese,
pretty: dataRaw.title.pretty,
},
id: dataRaw.id,
language: language,
tags: tags,
total: imageList.length,
image: imageList,
num_pages: dataRaw.num_pages,
num_favorites: dataRaw.num_favorites,
artist: artist,
group: group,
parodies: parodies,
characters: characters,
upload_date: `${getDate(time)} (${timeAgo(time)})`,
};
const data = {
data: objectData,
source: `${c.NHENTAI}/g/${dataRaw.id}`,
};
return data;
} catch (err: any) {
throw Error(err.message);
}
}

ファイルの表示

@ -0,0 +1,40 @@
import p from "phin";
import c from "../../utils/options";
import { getDate, timeAgo } from "../../utils/modifier";
interface INhentaiRelated {
title: string;
id: number;
upload_date: string;
total: number;
tags: string[];
}
export async function scrapeContent(url: string) {
try {
const res = await p({ url: url, parse: "json" });
const rawData: any = res.body;
const content = [];
for (let i = 0; i < rawData.result.length; i++) {
const time = new Date(rawData.result[i].upload_date * 1000);
const objectData: INhentaiRelated = {
title: rawData.result[i].title,
id: rawData.result[i].id,
upload_date: `${getDate(time)} (${timeAgo(time)})`,
total: rawData.result[i].num_pages,
tags: rawData.result[i].tags.map((tag: any) => tag.name),
};
content.push(objectData);
}
const data = {
data: content,
source: url.replace(c.NHENTAI_IP, c.NHENTAI),
};
return data;
} catch (err: any) {
throw Error(err.message);
}
}

ファイルの表示

@ -0,0 +1,52 @@
import p from "phin";
import c from "../../utils/options";
import { getDate, timeAgo } from "../../utils/modifier";
interface INhentaiSearch {
title: string;
id: number;
language: string;
upload_date: string;
total: number;
cover: string;
tags: string[];
}
export async function scrapeContent(url: string) {
try {
const res = await p({ url: url, parse: "json" });
const rawData: any = res.body;
const content = [];
const GALLERY = "https://i.nhentai.net/galleries";
const TYPE: any = {
j: "jpg",
p: "png",
g: "gif",
};
for (let i = 0; i < rawData.result.length; i++) {
const time = new Date(rawData.result[i].upload_date * 1000);
const objectData: INhentaiSearch = {
title: rawData.result[i].title,
id: rawData.result[i].id,
language: rawData.result[i].tags.find((tag: any) => tag.type === "language") ? rawData.result[i].tags.find((tag: any) => tag.type === "language").name : null,
upload_date: `${getDate(time)} (${timeAgo(time)})`,
total: rawData.result[i].num_pages,
cover: `${GALLERY}/${rawData.result[i].media_id}/1.${TYPE[rawData.result[i].images.cover.t]}`,
tags: rawData.result[i].tags.map((tag: any) => tag.name),
};
content.push(objectData);
}
const data = {
data: content,
page: Number(url.split("&page=")[1]),
sort: url.split("&sort=")[1].split("&")[0],
source: url.replace(c.NHENTAI_IP, c.NHENTAI),
};
return data;
} catch (err: any) {
throw Error(err.message);
}
}

57
src/scraper/pururin/pururinGetController.ts ノーマルファイル
ファイルの表示

@ -0,0 +1,57 @@
import { load } from "cheerio";
import p from "phin";
import c from "../../utils/options";
import { getPururinInfo, getUrl } from "../../utils/modifier";
interface IGetPururin {
title: string;
id: number;
tags: string[];
extension: string;
total: number;
image: string[];
}
interface IData{
data: object;
source: string;
}
export async function scrapeContent(url: string) {
try {
const res = await p(url);
const $ = load(res.body);
const title: string = $("div.content-wrapper h1").html() || "";
const tags: string[] = $("div.content-wrapper ul.list-inline li").map((i, abc) => {
return getPururinInfo($(abc).text());
}).get();
const cover = $("meta[property='og:image']").attr("content");
const extension = `.${cover?.split(".").pop()}`;
const total: number = parseInt($("gallery-thumbnails").attr(":total") || "0");
const id: number = parseInt($("gallery-thumbnails").attr(":id") || "0");
const image = [];
for (let i = 0; i < total; i++) {
image.push(`${getUrl(cover?.replace("cover", `${i + 1}`) ?? "")}`);
}
const objectData: IGetPururin = {
title,
id,
tags,
extension,
total,
image
};
const data: IData = {
data: objectData,
source: `${c.PURURIN}/gallery/${id}/janda`
};
return data;
} catch (err: any) {
throw Error(err.message);
}
}

ファイルの表示

@ -0,0 +1,63 @@
import { load } from "cheerio";
import p from "phin";
import c from "../../utils/options";
import { isText } from "domhandler";
import { getPururinInfo, getPururinPageCount } from "../../utils/modifier";
interface ISearchPururin {
title: string;
cover: string;
id: number;
info: string;
link: string;
total: number;
}
interface IData {
data: object;
page: number;
sort: string;
source: string;
}
export async function scrapeContent(url: string) {
try {
const res = await p(url);
const $ = load(res.body);
const dataRaw = $("img.card-img-top");
const info = $("div.info");
const infoBook = [];
for (let i = 0; i < info.length; i++) {
const child = info[i].children[0];
if (isText(child)) {
infoBook.push(getPururinInfo(child.data));
}
}
const content = [];
for (const abc of dataRaw) {
const objectData: ISearchPururin = {
title: abc.attribs["alt"],
cover: abc.attribs["data-src"].replace(/^\/\//, "https://"),
id: parseInt(abc.attribs["data-src"].split("data/")[1].split("/cover")[0]),
info: infoBook[dataRaw.index(abc)],
link: `${c.PURURIN}/gallery/${abc.attribs["data-src"].split("data/")[1].split("/cover")[0]}/janda`,
total: getPururinPageCount(infoBook[dataRaw.index(abc)])
};
content.push(objectData);
}
const data: IData = {
data: content,
page: parseInt(url.split("&page=")[1]),
sort: url.split("/search/")[1].split("?")[0],
source: c.PURURIN
};
return data;
} catch (err: any) {
throw Error(err.message);
}
}

ファイルの表示

@ -0,0 +1,45 @@
import { load } from "cheerio";
import p from "phin";
import c from "../../utils/options";
interface ISimplyHentaiGet {
title: string;
id: string;
tags: string[];
total: number;
image: string[];
language: string;
}
export async function scrapeContent(url: string) {
try {
const res = await p(url);
const $ = load(res.body as Buffer);
const script = $("script#__NEXT_DATA__");
const json = JSON.parse(script.html() as string);
const dataScrape: any = json.props.pageProps.data.pages;
const images: string[] = Object.keys(dataScrape)
.map((key: string) => dataScrape[key].sizes.full);
const tagsRaw: any = json.props.pageProps.data.tags;
const tags: string[] = Object.keys(tagsRaw).map((key: string) => tagsRaw[key].slug);
const language = json.props.pageProps.data.language;
const metaRaw= json.props.pageProps.meta;
const objectData: ISimplyHentaiGet = {
title: metaRaw.title,
id: url.replace(c.SIMPLY_HENTAI_PROXIFIED, ""),
tags: tags,
total: images.length,
image: images,
language: language.slug
};
const data = {
data: objectData,
source: url,
};
return data;
} catch (err: any) {
throw Error(err.message);
}
}

17
src/utils/limit-options.ts ノーマルファイル
ファイルの表示

@ -0,0 +1,17 @@
import rateLimit from "express-rate-limit";
import slowDown from "express-slow-down";
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 50,
message: "Too nasty, please slow down"
});
const slow = slowDown({
delayAfter: 50,
windowMs: 15 * 60 * 1000,
delayMs: 1000,
maxDelayMs: 20000,
});
export { limiter, slow };

8
src/utils/logger.ts ノーマルファイル
ファイルの表示

@ -0,0 +1,8 @@
import pino from "pino";
export const logger = pino({
level: "info",
transport: {
target: "pino-pretty"
},
});

90
src/utils/modifier.ts ノーマルファイル
ファイルの表示

@ -0,0 +1,90 @@
import p from "phin";
import { CookieJar } from "tough-cookie";
import { HttpsCookieAgent } from "http-cookie-agent/http";
import { config } from "dotenv";
import * as pkg from "../../package.json";
config();
const jar = new CookieJar();
jar.setCookie(process.env.CF as string, "https://nhentai.net/");
async function nhentaiStatus(): Promise<boolean> {
const res = await p({
url: "https://nhentai.net/api/galleries/search?query=futanari",
core: {
agent: new HttpsCookieAgent({ cookies: { jar, }, }),
},
"headers": {
"User-Agent": `${pkg.name}/${pkg.version} Node.js/16.9.1`
},
});
if (res.statusCode === 200) {
return true;
} else {
return false;
}
}
function getPururinInfo(value: string) {
return value.replace(/\n/g, " ").replace(/\s\s+/g, " ").trim();
}
function getPururinPageCount(value: string) {
const data = value.replace(/\n/g, " ").replace(/\s\s+/g, " ").trim().split(", ").pop();
return Number(data?.split(" ")[0]);
}
function getUrl(url: string) {
return url.replace(/^\/\//, "https://");
}
function getId(url: string) {
return url.replace(/^https?:\/\/[^\\/]+/, "").replace(/\/$/, "");
}
function getDate(date: Date) {
return date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
}
function timeAgo(input: Date) {
const date = new Date(input);
const formatter: any = new Intl.RelativeTimeFormat("en");
const ranges: { [key: string]: number } = {
years: 3600 * 24 * 365,
months: 3600 * 24 * 30,
weeks: 3600 * 24 * 7,
days: 3600 * 24,
hours: 3600,
minutes: 60,
seconds: 1
};
const secondsElapsed = (date.getTime() - Date.now()) / 1000;
for (const key in ranges) {
if (ranges[key] < Math.abs(secondsElapsed)) {
const delta = secondsElapsed / ranges[key];
return formatter.format(Math.round(delta), key);
}
}
}
async function mock(url: string) {
const site = await p({ url: url });
if (site.statusCode === 200) {
return true;
} else if (site.statusCode === 308) {
return true;
} else {
return false;
}
}
export const isNumeric = (val: string) : boolean => {
return !isNaN(Number(val));
};
export { getPururinInfo, getPururinPageCount, getUrl, getId, getDate, timeAgo, mock, nhentaiStatus };

10
src/utils/options.ts ノーマルファイル
ファイルの表示

@ -0,0 +1,10 @@
export default {
PURURIN: "https://pururin.to",
HENTAIFOX: "https://hentaifox.com",
HENTAI2READ: "https://hentai2read.com",
SIMPLY_HENTAI: "https://simply-hentai.com",
SIMPLY_HENTAI_PROXIFIED: "https://simplyh.sinxdr.workers.dev",
NHENTAI: "https://nhentai.net",
NHENTAI_IP: "http://35.186.156.165",
NHENTAI_IP_2: "http://173.82.30.99:3002",
};

24
test/nhentaiCookietest.ts ノーマルファイル
ファイルの表示

@ -0,0 +1,24 @@
import p from "phin";
import { CookieJar } from "tough-cookie";
import { HttpsCookieAgent } from "http-cookie-agent/http";
import { config } from "dotenv";
import * as pkg from "../package.json";
config();
const jar = new CookieJar();
jar.setCookie(process.env.CF as string, "https://nhentai.net/");
async function test(): Promise<void> {
const res = await p({
url: "https://nhentai.net/api/galleries/search?query=futanari",
core: {
agent: new HttpsCookieAgent({ cookies: { jar, }, }),
},
"headers": {
"User-Agent": `${pkg.name}/${pkg.version} Node.js/16.9.1`
},
});
console.log(res.statusCode);
}
test();

15
test/test.ts ノーマルファイル
ファイルの表示

@ -0,0 +1,15 @@
import c from "../src/utils/options";
import p from "phin";
for (const url of
[c.HENTAIFOX, c.PURURIN, c.HENTAI2READ, c.SIMPLY_HENTAI,
c.SIMPLY_HENTAI_PROXIFIED, c.NHENTAI, c.NHENTAI_IP, c.NHENTAI_IP_2]) {
p({ url }).then(res => {
if (res.statusCode !== 200) {
console.log(`${url} is not available, status code: ${res.statusCode}`);
}
else {
console.log(`${url} is available, can be scraped`);
}
});
}

28
tsconfig.json ノーマルファイル
ファイルの表示

@ -0,0 +1,28 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"compilerOptions": {
"outDir": "./build",
"allowJs": true,
"target": "ESNext",
"baseUrl": "src",
"resolveJsonModule": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"module": "commonjs",
"paths": {},
"typeRoots": ["./node_modules/@types"],
"inlineSourceMap": true,
"charset": "UTF-8",
"downlevelIteration": true,
"newLine": "lf",
"strict": true,
"strictBindCallApply": true,
"strictPropertyInitialization": false,
"declaration": true
},
"include": [
"src/**/*"
],
"exclude": ["node_modules", "build", "out", "tmp", "logs", "test"]
}