gadl/platforms/dmm-books.ts
2023-11-20 23:33:21 +09:00

309 lines
7.9 KiB
TypeScript

import fs from "node:fs/promises";
import path from "node:path";
import type { Book } from "../library";
import { userAgent, type Browser, type BrowserContext } from "../browser";
import type { Database } from "../database";
var NFBR: any;
async function getFiles() {
const params = new URLSearchParams(location.search);
const model = new NFBR.a6G.Model({
settings: new NFBR.Settings("NFBR.SettingData"),
viewerFontSize: NFBR.a0X.a3K,
viewerFontFace: NFBR.a0X.a3k,
viewerSpreadDouble: true,
viewerSpread: {},
});
const a6l = new NFBR.a6G.a6L(model);
const a2f = new NFBR.a2F();
const a5w = await a2f.a5W({
contentId: params.get(NFBR.a5q.Key.CONTENT_ID),
a6m: params.get(NFBR.a5q.Key.a6M),
preview:
params.get(NFBR.a5q.Key.LOOK_INSIDE) !== NFBR.a5q.LookInsideType.DISABLED,
previewType:
params.get(NFBR.a5q.Key.LOOK_INSIDE) ?? NFBR.a5q.LookInsideType.DISABLED,
contentType: a6l.getContentType(),
title: true,
});
const content = new NFBR.a6i.Content(a5w.url);
const a5n = new NFBR.a5n();
await a5n.a5s(content, "configuration", a6l);
const files: Array<{
url: string;
blocks: [];
width: number;
height: number;
}> = [];
for (const index of Object.keys(content.files)) {
const file = content.files[index];
const conf = content.configuration.contents[index];
const {
No,
DummyWidth,
DummyHeight,
Size: { Width, Height },
} = file.FileLinkInfo.PageLinkInfoList[0].Page;
const page = new NFBR.a6i.Page(
`${conf.file}/${No}.jpeg`,
index,
`${conf["original-file-path"]}#-acs-position-${file.PageToBookmark[0][0]}-${file.PageToBookmark[0][1]}`,
);
const w = [...`${conf.file}/${No}`]
.map((c) => c.charCodeAt(0))
.reduce((a, cc) => a + cc, 0);
const pattern = (w % NFBR.a0X.a3h) + 1;
const blocks = NFBR.a3E.a3f(
Width + DummyWidth,
Height + DummyHeight,
NFBR.a0X.a3g,
NFBR.a0X.a3G,
pattern,
);
const url = `${a5w.url}${page.url}`;
files.push({
url,
blocks,
width: Width,
height: Height,
});
}
return files;
}
async function drawImage(file: {
url: string;
blocks: Array<Record<string, number>>;
width: number;
height: number;
}) {
const canvas = Object.assign(document.createElement("canvas"), {
width: file.width,
height: file.height,
});
const image = (await new Promise((resolve) => {
Object.assign(new Image(), {
crossOrigin: "use-credentials",
src: file.url,
onload() {
resolve(this);
},
});
})) as HTMLImageElement;
const ctx = canvas.getContext("2d")!;
for (const q of file.blocks) {
ctx.drawImage(
image,
q.destX,
q.destY,
q.width,
q.height,
q.srcX,
q.srcY,
q.width,
q.height,
);
}
const dataUrl = canvas.toDataURL();
return dataUrl;
}
export function DmmBooks({ db, browser }: { db: Database; browser: Browser }) {
async function loadBrowserContext(): Promise<BrowserContext> {
const { secrets } = await db.get(
`select secrets from platforms where name = 'dmm-books'`,
);
const storageState = JSON.parse(secrets) ?? undefined;
const ctx = await browser.newContext({ storageState, userAgent });
return ctx;
}
async function* getSeriesBooks(
ctx: BrowserContext,
series: {
seriesId: string;
shopName: string;
title: string;
authors: Array<string>;
},
): AsyncGenerator<Book> {
const endpoint = `https://book.dmm.com/ajax/bff/contents/?shop_name=${series.shopName}&series_id=${series.seriesId}`;
const pager = {
page: 1,
perPage: 0,
totalCount: Infinity,
};
while (pager.page * pager.perPage <= pager.totalCount) {
const res = await ctx.request.get(`${endpoint}&page=${pager.page}`);
if (!res.ok()) {
throw new Error(`${res.status()} ${res.statusText()}`);
}
const body: {
volume_books: Array<{
title: string;
purchased?: {
streaming_url: string;
};
}>;
pager: {
page: number;
per_page: number;
total_count: number;
};
} = await res.json();
for (const book of body.volume_books.filter((b) => b.purchased)) {
yield {
id: NaN,
platform: "dmm-books",
readerUrl: book.purchased?.streaming_url!,
title: book.title || series.title || "",
authors: series.authors,
};
process.stderr.write(".");
}
pager.page += 1;
pager.perPage = body.pager.per_page;
pager.totalCount = body.pager.total_count;
}
}
async function* getAllBooks(ctx: BrowserContext): AsyncGenerator<Book> {
const endpoint = "https://book.dmm.com/ajax/bff/library/?shop_name=all";
const pager = {
page: 1,
perPage: 0,
totalCount: Infinity,
};
while (pager.page * pager.perPage <= pager.totalCount) {
const res = await ctx.request.get(`${endpoint}&page=${pager.page}`);
if (!res.ok()) {
throw new Error(`${res.status()} ${res.statusText()}`);
}
const body: {
series_books: Array<{
shop_name: string;
series_id: string;
title: string;
author: Array<string>;
}>;
pager: {
page: number;
per_page: number;
total_count: number;
};
} = await res.json();
for (const series of body.series_books) {
yield* getSeriesBooks(ctx, {
seriesId: series.series_id,
shopName: series.shop_name,
title: series.title,
authors: series.author,
});
}
pager.page += 1;
pager.perPage = body.pager.per_page;
pager.totalCount = body.pager.total_count;
}
}
return {
async login() {
const ctx = await browser.newContext();
const page = await ctx.newPage();
await page.goto("https://accounts.dmm.com/service/login/password");
await page.waitForURL("https://www.dmm.com/", { timeout: 0 });
await page.goto("https://www.dmm.com/service/-/exchange");
await Promise.race([
page.waitForURL("https://www.dmm.com/", { timeout: 0 }),
page.waitForURL("https://www.dmm.co.jp/top/", { timeout: 0 }),
]);
const secrets = await ctx.storageState();
await browser.close();
await db.run(
`update platforms set secrets = ? where name = 'dmm-books'`,
JSON.stringify(secrets),
);
},
async logout() {
await browser.close();
await db.run(
`update platforms set secrets = 'null' where name = 'dmm-books'`,
);
},
async *pull(): AsyncGenerator<Book> {
const ctx = await loadBrowserContext();
yield* getAllBooks(ctx);
process.stderr.write(`\n`);
await browser.close();
},
async download(dir: string, book: Book) {
const ctx = await loadBrowserContext();
const page = await ctx.newPage();
// TODO: --all
await fs.mkdir(path.dirname(dir), { recursive: true });
await fs.mkdir(dir);
await page.goto(book.readerUrl);
const files = await page.evaluate(getFiles);
const digits = String(files.length).length;
function pad(n: string) {
return n.padStart(digits, "0");
}
for (const [n, file] of Object.entries(files)) {
const dataUrl = await page.evaluate(drawImage, file);
const [prefix, base64] = dataUrl.split(",", 2);
if (!prefix.startsWith("data:image/png;")) {
throw new Error("Only image/png is supported.");
}
if (!prefix.endsWith(";base64")) {
throw new Error("Only base64 is supported.");
}
const buffer = Buffer.from(base64, "base64");
await fs.writeFile(`${dir}/${pad(n)}.png`, buffer);
process.stderr.write(".");
}
process.stderr.write(`\n`);
await browser.close();
},
};
}