gadl/platforms/dmm-books.ts
2023-11-19 21:25:56 +09:00

188 lines
4.9 KiB
TypeScript

import fs from "node:fs/promises";
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,
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 = [...page.url]
.map((c) => c.charCodeAt())
.reduce((a, cc) => a + cc, 0);
const pattern = (w % NFBR.a0X.a3h) + 1;
const blocks = NFBR.a3E.a3f(
Width,
Height,
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;
}
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 });
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 download(dir: string, books: Array<Book>) {
await fs.mkdir(dir);
const ctx = await loadBrowserContext();
const page = await ctx.newPage();
// TODO: 複数ブックのサポート
const book = books[0];
// TODO: downloadBook() にまとめる
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();
},
};
}