fanza-doujin
This commit is contained in:
parent
89e892f870
commit
5135d5121f
10 changed files with 235 additions and 90 deletions
|
@ -10,6 +10,7 @@ $ npx https://git.fogtype.com/nebel/gadl/archive/main.tar.gz --help
|
||||||
## Supported Sites
|
## Supported Sites
|
||||||
|
|
||||||
- DMM ブックス (漫画)
|
- DMM ブックス (漫画)
|
||||||
|
- FANZA 同人
|
||||||
- Google Play ブックス (漫画)
|
- Google Play ブックス (漫画)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
94
browser.ts
94
browser.ts
|
@ -3,11 +3,78 @@ import { chromium, devices } from "playwright";
|
||||||
import type { Database } from "./database";
|
import type { Database } from "./database";
|
||||||
import type { TPlatform } from "./platform";
|
import type { TPlatform } from "./platform";
|
||||||
|
|
||||||
|
export type ImageFile = {
|
||||||
|
url: string;
|
||||||
|
blocks?: Array<Record<string, number>>;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function drawImage(imageFile: ImageFile): Promise<string> {
|
||||||
|
const canvas = Object.assign(document.createElement("canvas"), {
|
||||||
|
width: imageFile.width,
|
||||||
|
height: imageFile.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
const image = (await new Promise((resolve) => {
|
||||||
|
Object.assign(new Image(), {
|
||||||
|
crossOrigin: "use-credentials",
|
||||||
|
src: imageFile.url,
|
||||||
|
onload() {
|
||||||
|
resolve(this);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})) as HTMLImageElement;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext("2d")!;
|
||||||
|
|
||||||
|
for (const q of imageFile.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchImage(imageFile: ImageFile): Promise<string> {
|
||||||
|
const res = await fetch(imageFile.url);
|
||||||
|
const blob = await res.blob();
|
||||||
|
const dataUrl: string = await new Promise((resolve, reject) => {
|
||||||
|
const fileReader = Object.assign(new FileReader(), {
|
||||||
|
onload(): void {
|
||||||
|
resolve(this.result);
|
||||||
|
},
|
||||||
|
onerror(e: ErrorEvent): void {
|
||||||
|
const error = new Error(`${e.type}: ${e.message}`);
|
||||||
|
reject(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
fileReader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
|
||||||
|
return dataUrl;
|
||||||
|
}
|
||||||
|
|
||||||
export type Browser = {
|
export type Browser = {
|
||||||
loadBrowserContext(platform: TPlatform): Promise<Playwright.BrowserContext>;
|
loadBrowserContext(platform: TPlatform): Promise<Playwright.BrowserContext>;
|
||||||
saveBrowserContext(platform: TPlatform, ctx: BrowserContext): Promise<void>;
|
saveBrowserContext(platform: TPlatform, ctx: BrowserContext): Promise<void>;
|
||||||
newContext: () => Promise<Playwright.BrowserContext>;
|
newContext(): Promise<Playwright.BrowserContext>;
|
||||||
close: () => Promise<void>;
|
close(): Promise<void>;
|
||||||
|
drawImage(
|
||||||
|
pageOrFrame: Playwright.Page | Playwright.Frame,
|
||||||
|
imageFile: ImageFile,
|
||||||
|
): Promise<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BrowserContext = Playwright.BrowserContext;
|
export type BrowserContext = Playwright.BrowserContext;
|
||||||
|
@ -38,6 +105,7 @@ export async function createBrowser({
|
||||||
const ctx = await browser.newContext({ storageState, userAgent });
|
const ctx = await browser.newContext({ storageState, userAgent });
|
||||||
return ctx;
|
return ctx;
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveBrowserContext(
|
async saveBrowserContext(
|
||||||
platform: TPlatform,
|
platform: TPlatform,
|
||||||
ctx: BrowserContext,
|
ctx: BrowserContext,
|
||||||
|
@ -49,7 +117,29 @@ export async function createBrowser({
|
||||||
platform,
|
platform,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
newContext: () => browser.newContext(),
|
newContext: () => browser.newContext(),
|
||||||
close: () => browser.close(),
|
close: () => browser.close(),
|
||||||
|
|
||||||
|
async drawImage(
|
||||||
|
pageOrFrame: Playwright.Page | Playwright.Frame,
|
||||||
|
imageFile: ImageFile,
|
||||||
|
): Promise<string> {
|
||||||
|
if (Array.isArray(imageFile.blocks) && imageFile.blocks.length > 0) {
|
||||||
|
return await pageOrFrame.evaluate(drawImage, imageFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageFile.url.startsWith("blob:")) {
|
||||||
|
return await pageOrFrame.evaluate(fetchImage, imageFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = "page" in pageOrFrame ? pageOrFrame.page() : pageOrFrame;
|
||||||
|
const res = await page.context().request.get(imageFile.url);
|
||||||
|
const buffer = await res.body();
|
||||||
|
const type = res.headers()["content-type"];
|
||||||
|
const base64 = buffer.toString("base64");
|
||||||
|
|
||||||
|
return `data:${type};base64,${base64}`;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
30
library.ts
30
library.ts
|
@ -20,7 +20,14 @@ export function createLibrary(db: Database) {
|
||||||
const platform = site(readerUrlOrBook);
|
const platform = site(readerUrlOrBook);
|
||||||
|
|
||||||
await db.run(
|
await db.run(
|
||||||
`insert into books(platform_id, reader_url) values((select id from platforms where name = ?), ?)`,
|
`\
|
||||||
|
insert into books(
|
||||||
|
platform_id,
|
||||||
|
reader_url)
|
||||||
|
values((select id from platforms where name = ?), ?)
|
||||||
|
on conflict(reader_url)
|
||||||
|
do nothing
|
||||||
|
`,
|
||||||
platform,
|
platform,
|
||||||
readerUrlOrBook,
|
readerUrlOrBook,
|
||||||
);
|
);
|
||||||
|
@ -48,10 +55,11 @@ on conflict(reader_url)
|
||||||
async delete(id: number) {
|
async delete(id: number) {
|
||||||
await db.run(`delete from books where id = ?`, id);
|
await db.run(`delete from books where id = ?`, id);
|
||||||
},
|
},
|
||||||
async get(id: number): Promise<Book | undefined> {
|
async get(readerUrlOrBookId: string | number): Promise<Book | undefined> {
|
||||||
const row = await db.get(
|
const row = await db.get(
|
||||||
`select books.id, platforms.name as platform, books.reader_url as readerUrl, books.title, books.authors from books left join platforms on books.platform_id = platforms.id where books.id = ?`,
|
`select books.id, platforms.name as platform, books.reader_url as readerUrl, books.title, books.authors from books left join platforms on books.platform_id = platforms.id where books.reader_url = ? or books.id = ?`,
|
||||||
id,
|
readerUrlOrBookId,
|
||||||
|
Number(readerUrlOrBookId),
|
||||||
);
|
);
|
||||||
|
|
||||||
const book: Book | undefined = row && {
|
const book: Book | undefined = row && {
|
||||||
|
@ -87,9 +95,17 @@ on conflict(reader_url)
|
||||||
throw new Error(`Not found: ${path}`);
|
throw new Error(`Not found: ${path}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = `${book.authors
|
if (!book.title) {
|
||||||
.slice(0, opts.outAuthorsLimit)
|
book.title = String(book.id);
|
||||||
.join("、")}「${book.title}」`.replace(/[/]/g, "%2F");
|
}
|
||||||
|
|
||||||
|
if (book.authors.length > 0) {
|
||||||
|
book.title = `「${book.title}」`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = `${book.authors.slice(0, opts.outAuthorsLimit).join("、")}${
|
||||||
|
book.title
|
||||||
|
}`.replace(/[/]/g, "%2F");
|
||||||
|
|
||||||
const out = createWriteStream(`${opts.outDir}/${title}.cbz`);
|
const out = createWriteStream(`${opts.outDir}/${title}.cbz`);
|
||||||
|
|
||||||
|
|
2
main.ts
2
main.ts
|
@ -128,7 +128,7 @@ const options = {
|
||||||
await library.add(args.values.download!);
|
await library.add(args.values.download!);
|
||||||
}
|
}
|
||||||
|
|
||||||
const book = await library.get(Number(args.values.download!));
|
const book = await library.get(args.values.download!);
|
||||||
|
|
||||||
if (!book) {
|
if (!book) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
2
migrations/3_add_fanza_doujin.sql
Normal file
2
migrations/3_add_fanza_doujin.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
insert into platforms(name) values
|
||||||
|
('fanza-doujin');
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@fogtype/gadl",
|
"name": "@fogtype/gadl",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": "bin/run.js",
|
"bin": "bin/run.js",
|
||||||
|
|
14
platform.ts
14
platform.ts
|
@ -4,20 +4,20 @@ import type { Book } from "./library";
|
||||||
import type { Browser } from "./browser";
|
import type { Browser } from "./browser";
|
||||||
import type { Database } from "./database";
|
import type { Database } from "./database";
|
||||||
import { DmmBooks } from "./platforms/dmm-books";
|
import { DmmBooks } from "./platforms/dmm-books";
|
||||||
|
import { FanzaDoujin } from "./platforms/fanza-doujin";
|
||||||
import { GooglePlayBooks } from "./platforms/google-play-books";
|
import { GooglePlayBooks } from "./platforms/google-play-books";
|
||||||
|
|
||||||
const platforms = {
|
const platforms = {
|
||||||
"dmm-books": DmmBooks,
|
"dmm-books": DmmBooks,
|
||||||
|
"fanza-doujin": FanzaDoujin,
|
||||||
"google-play-books": GooglePlayBooks,
|
"google-play-books": GooglePlayBooks,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TPlatform = keyof typeof platforms;
|
export type TPlatform = keyof typeof platforms;
|
||||||
|
|
||||||
export function site(url: string): TPlatform {
|
export function site(url: string): TPlatform {
|
||||||
const { origin } = new URL(url);
|
for (const [platform, { siteUrl }] of Object.entries(platforms)) {
|
||||||
|
if (siteUrl(new URL(url))) return platform as TPlatform;
|
||||||
for (const [platform, { site }] of Object.entries(platforms)) {
|
|
||||||
if (site.includes(origin)) return platform as TPlatform;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Unsupported URL: ${url}`);
|
throw new Error(`Unsupported URL: ${url}`);
|
||||||
|
@ -29,7 +29,11 @@ export function createPlatform(opts: {
|
||||||
browser: Browser;
|
browser: Browser;
|
||||||
}) {
|
}) {
|
||||||
if (!(opts.platform in platforms)) {
|
if (!(opts.platform in platforms)) {
|
||||||
throw new Error(`Available platform: ${Object.keys(platforms).join(", ")}`);
|
throw new Error(
|
||||||
|
`The value must be a platform type: ${[...Object.keys(platforms)].join(
|
||||||
|
", ",
|
||||||
|
)}.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const platform = platforms[opts.platform](opts.browser);
|
const platform = platforms[opts.platform](opts.browser);
|
||||||
|
|
|
@ -1,16 +1,9 @@
|
||||||
import type { Book } from "../library";
|
import type { Book } from "../library";
|
||||||
import type { Browser, BrowserContext } from "../browser";
|
import type { Browser, BrowserContext, ImageFile } from "../browser";
|
||||||
|
|
||||||
type ImageFile = {
|
|
||||||
url: string;
|
|
||||||
blocks: Array<Record<string, number>>;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
var NFBR: any;
|
var NFBR: any;
|
||||||
|
|
||||||
async function getImageFiles(): Promise<Array<ImageFile>> {
|
export async function getImageFiles(): Promise<Array<ImageFile>> {
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
const model = new NFBR.a6G.Model({
|
const model = new NFBR.a6G.Model({
|
||||||
settings: new NFBR.Settings("NFBR.SettingData"),
|
settings: new NFBR.Settings("NFBR.SettingData"),
|
||||||
|
@ -79,42 +72,6 @@ async function getImageFiles(): Promise<Array<ImageFile>> {
|
||||||
return imageFiles;
|
return imageFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function drawImage(imageFile: ImageFile) {
|
|
||||||
const canvas = Object.assign(document.createElement("canvas"), {
|
|
||||||
width: imageFile.width,
|
|
||||||
height: imageFile.height,
|
|
||||||
});
|
|
||||||
|
|
||||||
const image = (await new Promise((resolve) => {
|
|
||||||
Object.assign(new Image(), {
|
|
||||||
crossOrigin: "use-credentials",
|
|
||||||
src: imageFile.url,
|
|
||||||
onload() {
|
|
||||||
resolve(this);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})) as HTMLImageElement;
|
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d")!;
|
|
||||||
|
|
||||||
for (const q of imageFile.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(browser: Browser) {
|
export function DmmBooks(browser: Browser) {
|
||||||
async function* getSeriesBooks(
|
async function* getSeriesBooks(
|
||||||
ctx: BrowserContext,
|
ctx: BrowserContext,
|
||||||
|
@ -246,7 +203,7 @@ export function DmmBooks(browser: Browser) {
|
||||||
const imageFiles = await page.evaluate(getImageFiles);
|
const imageFiles = await page.evaluate(getImageFiles);
|
||||||
|
|
||||||
return imageFiles.map((imageFile) => async () => {
|
return imageFiles.map((imageFile) => async () => {
|
||||||
const dataUrl = await page.evaluate(drawImage, imageFile);
|
const dataUrl = await browser.drawImage(page, imageFile);
|
||||||
|
|
||||||
process.stderr.write(".");
|
process.stderr.write(".");
|
||||||
|
|
||||||
|
@ -256,4 +213,5 @@ export function DmmBooks(browser: Browser) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
DmmBooks.site = ["https://book.dmm.com", "https://book.dmm.co.jp"];
|
DmmBooks.siteUrl = (url: URL) =>
|
||||||
|
["https://book.dmm.com", "https://book.dmm.co.jp"].includes(url.origin);
|
||||||
|
|
97
platforms/fanza-doujin.ts
Normal file
97
platforms/fanza-doujin.ts
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import type { Book } from "../library";
|
||||||
|
import type { Browser, BrowserContext, ImageFile } from "../browser";
|
||||||
|
import { getImageFiles } from "./dmm-books";
|
||||||
|
|
||||||
|
export function FanzaDoujin(browser: Browser) {
|
||||||
|
async function* getAllBooks(ctx: BrowserContext): AsyncGenerator<Book> {
|
||||||
|
const endpoint =
|
||||||
|
"https://www.dmm.co.jp/dc/doujin/api/mylibraries/?limit=20";
|
||||||
|
const pager = {
|
||||||
|
page: 1,
|
||||||
|
perPage: 20,
|
||||||
|
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: {
|
||||||
|
data: {
|
||||||
|
items: Record<
|
||||||
|
string,
|
||||||
|
Array<{
|
||||||
|
productId: string;
|
||||||
|
title: string;
|
||||||
|
makerName: string;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
} = await res.json();
|
||||||
|
|
||||||
|
for (const item of Object.values(body.data.items).flat()) {
|
||||||
|
yield {
|
||||||
|
id: NaN,
|
||||||
|
platform: "fanza-doujin",
|
||||||
|
readerUrl: `https://www.dmm.co.jp/dc/-/mylibrary/detail/=/product_id=${item.productId}/`,
|
||||||
|
title: item.title || "",
|
||||||
|
authors: [item.makerName],
|
||||||
|
};
|
||||||
|
|
||||||
|
process.stderr.write(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
pager.page += 1;
|
||||||
|
pager.totalCount = body.data.total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
async login() {
|
||||||
|
const ctx = await browser.newContext();
|
||||||
|
const page = await ctx.newPage();
|
||||||
|
await page.goto("https://accounts.dmm.co.jp/service/login/password");
|
||||||
|
await page.waitForURL("https://www.dmm.co.jp/top/", { timeout: 0 });
|
||||||
|
await browser.saveBrowserContext("fanza-doujin", ctx);
|
||||||
|
},
|
||||||
|
|
||||||
|
async *pull(): AsyncGenerator<Book> {
|
||||||
|
const ctx = await browser.loadBrowserContext("fanza-doujin");
|
||||||
|
|
||||||
|
yield* getAllBooks(ctx);
|
||||||
|
|
||||||
|
process.stderr.write(`\n`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getFiles(book: Book): Promise<Array<() => Promise<string>>> {
|
||||||
|
const ctx = await browser.loadBrowserContext("fanza-doujin");
|
||||||
|
const page = await ctx.newPage();
|
||||||
|
|
||||||
|
await page.goto(book.readerUrl);
|
||||||
|
await page.waitForSelector(`li[class^="fileTreeItem"]`);
|
||||||
|
await page.click(`li[class^="fileTreeItem"]>a`);
|
||||||
|
await page.waitForURL((url) =>
|
||||||
|
url.href.startsWith("https://www.dmm.co.jp/dc/-/viewer/=/product_id="),
|
||||||
|
);
|
||||||
|
|
||||||
|
const imageFiles: Array<ImageFile> = await page.evaluate(getImageFiles);
|
||||||
|
|
||||||
|
return imageFiles.map((imageFile) => async () => {
|
||||||
|
const dataUrl = await browser.drawImage(page, imageFile);
|
||||||
|
|
||||||
|
process.stderr.write(".");
|
||||||
|
|
||||||
|
return dataUrl;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
FanzaDoujin.siteUrl = (url: URL) =>
|
||||||
|
url.href.startsWith(
|
||||||
|
"https://www.dmm.co.jp/dc/-/mylibrary/detail/=/product_id=",
|
||||||
|
);
|
|
@ -1,9 +1,5 @@
|
||||||
import type { Book } from "../library";
|
import type { Book } from "../library";
|
||||||
import type { Browser } from "../browser";
|
import type { Browser, ImageFile } from "../browser";
|
||||||
|
|
||||||
type ImageFile = {
|
|
||||||
url: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function getImageFiles(): Promise<Array<ImageFile>> {
|
async function getImageFiles(): Promise<Array<ImageFile>> {
|
||||||
const pages: NodeListOf<HTMLElement> = await new Promise(async function (
|
const pages: NodeListOf<HTMLElement> = await new Promise(async function (
|
||||||
|
@ -41,26 +37,6 @@ async function getImageFiles(): Promise<Array<ImageFile>> {
|
||||||
return [...images].map((image) => ({ url: image.href.baseVal }));
|
return [...images].map((image) => ({ url: image.href.baseVal }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchImage(imageFile: ImageFile): Promise<string> {
|
|
||||||
const res = await fetch(imageFile.url);
|
|
||||||
const blob = await res.blob();
|
|
||||||
const dataUrl: string = await new Promise((resolve, reject) => {
|
|
||||||
const fileReader = Object.assign(new FileReader(), {
|
|
||||||
onload(): void {
|
|
||||||
resolve(this.result);
|
|
||||||
},
|
|
||||||
onerror(e: ErrorEvent): void {
|
|
||||||
const error = new Error(`${e.type}: ${e.message}`);
|
|
||||||
reject(error);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
fileReader.readAsDataURL(blob);
|
|
||||||
});
|
|
||||||
|
|
||||||
return dataUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GooglePlayBooks(browser: Browser) {
|
export function GooglePlayBooks(browser: Browser) {
|
||||||
return {
|
return {
|
||||||
async login() {
|
async login() {
|
||||||
|
@ -150,7 +126,7 @@ export function GooglePlayBooks(browser: Browser) {
|
||||||
for (const imageFile of imageFiles) {
|
for (const imageFile of imageFiles) {
|
||||||
if (fileMap.has(imageFile.url)) continue;
|
if (fileMap.has(imageFile.url)) continue;
|
||||||
|
|
||||||
const dataUrl = await frame.evaluate(fetchImage, imageFile);
|
const dataUrl = await browser.drawImage(frame, imageFile);
|
||||||
|
|
||||||
process.stderr.write(".");
|
process.stderr.write(".");
|
||||||
|
|
||||||
|
@ -163,4 +139,5 @@ export function GooglePlayBooks(browser: Browser) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
GooglePlayBooks.site = ["https://play.google.com"];
|
GooglePlayBooks.siteUrl = (url: URL) =>
|
||||||
|
url.origin === "https://play.google.com";
|
||||||
|
|
Loading…
Add table
Reference in a new issue