gadl/browser.ts

167 lines
4.3 KiB
TypeScript

import * as Playwright from "playwright";
import { chromium, devices } from "playwright";
import type { Database } from "./database";
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;
}
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 });
},
};
}