From fb47c95173f547aad731c22ef105c7e4d8d05319 Mon Sep 17 00:00:00 2001 From: Kohei Watanabe Date: Sun, 19 Nov 2023 20:47:49 +0900 Subject: [PATCH] download --- browser.ts | 6 +- main.ts | 15 +++- platform.ts | 14 +--- platforms/dmm-books.ts | 170 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 187 insertions(+), 18 deletions(-) diff --git a/browser.ts b/browser.ts index 81a523b..d442f72 100644 --- a/browser.ts +++ b/browser.ts @@ -1,3 +1,5 @@ -import { type Browser, chromium } from "playwright"; +import type { Browser, BrowserContext } from "playwright"; +import { chromium, devices } from "playwright"; -export { Browser, chromium }; +export { Browser, BrowserContext, chromium }; +export const { userAgent } = devices["Desktop Chrome"]; diff --git a/main.ts b/main.ts index a5ef560..5477b24 100644 --- a/main.ts +++ b/main.ts @@ -21,7 +21,7 @@ const options = { async run() { const db = await createDatabase(args.values.db!); const browser = await chromium.launch({ headless: false }); - const platform = createPlatform(db, browser); + const platform = createPlatform({ db, browser }); await platform.login(); }, }, @@ -30,7 +30,7 @@ const options = { async run() { const db = await createDatabase(args.values.db!); const browser = await chromium.launch(); - const platform = createPlatform(db, browser); + const platform = createPlatform({ db, browser }); await platform.logout(); }, }, @@ -52,6 +52,17 @@ const options = { console.dir(books, { depth: null }); }, }, + download: { + type: "string", + async run() { + const db = await createDatabase(args.values.db!); + const library = createLibrary(db); + const books = await library.getBooks(); + const browser = await chromium.launch(); + const platform = createPlatform({ db, browser }); + await platform.download(args.values.download!, books); + }, + }, help: { type: "boolean", short: "h", diff --git a/platform.ts b/platform.ts index d36d34f..b5501a4 100644 --- a/platform.ts +++ b/platform.ts @@ -2,15 +2,7 @@ import type { Database } from "./database"; import type { Browser } from "./browser"; import { DmmBooks } from "./platforms/dmm-books"; -export function createPlatform(db: Database, browser: Browser) { - const platform = DmmBooks(db, browser); - - return { - async login() { - await platform.login(); - }, - async logout() { - await platform.logout(); - }, - }; +export function createPlatform(opt: { db: Database; browser: Browser }) { + const platform = DmmBooks(opt); + return platform; } diff --git a/platforms/dmm-books.ts b/platforms/dmm-books.ts index b605fda..6a32531 100644 --- a/platforms/dmm-books.ts +++ b/platforms/dmm-books.ts @@ -1,7 +1,129 @@ +import fs from "node:fs/promises"; +import type { Book } from "../library"; +import { userAgent, type Browser, type BrowserContext } from "../browser"; import type { Database } from "../database"; -import type { Browser } from "../browser"; -export function DmmBooks(db: Database, browser: Browser) { +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>; + 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; + } + return { async login() { const ctx = await browser.newContext(); @@ -9,16 +131,58 @@ export function DmmBooks(db: Database, browser: Browser) { 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 ctx.close(); + 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) { + 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(); + }, }; }