diff --git a/.github/workflows/deno.yml b/.github/workflows/deno.yml new file mode 100644 index 00000000..16068ce6 --- /dev/null +++ b/.github/workflows/deno.yml @@ -0,0 +1,49 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# This workflow will install Deno then run: +# - `deno fmt` +# - `deno lint` +# - `deno check` +# For more information see: https://github.com/denoland/setup-deno + +name: Deno + +on: + push: + branches: ["master"] + pull_request: + branches: ["master"] + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - name: Setup repo + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Setup Deno + uses: denoland/setup-deno@v2 + with: + deno-version: latest + cache: true + + - name: Verify formatting + run: deno fmt --check + + - name: Run linter + run: deno lint + + - name: Check types + run: deno check --frozen=false *.ts + + # - name: Run tests + # run: deno test -A diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..da970314 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ejs"] + path = ejs + url = https://github.com/yt-dlp/ejs.git diff --git a/deno.json b/deno.json index 14e7e38e..01bbf68b 100644 --- a/deno.json +++ b/deno.json @@ -2,11 +2,21 @@ "compilerOptions": { "lib": [ "deno.worker" ] }, + "fmt": { + "include": ["src/", "*.ts"], + "indentWidth": 4 + }, "imports": { "@std/crypto": "jsr:@std/crypto@^0.224.0", "@std/fs": "jsr:@std/fs@^0.224.0", "@std/http": "jsr:@std/http@0.224.5", "@std/path": "jsr:@std/path@^0.224.0", "ejs/": "https://esm.sh/gh/yt-dlp/ejs@0.3.0&standalone/" + }, + "lint": { + "include": ["src/", "*.ts"], + "rules": { + "exclude": [ "no-import-prefix" ] + } } } diff --git a/ejs b/ejs new file mode 160000 index 00000000..2655b1f5 --- /dev/null +++ b/ejs @@ -0,0 +1 @@ +Subproject commit 2655b1f55f98e5870d4e124704a21f4d793b4e1c diff --git a/server.ts b/server.ts index c7296e05..b21c0f8c 100644 --- a/server.ts +++ b/server.ts @@ -28,7 +28,7 @@ async function baseHandler(req: Request): Promise { { status: 404, headers: { "Content-Type": "text/plain" }, - } + }, ); } } @@ -48,7 +48,7 @@ async function baseHandler(req: Request): Promise { } } - if (pathname === '/metrics') { + if (pathname === "/metrics") { return new Response(registry.metrics(), { headers: { "Content-Type": "text/plain" }, }); @@ -57,28 +57,39 @@ async function baseHandler(req: Request): Promise { const authHeader = req.headers.get("authorization"); if (API_TOKEN && API_TOKEN !== "") { if (authHeader !== API_TOKEN) { - const error = authHeader ? 'Invalid API token' : 'Missing API token'; - return new Response(JSON.stringify({ error }), { status: 401, headers: { "Content-Type": "application/json" } }); + const error = authHeader + ? "Invalid API token" + : "Missing API token"; + return new Response(JSON.stringify({ error }), { + status: 401, + headers: { "Content-Type": "application/json" }, + }); } } let handle: (ctx: RequestContext) => Promise; - if (pathname === '/decrypt_signature') { + if (pathname === "/decrypt_signature") { handle = handleDecryptSignature; - } else if (pathname === '/get_sts') { + } else if (pathname === "/get_sts") { handle = handleGetSts; - } else if (pathname === '/resolve_url') { + } else if (pathname === "/resolve_url") { handle = handleResolveUrl; } else { - return new Response(JSON.stringify({ error: 'Not Found' }), { status: 404, headers: { "Content-Type": "application/json" } }); + return new Response(JSON.stringify({ error: "Not Found" }), { + status: 404, + headers: { "Content-Type": "application/json" }, + }); } let body; try { body = await req.json() as ApiRequest; } catch { - return new Response(JSON.stringify({ error: 'Invalid JSON body' }), { status: 400, headers: { "Content-Type": "application/json" } }); + return new Response(JSON.stringify({ error: "Invalid JSON body" }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); } const ctx: RequestContext = { req, body }; @@ -88,8 +99,8 @@ async function baseHandler(req: Request): Promise { const handler = baseHandler; -const port = Deno.env.get("PORT") || 8001; -const host = Deno.env.get("HOST") || '0.0.0.0'; +const port = Deno.env.get("PORT") || "8001"; +const host = Deno.env.get("HOST") || "0.0.0.0"; await initializeCache(); initializeWorkers(); diff --git a/src/handlers/decryptSignature.ts b/src/handlers/decryptSignature.ts index d51ce8c6..ea33ceb8 100644 --- a/src/handlers/decryptSignature.ts +++ b/src/handlers/decryptSignature.ts @@ -1,21 +1,33 @@ import { getSolvers } from "../solver.ts"; -import type { RequestContext, SignatureRequest, SignatureResponse } from "../types.ts"; +import type { + RequestContext, + SignatureRequest, + SignatureResponse, +} from "../types.ts"; -export async function handleDecryptSignature(ctx: RequestContext): Promise { - const { encrypted_signature, n_param, player_url } = ctx.body as SignatureRequest; +export async function handleDecryptSignature( + ctx: RequestContext, +): Promise { + const { encrypted_signature, n_param, player_url } = ctx + .body as SignatureRequest; const solvers = await getSolvers(player_url); if (!solvers) { - return new Response(JSON.stringify({ error: "Failed to generate solvers from player script" }), { status: 500, headers: { "Content-Type": "application/json" } }); + return new Response( + JSON.stringify({ + error: "Failed to generate solvers from player script", + }), + { status: 500, headers: { "Content-Type": "application/json" } }, + ); } - let decrypted_signature = ''; + let decrypted_signature = ""; if (encrypted_signature && solvers.sig) { decrypted_signature = solvers.sig(encrypted_signature); } - let decrypted_n_sig = ''; + let decrypted_n_sig = ""; if (n_param && solvers.n) { decrypted_n_sig = solvers.n(n_param); } @@ -25,5 +37,8 @@ export async function handleDecryptSignature(ctx: RequestContext): Promise { const response: StsResponse = { sts: cachedSts }; return new Response(JSON.stringify(response), { status: 200, - headers: { "Content-Type": "application/json", "X-Cache-Hit": "true" }, + headers: { + "Content-Type": "application/json", + "X-Cache-Hit": "true", + }, }); } @@ -26,12 +29,18 @@ export async function handleGetSts(ctx: RequestContext): Promise { const response: StsResponse = { sts }; return new Response(JSON.stringify(response), { status: 200, - headers: { "Content-Type": "application/json", "X-Cache-Hit": "false" }, + headers: { + "Content-Type": "application/json", + "X-Cache-Hit": "false", + }, }); } else { - return new Response(JSON.stringify({ error: "Timestamp not found in player script" }), { - status: 404, - headers: { "Content-Type": "application/json" }, - }); + return new Response( + JSON.stringify({ error: "Timestamp not found in player script" }), + { + status: 404, + headers: { "Content-Type": "application/json" }, + }, + ); } -} \ No newline at end of file +} diff --git a/src/handlers/resolveUrl.ts b/src/handlers/resolveUrl.ts index 8f17a30b..7bd32d5e 100644 --- a/src/handlers/resolveUrl.ts +++ b/src/handlers/resolveUrl.ts @@ -1,23 +1,46 @@ import { getSolvers } from "../solver.ts"; -import type { RequestContext, ResolveUrlRequest, ResolveUrlResponse } from "../types.ts"; +import type { + RequestContext, + ResolveUrlRequest, + ResolveUrlResponse, +} from "../types.ts"; export async function handleResolveUrl(ctx: RequestContext): Promise { - const { stream_url, player_url, encrypted_signature, signature_key, n_param: nParamFromRequest } = ctx.body as ResolveUrlRequest; + const { + stream_url, + player_url, + encrypted_signature, + signature_key, + n_param: nParamFromRequest, + } = ctx.body as ResolveUrlRequest; const solvers = await getSolvers(player_url); if (!solvers) { - return new Response(JSON.stringify({ error: "Failed to generate solvers from player script" }), { status: 500, headers: { "Content-Type": "application/json" } }); + return new Response( + JSON.stringify({ + error: "Failed to generate solvers from player script", + }), + { status: 500, headers: { "Content-Type": "application/json" } }, + ); } const url = new URL(stream_url); if (encrypted_signature) { if (!solvers.sig) { - return new Response(JSON.stringify({ error: "No signature solver found for this player" }), { status: 500, headers: { "Content-Type": "application/json" } }); + return new Response( + JSON.stringify({ + error: "No signature solver found for this player", + }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + }, + ); } const decryptedSig = solvers.sig(encrypted_signature); - const sigKey = signature_key || 'sig'; + const sigKey = signature_key || "sig"; url.searchParams.set(sigKey, decryptedSig); url.searchParams.delete("s"); } @@ -29,15 +52,26 @@ export async function handleResolveUrl(ctx: RequestContext): Promise { if (solvers.n) { if (!nParam) { - return new Response(JSON.stringify({ error: "n_param not found in request or stream_url" }), { status: 400, headers: { "Content-Type": "application/json" } }); + return new Response( + JSON.stringify({ + error: "n_param not found in request or stream_url", + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ); } const decryptedN = solvers.n(nParam); url.searchParams.set("n", decryptedN); } - + const response: ResolveUrlResponse = { resolved_url: url.toString(), }; - return new Response(JSON.stringify(response), { status: 200, headers: { "Content-Type": "application/json" } }); -} \ No newline at end of file + return new Response(JSON.stringify(response), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} diff --git a/src/instrumentedCache.ts b/src/instrumentedCache.ts index 1e3d0b9e..2c16bd1a 100644 --- a/src/instrumentedCache.ts +++ b/src/instrumentedCache.ts @@ -16,4 +16,4 @@ export class InstrumentedLRU extends LRU { super.remove(key); cacheSize.labels({ cache_name: this.cacheName }).set(this.size); } -} \ No newline at end of file +} diff --git a/src/metrics.ts b/src/metrics.ts index d5537e9b..f4407a05 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -20,7 +20,14 @@ export const endpointHits = Counter.with({ export const responseCodes = Counter.with({ name: "http_responses_total", help: "Total number of HTTP responses.", - labels: ["method", "pathname", "status", "player_id", "plugin_version", "user_agent"], + labels: [ + "method", + "pathname", + "status", + "player_id", + "plugin_version", + "user_agent", + ], registry: [registry], }); @@ -58,4 +65,4 @@ export const playerScriptFetches = Counter.with({ help: "Total number of player script fetches.", labels: ["player_url", "status"], registry: [registry], -}); \ No newline at end of file +}); diff --git a/src/middleware.ts b/src/middleware.ts index 11b3ad6a..2ad421de 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,5 +1,5 @@ import { extractPlayerId } from "./utils.ts"; -import { endpointHits, responseCodes, endpointLatency } from "./metrics.ts"; +import { endpointHits, endpointLatency, responseCodes } from "./metrics.ts"; import type { RequestContext } from "./types.ts"; type Next = (ctx: RequestContext) => Promise; @@ -8,10 +8,17 @@ export function withMetrics(handler: Next): Next { return async (ctx: RequestContext) => { const { pathname } = new URL(ctx.req.url); const playerId = extractPlayerId(ctx.body.player_url); - const pluginVersion = ctx.req.headers.get("Plugin-Version") ?? "unknown"; + const pluginVersion = ctx.req.headers.get("Plugin-Version") ?? + "unknown"; const userAgent = ctx.req.headers.get("User-Agent") ?? "unknown"; - endpointHits.labels({ method: ctx.req.method, pathname, player_id: playerId, plugin_version: pluginVersion, user_agent: userAgent }).inc(); + endpointHits.labels({ + method: ctx.req.method, + pathname, + player_id: playerId, + plugin_version: pluginVersion, + user_agent: userAgent, + }).inc(); const start = performance.now(); let response: Response; @@ -19,13 +26,30 @@ export function withMetrics(handler: Next): Next { response = await handler(ctx); } catch (e) { const message = e instanceof Error ? e.message : String(e); - response = new Response(JSON.stringify({ error: message }), { status: 500, headers: { "Content-Type": "application/json" } }); + response = new Response(JSON.stringify({ error: message }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); } const duration = (performance.now() - start) / 1000; - const cached = response.headers.get("X-Cache-Hit") === "true" ? "true" : "false"; - endpointLatency.labels({ method: ctx.req.method, pathname, player_id: playerId, cached}).observe(duration); - responseCodes.labels({ method: ctx.req.method, pathname, status: String(response.status), player_id: playerId, plugin_version: pluginVersion, user_agent: userAgent }).inc(); + const cached = response.headers.get("X-Cache-Hit") === "true" + ? "true" + : "false"; + endpointLatency.labels({ + method: ctx.req.method, + pathname, + player_id: playerId, + cached, + }).observe(duration); + responseCodes.labels({ + method: ctx.req.method, + pathname, + status: String(response.status), + player_id: playerId, + plugin_version: pluginVersion, + user_agent: userAgent, + }).inc(); return response; }; diff --git a/src/playerCache.ts b/src/playerCache.ts index b1fd3c47..8e0cda7a 100644 --- a/src/playerCache.ts +++ b/src/playerCache.ts @@ -5,21 +5,26 @@ import { cacheSize, playerScriptFetches } from "./metrics.ts"; let cache_prefix = Deno.cwd(); const HOME = Deno.env.get("HOME"); -if ( HOME ) { - cache_prefix = join(HOME, '.cache', 'yt-cipher'); +if (HOME) { + cache_prefix = join(HOME, ".cache", "yt-cipher"); } const CACHE_HOME = Deno.env.get("XDG_CACHE_HOME"); -if ( CACHE_HOME ) { - cache_prefix = join(CACHE_HOME, 'yt-cipher'); +if (CACHE_HOME) { + cache_prefix = join(CACHE_HOME, "yt-cipher"); } -export const CACHE_DIR = join(cache_prefix, 'player_cache'); +export const CACHE_DIR = join(cache_prefix, "player_cache"); export async function getPlayerFilePath(playerUrl: string): Promise { // This hash of the player script url will mean that diff region scripts are treated as unequals, even for the same version # // I dont think I have ever seen 2 scripts of the same version differ between regions but if they ever do this will catch it // As far as player script access, I haven't ever heard about YT ratelimiting those either so ehh - const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(playerUrl)); - const hash = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join(''); + const hashBuffer = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(playerUrl), + ); + const hash = Array.from(new Uint8Array(hashBuffer)).map((b) => + b.toString(16).padStart(2, "0") + ).join(""); const filePath = join(CACHE_DIR, `${hash}.js`); try { @@ -31,9 +36,14 @@ export async function getPlayerFilePath(playerUrl: string): Promise { if (error instanceof Deno.errors.NotFound) { console.log(`Cache miss for player: ${playerUrl}. Fetching...`); const response = await fetch(playerUrl); - playerScriptFetches.labels({ player_url: playerUrl, status: response.statusText }).inc(); + playerScriptFetches.labels({ + player_url: playerUrl, + status: response.statusText, + }).inc(); if (!response.ok) { - throw new Error(`Failed to fetch player from ${playerUrl}: ${response.statusText}`); + throw new Error( + `Failed to fetch player from ${playerUrl}: ${response.statusText}`, + ); } const playerContent = await response.text(); await Deno.writeTextFile(filePath, playerContent); @@ -43,8 +53,8 @@ export async function getPlayerFilePath(playerUrl: string): Promise { for await (const _ of Deno.readDir(CACHE_DIR)) { fileCount++; } - cacheSize.labels({ cache_name: 'player' }).set(fileCount); - + cacheSize.labels({ cache_name: "player" }).set(fileCount); + console.log(`Saved player to cache: ${filePath}`); return filePath; } @@ -63,7 +73,8 @@ export async function initializeCache() { if (dirEntry.isFile) { const filePath = join(CACHE_DIR, dirEntry.name); const stat = await Deno.stat(filePath); - const lastAccessed = stat.atime?.getTime() ?? stat.mtime?.getTime() ?? stat.birthtime?.getTime(); + const lastAccessed = stat.atime?.getTime() ?? + stat.mtime?.getTime() ?? stat.birthtime?.getTime(); if (lastAccessed && (Date.now() - lastAccessed > thirtyDays)) { console.log(`Deleting stale player cache file: ${filePath}`); await Deno.remove(filePath); @@ -72,6 +83,6 @@ export async function initializeCache() { } } } - cacheSize.labels({ cache_name: 'player' }).set(fileCount); + cacheSize.labels({ cache_name: "player" }).set(fileCount); console.log(`Player cache directory ensured at: ${CACHE_DIR}`); } diff --git a/src/preprocessedCache.ts b/src/preprocessedCache.ts index 45cbc400..c6c0b2bf 100644 --- a/src/preprocessedCache.ts +++ b/src/preprocessedCache.ts @@ -1,6 +1,9 @@ import { InstrumentedLRU } from "./instrumentedCache.ts"; // The key is the hash of the player URL, and the value is the preprocessed script content. -const cacheSizeEnv = Deno.env.get('PREPROCESSED_CACHE_SIZE'); +const cacheSizeEnv = Deno.env.get("PREPROCESSED_CACHE_SIZE"); const maxCacheSize = cacheSizeEnv ? parseInt(cacheSizeEnv, 10) : 150; -export const preprocessedCache = new InstrumentedLRU('preprocessed', maxCacheSize); \ No newline at end of file +export const preprocessedCache = new InstrumentedLRU( + "preprocessed", + maxCacheSize, +); diff --git a/src/solver.ts b/src/solver.ts index dc617fbb..6d5f8161 100644 --- a/src/solver.ts +++ b/src/solver.ts @@ -29,7 +29,7 @@ export async function getSolvers(player_url: string): Promise { } preprocessedCache.set(playerCacheKey, preprocessedPlayer); } - + solvers = getFromPrepared(preprocessedPlayer); if (solvers) { solverCache.set(playerCacheKey, solvers); @@ -37,4 +37,4 @@ export async function getSolvers(player_url: string): Promise { } return null; -} \ No newline at end of file +} diff --git a/src/solverCache.ts b/src/solverCache.ts index b74f5044..b23fef24 100644 --- a/src/solverCache.ts +++ b/src/solverCache.ts @@ -2,6 +2,6 @@ import { InstrumentedLRU } from "./instrumentedCache.ts"; import type { Solvers } from "./types.ts"; // key = hash of the player url -const cacheSizeEnv = Deno.env.get('SOLVER_CACHE_SIZE'); +const cacheSizeEnv = Deno.env.get("SOLVER_CACHE_SIZE"); const maxCacheSize = cacheSizeEnv ? parseInt(cacheSizeEnv, 10) : 50; -export const solverCache = new InstrumentedLRU('solver', maxCacheSize); \ No newline at end of file +export const solverCache = new InstrumentedLRU("solver", maxCacheSize); diff --git a/src/stsCache.ts b/src/stsCache.ts index 43aea5d5..6ae804dd 100644 --- a/src/stsCache.ts +++ b/src/stsCache.ts @@ -1,6 +1,6 @@ import { InstrumentedLRU } from "./instrumentedCache.ts"; // key = hash of player URL -const cacheSizeEnv = Deno.env.get('STS_CACHE_SIZE'); +const cacheSizeEnv = Deno.env.get("STS_CACHE_SIZE"); const maxCacheSize = cacheSizeEnv ? parseInt(cacheSizeEnv, 10) : 150; -export const stsCache = new InstrumentedLRU('sts', maxCacheSize); \ No newline at end of file +export const stsCache = new InstrumentedLRU("sts", maxCacheSize); diff --git a/src/types.ts b/src/types.ts index c82a275c..2919752b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,14 +41,15 @@ export interface WorkerWithStatus extends Worker { export interface Task { data: string; resolve: (output: string) => void; + // deno-lint-ignore no-explicit-any reject: (error: any) => void; } export type ApiRequest = SignatureRequest | StsRequest | ResolveUrlRequest; // Parsing into this context helps avoid multi copies of requests -// since request body can only be read once. +// since request body can only be read once. export interface RequestContext { req: Request; body: ApiRequest; -} \ No newline at end of file +} diff --git a/src/utils.ts b/src/utils.ts index 43405f1c..76b0a227 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,9 +2,9 @@ const ALLOWED_HOSTNAMES = ["youtube.com", "www.youtube.com", "m.youtube.com"]; export function validateAndNormalizePlayerUrl(playerUrl: string): string { // Handle relative paths - if (playerUrl.startsWith('/')) { - if (playerUrl.startsWith('/s/player/')) { - return `https://www.youtube.com${playerUrl}`; + if (playerUrl.startsWith("/")) { + if (playerUrl.startsWith("/s/player/")) { + return `https://www.youtube.com${playerUrl}`; } throw new Error(`Invalid player path: ${playerUrl}`); } @@ -17,7 +17,7 @@ export function validateAndNormalizePlayerUrl(playerUrl: string): string { } else { throw new Error(`Player URL from invalid host: ${url.hostname}`); } - } catch (e) { + } catch (_e) { // Not a valid URL, and not a valid path. throw new Error(`Invalid player URL: ${playerUrl}`); } @@ -25,17 +25,17 @@ export function validateAndNormalizePlayerUrl(playerUrl: string): string { export function extractPlayerId(playerUrl: string): string { try { const url = new URL(playerUrl); - const pathParts = url.pathname.split('/'); - const playerIndex = pathParts.indexOf('player'); + const pathParts = url.pathname.split("/"); + const playerIndex = pathParts.indexOf("player"); if (playerIndex !== -1 && playerIndex + 1 < pathParts.length) { return pathParts[playerIndex + 1]; } - } catch (e) { + } catch (_e) { // Fallback for relative paths const match = playerUrl.match(/\/s\/player\/([^\/]+)/); if (match) { return match[1]; } } - return 'unknown'; -} \ No newline at end of file + return "unknown"; +} diff --git a/src/validation.ts b/src/validation.ts index 097f0a59..934ba4b0 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -3,26 +3,34 @@ import { validateAndNormalizePlayerUrl } from "./utils.ts"; type Next = (ctx: RequestContext) => Promise; type ValidationSchema = { + // deno-lint-ignore no-explicit-any [key: string]: (value: any) => boolean; }; const signatureRequestSchema: ValidationSchema = { - player_url: (val) => typeof val === 'string', + player_url: (val) => typeof val === "string", }; const stsRequestSchema: ValidationSchema = { - player_url: (val) => typeof val === 'string', + player_url: (val) => typeof val === "string", }; const resolveUrlRequestSchema: ValidationSchema = { - player_url: (val) => typeof val === 'string', - stream_url: (val) => typeof val === 'string', + player_url: (val) => typeof val === "string", + stream_url: (val) => typeof val === "string", }; -function validateObject(obj: any, schema: ValidationSchema): { isValid: boolean, errors: string[] } { +function validateObject( + // deno-lint-ignore no-explicit-any + obj: any, + schema: ValidationSchema, +): { isValid: boolean; errors: string[] } { const errors: string[] = []; for (const key in schema) { - if (!obj.hasOwnProperty(key) || !schema[key](obj[key])) { + if ( + !Object.prototype.hasOwnProperty.call(obj, key) || + !schema[key](obj[key]) + ) { errors.push(`'${key}' is missing or invalid`); } } @@ -30,42 +38,53 @@ function validateObject(obj: any, schema: ValidationSchema): { isValid: boolean, } export function withValidation(handler: Next): Next { + // deno-lint-ignore require-await return async (ctx: RequestContext) => { const { pathname } = new URL(ctx.req.url); let schema: ValidationSchema; - if (pathname === '/decrypt_signature') { + if (pathname === "/decrypt_signature") { schema = signatureRequestSchema; - } else if (pathname === '/get_sts') { + } else if (pathname === "/get_sts") { schema = stsRequestSchema; - } else if (pathname === '/resolve_url') { + } else if (pathname === "/resolve_url") { schema = resolveUrlRequestSchema; } else { return handler(ctx); } - + const body = ctx.body as ApiRequest; const { isValid, errors } = validateObject(body, schema); if (!isValid) { - return new Response(JSON.stringify({ error: `Invalid request body: ${errors.join(', ')}` }), { - status: 400, - headers: { "Content-Type": "application/json" }, - }); + return new Response( + JSON.stringify({ + error: `Invalid request body: ${errors.join(", ")}`, + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ); } - + try { - const normalizedUrl = validateAndNormalizePlayerUrl(body.player_url); + const normalizedUrl = validateAndNormalizePlayerUrl( + body.player_url, + ); // mutate the context with the normalized URL ctx.body.player_url = normalizedUrl; } catch (e) { - return new Response(JSON.stringify({ error: (e as Error).message }), { - status: 400, - headers: { "Content-Type": "application/json" }, - }); + return new Response( + JSON.stringify({ error: (e as Error).message }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ); } return handler(ctx); }; -} \ No newline at end of file +} diff --git a/src/workerPool.ts b/src/workerPool.ts index 79b6a9b2..60a00b56 100644 --- a/src/workerPool.ts +++ b/src/workerPool.ts @@ -1,12 +1,13 @@ -import type { WorkerWithStatus, Task } from "./types.ts"; +import type { Task, WorkerWithStatus } from "./types.ts"; -const CONCURRENCY = parseInt(Deno.env.get("MAX_THREADS") || "", 10) || navigator.hardwareConcurrency || 1; +const CONCURRENCY = parseInt(Deno.env.get("MAX_THREADS") || "", 10) || + navigator.hardwareConcurrency || 1; const workers: WorkerWithStatus[] = []; const taskQueue: Task[] = []; function dispatch() { - const idleWorker = workers.find(w => w.isIdle); + const idleWorker = workers.find((w) => w.isIdle); if (!idleWorker || taskQueue.length === 0) { return; } @@ -19,7 +20,7 @@ function dispatch() { idleWorker.isIdle = true; const { type, data } = e.data; - if (type === 'success') { + if (type === "success") { task.resolve(data); } else { console.error("Received error from worker:", data); @@ -43,9 +44,12 @@ export function execInPool(data: string): Promise { export function initializeWorkers() { for (let i = 0; i < CONCURRENCY; i++) { - const worker: WorkerWithStatus = new Worker(new URL("../worker.ts", import.meta.url).href, { type: "module" }); + const worker: WorkerWithStatus = new Worker( + new URL("../worker.ts", import.meta.url).href, + { type: "module" }, + ); worker.isIdle = true; workers.push(worker); } console.log(`Initialized ${CONCURRENCY} workers`); -} \ No newline at end of file +} diff --git a/worker.ts b/worker.ts index 6a35aae2..e541684a 100644 --- a/worker.ts +++ b/worker.ts @@ -3,13 +3,13 @@ import { preprocessPlayer } from "ejs/src/yt/solver/solvers.ts"; self.onmessage = (e: MessageEvent) => { try { const output = preprocessPlayer(e.data); - self.postMessage({ type: 'success', data: output }); + self.postMessage({ type: "success", data: output }); } catch (error) { self.postMessage({ - type: 'error', + type: "error", data: { message: error, - } + }, }); } -}; \ No newline at end of file +};