168 lines
4.3 KiB
TypeScript
168 lines
4.3 KiB
TypeScript
import type * as Playwright from "playwright";
|
|
import { chromium, devices } from "playwright";
|
|
import type { Database } from "./database";
|
|
import type { TPlatform } from "./platform";
|
|
|
|
export type PageOrFrame = Playwright.Page | Playwright.Frame;
|
|
|
|
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;
|
|
}
|
|
|
|
async function dataUrlToBlob(dataUrl: string): Promise<Blob> {
|
|
const res = await fetch(dataUrl);
|
|
return await res.blob();
|
|
}
|
|
|
|
export type Browser = {
|
|
loadBrowserContext(platform: TPlatform): Promise<Playwright.BrowserContext>;
|
|
saveBrowserContext(platform: TPlatform, ctx: BrowserContext): Promise<void>;
|
|
newContext(): Promise<Playwright.BrowserContext>;
|
|
close(): Promise<void>;
|
|
drawImage(
|
|
pageOrFrame: Playwright.Page | Playwright.Frame,
|
|
imageFile: ImageFile,
|
|
): Promise<Blob>;
|
|
};
|
|
|
|
export type BrowserContext = Playwright.BrowserContext;
|
|
|
|
export async function createBrowser({
|
|
db,
|
|
headless = true,
|
|
}: {
|
|
db: Database;
|
|
headless?: boolean;
|
|
}): Promise<Browser> {
|
|
const { userAgent } = devices["Desktop Chrome"];
|
|
const browser = await chromium.launch({
|
|
headless,
|
|
args: ["--disable-blink-features=AutomationControlled"],
|
|
});
|
|
|
|
return {
|
|
async loadBrowserContext(
|
|
platform: TPlatform,
|
|
): Promise<Playwright.BrowserContext> {
|
|
const { secrets } = await db.get(
|
|
`select secrets from platforms where name = ?`,
|
|
platform,
|
|
);
|
|
|
|
const storageState = JSON.parse(secrets) ?? undefined;
|
|
const ctx = await browser.newContext({ storageState, userAgent });
|
|
return ctx;
|
|
},
|
|
|
|
async saveBrowserContext(
|
|
platform: TPlatform,
|
|
ctx: BrowserContext,
|
|
): Promise<void> {
|
|
const secrets = await ctx.storageState();
|
|
await db.run(
|
|
`update platforms set secrets = ? where name = ?`,
|
|
JSON.stringify(secrets),
|
|
platform,
|
|
);
|
|
},
|
|
|
|
newContext: () => browser.newContext(),
|
|
close: () => browser.close(),
|
|
|
|
async drawImage(
|
|
pageOrFrame: Playwright.Page | Playwright.Frame,
|
|
imageFile: ImageFile,
|
|
): Promise<Blob> {
|
|
if (Array.isArray(imageFile.blocks) && imageFile.blocks.length > 0) {
|
|
const dataUrl = await pageOrFrame.evaluate(drawImage, imageFile);
|
|
return await dataUrlToBlob(dataUrl);
|
|
}
|
|
|
|
if (imageFile.url.startsWith("blob:")) {
|
|
const dataUrl = await pageOrFrame.evaluate(fetchImage, imageFile);
|
|
return await dataUrlToBlob(dataUrl);
|
|
}
|
|
|
|
const page = "page" in pageOrFrame ? pageOrFrame.page() : pageOrFrame;
|
|
const res = await page.context().request.get(imageFile.url);
|
|
const headers = res.headers();
|
|
const buffer = await res.body();
|
|
|
|
let type = headers["content-type"];
|
|
|
|
if (type === "binary/octet-stream") {
|
|
const [, extension] =
|
|
/^attachment *; *filename="[^"]+[.]([^.]+)" *(?:$|;)/i.exec(
|
|
headers["content-disposition"],
|
|
) ?? [];
|
|
|
|
switch (extension) {
|
|
case "zip":
|
|
type = "application/zip";
|
|
break;
|
|
}
|
|
}
|
|
|
|
return new Blob([buffer], { type });
|
|
},
|
|
};
|
|
}
|