import type { Browser, BrowserContext, ImageFile, PageOrFrame, } from "../browser"; import type { Book } from "../library"; // リーダーのページ要素 const workTreeItemsSelector = `[class^=_worktree_] li[class^=_item_]`; function Reader(page: PageOrFrame, readerUrl: string) { const workId = /^https:[/][/]play[.]dlsite[.]com[/]#[/]work[/]([^/]+)/.exec( readerUrl, )?.[1]; if (!workId) { throw new Error(`workId is not included: ${readerUrl}`); } return { async load() { await page.goto(readerUrl); }, async downloadUrl(): Promise { const isBook = workId.startsWith("B"); if (isBook) { // PDFファイルでないことを確認 const items = await page.waitForSelector(workTreeItemsSelector); const text = await items.textContent(); if (!text?.match(/PDFファイル/u)) return null; } return `https://www.dlsite.com/home/download/=/product_id/${workId}.html`; }, }; } export function DlsiteManiax(browser: Browser) { async function* getAllBooks(ctx: BrowserContext): AsyncGenerator { const totalCountEndpoint = "https://play.dlsite.com/api/product_count"; const endpoint = "https://play.dlsite.com/api/purchases"; const pager = { page: 1, perPage: 50, totalCount: Infinity, }; const res = await ctx.request.get(totalCountEndpoint); const body: { user: number; } = await res.json(); pager.totalCount = body.user; while ((pager.page - 1) * 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: { limit: number; works: Array<{ workno: number; name: { ja_JP: string; }; maker: { name: { ja_JP: string; }; }; author_name: string | null; }>; } = await res.json(); for (const work of Object.values(body.works).flat()) { yield { id: NaN, platform: "dlsite-maniax", readerUrl: `https://play.dlsite.com/#/work/${work.workno}`, title: work.name.ja_JP || "", authors: [work.author_name || work.maker.name.ja_JP || ""], }; process.stderr.write("."); } pager.page += 1; pager.perPage = body.limit; } } return { async *pull(): AsyncGenerator { const ctx = await browser.loadBrowserContext("dlsite-maniax"); yield* getAllBooks(ctx); process.stderr.write(`\n`); }, async getFiles(book: Book): Promise Promise>> { const ctx = await browser.loadBrowserContext("dlsite-maniax"); const page = await ctx.newPage(); const reader = Reader(page, book.readerUrl); await reader.load(); const downloadUrl = await reader.downloadUrl(); if (downloadUrl) { const imageFile: ImageFile = { url: downloadUrl }; return [ async () => { const blob = await browser.drawImage(page, imageFile); process.stderr.write("."); return blob; }, ]; } // ページ数 … 画面に表示されている要素を辿る await page.waitForSelector(workTreeItemsSelector); const workTreeItems = await page.locator(workTreeItemsSelector).count(); await page.click(workTreeItemsSelector); // 見開き表示の無効化 … 初回: 右下見開きボタンをクリックして無効化 const spreadButton = page.getByRole("button", { name: "見開き" }); await spreadButton.click(); await Promise.all([ spreadButton.waitFor({ state: "detached" }), page.mouse.click(0, 720 / 2), ]); await page.keyboard.press("ArrowRight"); // ページ数だけ画面送りを繰り返し行い、canvasをそのままキャプチャしていく const files: Array<() => Promise> = []; while (files.length < workTreeItems) { await page.waitForTimeout(1000); const n = Math.min(2, Math.max(0, workTreeItems - 1 - files.length)); const canvas = page.locator("canvas").nth(n); await canvas.waitFor({ state: "visible" }); const [width, height] = await Promise.all( ["width", "height"].map((d) => canvas.getAttribute(d).then(Number)), ); await page.setViewportSize({ width, height }); await page.waitForTimeout(1000); const buff = await canvas.screenshot(); files.push(async () => new Blob([buff], { type: "image/png" })); process.stderr.write("."); await page.keyboard.press("ArrowLeft"); } return files; }, loginEndpoints: ["https://www.dlsite.com/home/login"], loginSuccessUrl: (url: URL) => url.origin === "https://www.dlsite.com", logoutEndpoints: ["https://www.dlsite.com/home/logout"], }; } DlsiteManiax.siteUrl = (url: URL) => url.href.startsWith("https://play.dlsite.com/#/work/");