diff --git a/client.ts b/client.ts index 8f14bd3..f116114 100644 --- a/client.ts +++ b/client.ts @@ -1,6 +1,7 @@ import { 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"; export type GID = [number, string]; @@ -27,6 +28,11 @@ export class Client { ) { 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"); + } request( url: string | Request | URL, method: string | undefined = undefined, @@ -37,23 +43,16 @@ export class Client { if (this.cookies) { if (typeof url === "string") { const u = new URL(url); - if ( - u.hostname == "e-hentai.org" || u.hostname == "exhentai.org" - ) { + if (this.is_eh(u.hostname)) { headers.set("Cookie", this.cookies); } } else if (url instanceof Request) { const u = new URL(url.url); - if ( - u.hostname == "e-hentai.org" || u.hostname == "exhentai.org" - ) { + if (this.is_eh(u.hostname)) { headers.set("Cookie", this.cookies); } } else if (url instanceof URL) { - if ( - url.hostname == "e-hentai.org" || - url.hostname == "exhentai.org" - ) { + if (this.is_eh(url.hostname)) { headers.set("Cookie", this.cookies); } } @@ -64,10 +63,17 @@ export class Client { } return fetch(url); } else { - return fetch( - url, - Object.assign({ headers, method: method || "GET" }, options), - ); + 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; + } + return fetch(url, d); } } /** @@ -122,4 +128,20 @@ export class Client { } 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); + } } diff --git a/db.ts b/db.ts index 1330403..7f58175 100644 --- a/db.ts +++ b/db.ts @@ -1,4 +1,4 @@ -const { DB } = await import("sqlite/mod.ts"); +import { DB } from "sqlite/mod.ts"; import { SemVer } from "std/semver/mod.ts"; import { join } from "std/path/mod.ts"; import { SqliteError } from "sqlite/mod.ts"; diff --git a/page/MPVPage.ts b/page/MPVPage.ts new file mode 100644 index 0000000..f9fb211 --- /dev/null +++ b/page/MPVPage.ts @@ -0,0 +1,186 @@ +import { DOMParser } from "deno_dom/deno-dom-wasm-noinit.ts"; +import { Client } from "../client.ts"; +import { initDOMParser } from "../utils.ts"; + +export type MPVRawImage = { + /**Image name*/ + n: string; + /**Page token*/ + k: string; + /**Thumbnail*/ + t: string; +}; + +export type MPVDispatchData = { + /**width x height :: file size*/ + d: string; + /**Image*/ + i: string; + /**URL to load full image. full url is `${base_url}${lf}` */ + lf: string; + /**Forum token url. full url is `${base_url}r/${ll}`*/ + ll: string; + /**File search url. full url is `${base_url}${lo}` */ + lo: string; + /**`ori` or `Download original ${width} x ${height} ${file_size} source`*/ + o: string; + /**Reload token*/ + s: string; + /**Width*/ + xres: string; + /**Height*/ + yres: string; +}; + +class MPVImage { + base; + /**Page number*/ + index; + #mpv; + data: MPVDispatchData | undefined; + constructor(base: MPVRawImage, index: number, mpv: MPVPage) { + this.base = base; + this.index = index; + this.#mpv = mpv; + } + get name() { + return this.base.n; + } + get page_number() { + return this.index; + } + get page_token() { + return this.base.k; + } + get thumbnail() { + return this.base.t; + } + get xres() { + const xres = this.data?.xres; + if (!xres) return undefined; + return parseInt(xres); + } + get yres() { + const yres = this.data?.yres; + if (!yres) return undefined; + return parseInt(yres); + } + async load() { + if (this.data === undefined) { + this.data = await this.#mpv.image_dispatch( + this.index, + this.page_token, + ); + } + } +} + +class MPVPage { + dom; + doc; + client; + #meta_script: string | undefined; + #api_url: string | undefined; + #gid: number | undefined; + #mpvkey: string | undefined; + #pagecount: number | undefined; + #imagelist: MPVImage[] | undefined; + #base_url: string | undefined; + constructor(html: string, client: Client) { + const dom = (new DOMParser()).parseFromString(html, "text/html"); + if (!dom) { + throw Error("Failed to parse HTML document."); + } + this.dom = dom; + const doc = this.dom.documentElement; + if (!doc) { + throw Error("HTML document don't have a document element."); + } + this.doc = doc; + this.client = client; + } + get api_url() { + if (this.#api_url === undefined) { + const api_url: string = eval(`${this.meta_script};api_url`); + this.#api_url = api_url; + return api_url; + } else return this.#api_url; + } + get base_url() { + if (this.#base_url === undefined) { + const base_url: string = eval(`${this.meta_script};base_url`); + this.#base_url = base_url; + return base_url; + } else return this.#base_url; + } + async image_dispatch( + page: number, + imgkey: string, + nl: string | undefined = undefined, + ): Promise { + const param: Record = { + method: "imagedispatch", + gid: this.gid, + page, + imgkey, + mpvkey: this.mpvkey, + }; + if (nl) param.nl = nl; + const re = await this.client.post(this.api_url, { + body: JSON.stringify(param), + headers: { "content-type": "application/json" }, + }); + if (re.status != 200) { + throw new Error( + `Fetch ${this.api_url} failed, status ${re.status} ${re.statusText}`, + ); + } + return re.json(); + } + get gid() { + if (this.#gid === undefined) { + const gid: number = eval(`${this.meta_script};gid`); + this.#gid = gid; + return gid; + } else return this.#gid; + } + get imagelist() { + if (this.#imagelist === undefined) { + const t: MPVRawImage[] = eval(`${this.meta_script};imagelist`); + const imagelist = t.map((v, i) => new MPVImage(v, i + 1, this)); + return imagelist; + } else return this.#imagelist; + } + get meta_script() { + if (this.#meta_script === undefined) { + const c = this.doc.getElementsByTagName("script"); + for (const e of c) { + const t = e.innerHTML.trim(); + if (t.startsWith("var ")) { + this.#meta_script = t; + return t; + } + } + throw Error("Failed to locate meta script."); + } else return this.#meta_script; + } + get mpvkey() { + if (this.#mpvkey === undefined) { + const mpvkey: string = eval(`${this.meta_script};mpvkey`); + this.#mpvkey = mpvkey; + return mpvkey; + } else return this.#mpvkey; + } + get pagecount() { + if (this.#pagecount === undefined) { + const pagecount: number = eval(`${this.meta_script};pagecount`); + this.#pagecount = pagecount; + return pagecount; + } else return this.#pagecount; + } +} + +export async function load_mpv_page(html: string, client: Client) { + await initDOMParser(); + return new MPVPage(html, client); +} diff --git a/page/MPVPage_test.ts b/page/MPVPage_test.ts new file mode 100644 index 0000000..808eb70 --- /dev/null +++ b/page/MPVPage_test.ts @@ -0,0 +1,26 @@ +import { assertEquals } from "https://deno.land/std@0.188.0/testing/asserts.ts"; +import { Client } from "../client.ts"; +import { load_settings } from "../config.ts"; +import { API_PERMISSION } from "../test_base.ts"; +import { assert } from "https://deno.land/std@0.188.0/_util/asserts.ts"; + +Deno.test({ + name: "GalleryPage_test", + permissions: API_PERMISSION, +}, async () => { + const cfg = await load_settings("./config.json"); + const client = new Client(cfg); + const re = await client.fetchMPVPage(1473589, "b1f3c60a95"); + assertEquals(re.gid, 1473589); + assertEquals(re.pagecount, 8); + assertEquals(re.imagelist.length, re.pagecount); + const p1 = re.imagelist[0]; + assertEquals(p1.index, 1); + assertEquals(p1.name, "1.png"); + assertEquals(p1.page_token, "7ffd92a751"); + await p1.load(); + assert(p1.data); + assertEquals(p1.xres, 2449); + assertEquals(p1.yres, 3427); + console.log(p1.data.i); +}); diff --git a/test_base.ts b/test_base.ts index e92207c..186c98f 100644 --- a/test_base.ts +++ b/test_base.ts @@ -2,7 +2,12 @@ import { exists } from "std/fs/exists.ts"; export const API_PERMISSION: Deno.PermissionOptions = { read: ["./config.json"], - net: ["e-hentai.org", "exhentai.org"], + net: [ + "e-hentai.org", + "exhentai.org", + "api.e-hentai.org", + "api.exhentai.org", + ], }; export async function remove_if_exists(f: string) {