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>; 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 { 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; }, ): AsyncGenerator { 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 { 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; }>; 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 }); 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 { 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(); }, }; }