Files
eh-downloader/client.ts
2023-07-22 17:17:01 +08:00

185 lines
6.1 KiB
TypeScript

import type { Config } from "./config.ts";
import { load_gallery_metadata } from "./page/GalleryMetadata.ts";
import { load_gallery_page } from "./page/GalleryPage.ts";
import { load_mpv_page } from "./page/MPVPage.ts";
import { load_single_page } from "./page/SinglePage.ts";
import { RecoverableError } from "./task_manager.ts";
export type GID = [number, string];
export class Client {
cookies;
host;
ua;
signal;
constructor(cfg: Config, signal?: AbortSignal) {
this.cookies = cfg.cookies;
this.ua = cfg.ua ||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36";
this.host = cfg.ex ? "exhentai.org" : "e-hentai.org";
this.signal = signal;
}
get(
url: string | Request | URL,
options: RequestInit | undefined = undefined,
) {
return this.request(url, "GET", options);
}
post(
url: string | Request | URL,
options: RequestInit | undefined = undefined,
) {
return this.request(url, "POST", options);
}
is_eh(hostname: string) {
return hostname == "e-hentai.org" || hostname == "exhentai.org" ||
hostname.endsWith(".e-hentai.org") ||
hostname.endsWith(".exhentai.org");
}
async redirect(url: string) {
const re = await this.get(url, { redirect: "manual" });
if (re.status == 301 || re.status == 302) {
const t = re.headers.get("location");
re.body?.cancel();
return t ? t : undefined;
} else return undefined;
}
request(
url: string | Request | URL,
method: string | undefined = undefined,
options: RequestInit | undefined = undefined,
) {
const headers = new Headers();
headers.set("User-Agent", this.ua);
if (this.cookies) {
if (typeof url === "string") {
const u = new URL(url);
if (this.is_eh(u.hostname)) {
headers.set("Cookie", this.cookies);
}
} else if (url instanceof Request) {
const u = new URL(url.url);
if (this.is_eh(u.hostname)) {
headers.set("Cookie", this.cookies);
}
} else if (url instanceof URL) {
if (this.is_eh(url.hostname)) {
headers.set("Cookie", this.cookies);
}
}
}
if (url instanceof Request) {
for (const v of headers) {
url.headers.set(v[0], v[1]);
}
try {
return fetch(url);
} catch (e) {
if (e instanceof TypeError) {
throw new RecoverableError(e.message, { cause: e.cause });
}
throw e;
}
} else {
const d = Object.assign({ method: method || "GET" }, options);
if (d.headers) {
const nheaders = new Headers(d.headers);
for (const v of headers) {
nheaders.set(v[0], v[1]);
}
d.headers = nheaders;
} else {
d.headers = headers;
}
if (!d.signal && this.signal) {
d.signal = this.signal;
}
try {
return fetch(url, d);
} catch (e) {
if (e instanceof TypeError) {
throw new RecoverableError(e.message, { cause: e.cause });
}
throw e;
}
}
}
/**
* Fetch a single page (use HTML)
* @param gid Gallery ID
* @param page_token Page token
* @param index Page index
* @param nl Reload token
* @returns
*/
async fetchSignlePage(
gid: number,
page_token: string,
index: number,
nl?: string,
) {
const p = nl ? `?nl=${nl}` : "";
const url = `https://${this.host}/s/${page_token}/${gid}-${index}${p}`;
const re = await this.get(url);
if (re.status != 200) {
throw new Error(
`Fetch ${url} failed, status ${re.status} ${re.statusText}`,
);
}
return load_single_page(await re.text(), this);
}
/**
* Fetch metadata via API
* @param gids A list of Gallery ID and token
* @returns
*/
async fetchGalleryMetadataByAPI(...gids: GID[]) {
if (gids.length > 25) throw Error("Load limiting is reached.");
const data = { method: "gdata", gidlist: gids, namespace: 1 };
const re = await this.post(`https://${this.host}/api.php`, {
body: JSON.stringify(data),
headers: { "content-type": "application/json" },
});
if (re.status != 200) {
throw new Error(
`Fetch api.php failed, status ${re.status} ${re.statusText}`,
);
}
return load_gallery_metadata(await re.text());
}
/**
* Fetch a gallery page (use HTML)
* @param gid Gallery ID
* @param token Token
* @param page Page number
* @returns
*/
async fetchGalleryPage(gid: number, token: string, page?: number) {
let url = `https://${this.host}/g/${gid}/${token}/`;
if (page) url += `?p=${page}`;
const re = await this.get(url);
if (re.status != 200) {
throw new Error(
`Fetch ${url} failed, status ${re.status} ${re.statusText}`,
);
}
return load_gallery_page(await re.text(), this);
}
/**
* Fetch a Multi-Page Viewer page
* @param gid Gallery ID
* @param token Token
* @returns
*/
async fetchMPVPage(gid: number, token: string) {
const url = `https://${this.host}/mpv/${gid}/${token}/`;
const re = await this.get(url);
if (re.status != 200) {
throw new Error(
`Fetch ${url} failed, status ${re.status} ${re.statusText}`,
);
}
return load_mpv_page(await re.text(), this);
}
}