This commit is contained in:
Nebel 2023-11-19 20:47:49 +09:00
parent 7208920cd1
commit fb47c95173
Signed by: nebel
GPG key ID: 79807D08C6EF6460
4 changed files with 187 additions and 18 deletions

View file

@ -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"];

15
main.ts
View file

@ -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",

View file

@ -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;
}

View file

@ -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<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();
@ -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<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();
},
};
}