From 3611d412401c6b224cfa3db0ed50074e3caf9d47 Mon Sep 17 00:00:00 2001 From: tcely Date: Tue, 25 Nov 2025 23:52:45 -0500 Subject: [PATCH 01/71] Add a submodule for `ejs` --- .gitmodules | 3 +++ ejs | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 ejs 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/ejs b/ejs new file mode 160000 index 00000000..2655b1f5 --- /dev/null +++ b/ejs @@ -0,0 +1 @@ +Subproject commit 2655b1f55f98e5870d4e124704a21f4d793b4e1c From c03119330a2f0877bb67c6d2b59674e7331025a8 Mon Sep 17 00:00:00 2001 From: tcely Date: Tue, 25 Nov 2025 23:55:56 -0500 Subject: [PATCH 02/71] Remove `ejs` from .gitignore --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index defe4625..1d74e219 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -ejs/ -.vscode/ \ No newline at end of file +.vscode/ From fe537a5b1a4a84fdb333acd0a8de678fc712d842 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 26 Nov 2025 00:11:31 -0500 Subject: [PATCH 03/71] Update `ejs` with git in Dockerfile --- Dockerfile | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index d6c98202..c7771ec9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,16 +4,11 @@ WORKDIR /usr/src/app RUN apt-get update && apt-get install -y git npm -RUN git clone https://github.com/yt-dlp/ejs.git -# Pin to a specific commit -RUN cd ejs && git checkout 2655b1f55f98e5870d4e124704a21f4d793b4e1c && cd .. - -COPY scripts/patch-ejs.ts ./scripts/patch-ejs.ts -RUN deno run --allow-read --allow-write ./scripts/patch-ejs.ts +COPY . . -RUN rm -rf ./ejs/.git ./ejs/node_modules || true +RUN git submodule update --init --recursive -COPY . . +RUN deno run --allow-read --allow-write ./scripts/patch-ejs.ts RUN deno compile \ --no-check \ @@ -35,4 +30,4 @@ COPY --from=builder --chown=nonroot:nonroot /usr/src/app/player_cache /app/playe USER nonroot EXPOSE 8001 -ENTRYPOINT ["/app/server"] \ No newline at end of file +ENTRYPOINT ["/app/server"] From 862abe7c3fd7c0237ea438ae0e97feb974fe8cb9 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 26 Nov 2025 00:39:47 -0500 Subject: [PATCH 04/71] Checkout submodules recursively --- .github/workflows/main.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0b8eed2f..86124530 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,6 +14,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + submodules: recursive - name: Log in to GitHub Container Registry uses: docker/login-action@v3 @@ -41,4 +43,4 @@ jobs: platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file + labels: ${{ steps.meta.outputs.labels }} From c8609a3b61e64ceeb7950c3719f58de1b6f44674 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 26 Nov 2025 04:24:08 -0500 Subject: [PATCH 05/71] Create deno.json --- deno.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 deno.json diff --git a/deno.json b/deno.json new file mode 100644 index 00000000..5fd5a312 --- /dev/null +++ b/deno.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "lib": [ "deno.worker" ] + }, + "imports": { + "astring": "npm:astring@^1.9.0", + "meriyah": "npm:meriyah@^6.1.4" + } +} From b499a59feca254be55183ad8c6d0a4fea761dc72 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 26 Nov 2025 04:42:18 -0500 Subject: [PATCH 06/71] Update imports to use jsr --- src/playerCache.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/playerCache.ts b/src/playerCache.ts index 57fca1b0..75bd2ed3 100644 --- a/src/playerCache.ts +++ b/src/playerCache.ts @@ -1,6 +1,6 @@ -import { crypto } from "https://deno.land/std@0.224.0/crypto/mod.ts"; -import { ensureDir } from "https://deno.land/std@0.224.0/fs/ensure_dir.ts"; -import { join } from "https://deno.land/std@0.224.0/path/mod.ts"; +import { crypto } from "jsr:@std/crypto@0.224.0"; +import { ensureDir } from "jsr:@std/fs@0.224.0"; +import { join } from "jsr:@std/path@0.224.0"; import { cacheSize, playerScriptFetches } from "./metrics.ts"; export const CACHE_DIR = join(Deno.cwd(), 'player_cache'); @@ -65,4 +65,4 @@ export async function initializeCache() { } cacheSize.labels({ cache_name: 'player' }).set(fileCount); console.log(`Player cache directory ensured at: ${CACHE_DIR}`); -} \ No newline at end of file +} From 61e8aa36c640b9baf3a8522e8c205c7574c7baf0 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 26 Nov 2025 04:47:46 -0500 Subject: [PATCH 07/71] Remove old work-arounds - Patching ejs is no longer needed with the imports defined in `deno.json` - The compiler options allow including the worker --- Dockerfile | 3 --- 1 file changed, 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index c7771ec9..9cac9518 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,10 +8,7 @@ COPY . . RUN git submodule update --init --recursive -RUN deno run --allow-read --allow-write ./scripts/patch-ejs.ts - RUN deno compile \ - --no-check \ --output server \ --allow-net --allow-read --allow-write --allow-env \ --include worker.ts \ From e0b14887039455b6e18dfe62b46a571427ae1945 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 26 Nov 2025 04:49:14 -0500 Subject: [PATCH 08/71] Delete scripts directory --- scripts/patch-ejs.ts | 36 ------------------------------------ 1 file changed, 36 deletions(-) delete mode 100644 scripts/patch-ejs.ts diff --git a/scripts/patch-ejs.ts b/scripts/patch-ejs.ts deleted file mode 100644 index 11f6890d..00000000 --- a/scripts/patch-ejs.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { walk } from "https://deno.land/std@0.224.0/fs/walk.ts"; -import { join } from "https://deno.land/std@0.224.0/path/mod.ts"; - -const EJS_SRC_DIR = join(Deno.cwd(), "ejs/src"); - -async function patchFile(path: string) { - let content = await Deno.readTextFile(path); - let changed = false; - - const replacements = [ - { from: /from ["']meriyah["']/g, to: 'from "npm:meriyah"' }, - { from: /from ["']astring["']/g, to: 'from "npm:astring"' } - ]; - - for (const replacement of replacements) { - if (replacement.from.test(content)) { - content = content.replace(replacement.from, replacement.to); - changed = true; - } - } - - if (changed) { - await Deno.writeTextFile(path, content); - console.log(`Patched ${path}`); - } -} - -console.log(`Starting to patch files in ${EJS_SRC_DIR}...`); - -for await (const entry of walk(EJS_SRC_DIR, { exts: [".ts"] })) { - if (entry.isFile) { - await patchFile(entry.path); - } -} - -console.log("Patching complete."); \ No newline at end of file From a179f23b2164433a8d092333376efcfad99156ed Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 26 Nov 2025 05:31:38 -0500 Subject: [PATCH 09/71] Update imports to use jsr in server.ts --- server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server.ts b/server.ts index baf42ba1..91d1bb39 100644 --- a/server.ts +++ b/server.ts @@ -1,4 +1,4 @@ -import { serve } from "https://deno.land/std@0.224.0/http/server.ts"; +import { serve } from "jsr:@std/http@0.224.0"; import { initializeWorkers } from "./src/workerPool.ts"; import { initializeCache } from "./src/playerCache.ts"; import { handleDecryptSignature } from "./src/handlers/decryptSignature.ts"; @@ -68,4 +68,4 @@ await initializeCache(); initializeWorkers(); console.log(`Server listening on http://${host}:${port}`); -await serve(handler, { port: Number(port), hostname: host }); \ No newline at end of file +await serve(handler, { port: Number(port), hostname: host }); From d7792a4f10012137ce090fe0a962f2069c228685 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 26 Nov 2025 06:41:08 -0500 Subject: [PATCH 10/71] Use a later version of std/http --- server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.ts b/server.ts index 91d1bb39..0d95fec0 100644 --- a/server.ts +++ b/server.ts @@ -1,4 +1,4 @@ -import { serve } from "jsr:@std/http@0.224.0"; +import { serve } from "jsr:@std/http@0.224.5"; import { initializeWorkers } from "./src/workerPool.ts"; import { initializeCache } from "./src/playerCache.ts"; import { handleDecryptSignature } from "./src/handlers/decryptSignature.ts"; From cb5398f1b2e14a7883b5e671fa6976722ceff86a Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 26 Nov 2025 06:48:47 -0500 Subject: [PATCH 11/71] Add imports for src/playerCache.ts --- deno.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/deno.json b/deno.json index 5fd5a312..7416ae14 100644 --- a/deno.json +++ b/deno.json @@ -3,6 +3,9 @@ "lib": [ "deno.worker" ] }, "imports": { + "@std/crypto": "jsr:@std/crypto@^0.224.0", + "@std/fs": "jsr:@std/fs@^0.224.0", + "@std/path": "jsr:@std/path@^0.224.0", "astring": "npm:astring@^1.9.0", "meriyah": "npm:meriyah@^6.1.4" } From 3db5cd86de6aa07fe5d8771d166f5fb830086744 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 26 Nov 2025 06:51:13 -0500 Subject: [PATCH 12/71] Use imports from deno.json --- src/playerCache.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/playerCache.ts b/src/playerCache.ts index 75bd2ed3..7391d1ba 100644 --- a/src/playerCache.ts +++ b/src/playerCache.ts @@ -1,6 +1,6 @@ -import { crypto } from "jsr:@std/crypto@0.224.0"; -import { ensureDir } from "jsr:@std/fs@0.224.0"; -import { join } from "jsr:@std/path@0.224.0"; +import { crypto } from "@std/crypto"; +import { ensureDir } from "@std/fs"; +import { join } from "@std/path"; import { cacheSize, playerScriptFetches } from "./metrics.ts"; export const CACHE_DIR = join(Deno.cwd(), 'player_cache'); From 19b3cb0c308d5cbcaf27d6594b36271346fc730b Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 26 Nov 2025 07:02:43 -0500 Subject: [PATCH 13/71] Create deno.lock --- deno.lock | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 deno.lock diff --git a/deno.lock b/deno.lock new file mode 100644 index 00000000..995c6e6a --- /dev/null +++ b/deno.lock @@ -0,0 +1,105 @@ +{ + "version": "5", + "specifiers": { + "jsr:@std/assert@0.224": "0.224.0", + "jsr:@std/async@^1.0.0-rc.1": "1.0.15", + "jsr:@std/cli@~0.224.7": "0.224.7", + "jsr:@std/crypto@0.224": "0.224.0", + "jsr:@std/encoding@0.224": "0.224.3", + "jsr:@std/encoding@1.0.0-rc.2": "1.0.0-rc.2", + "jsr:@std/fmt@~0.225.4": "0.225.6", + "jsr:@std/fs@0.224": "0.224.0", + "jsr:@std/http@~0.224.5": "0.224.5", + "jsr:@std/media-types@^1.0.0-rc.1": "1.1.0", + "jsr:@std/net@~0.224.3": "0.224.5", + "jsr:@std/path@0.224": "0.224.0", + "jsr:@std/path@1.0.0-rc.2": "1.0.0-rc.2", + "jsr:@std/streams@~0.224.5": "0.224.5", + "npm:astring@^1.9.0": "1.9.0", + "npm:meriyah@^6.1.4": "6.1.4" + }, + "jsr": { + "@std/assert@0.224.0": { + "integrity": "8643233ec7aec38a940a8264a6e3eed9bfa44e7a71cc6b3c8874213ff401967f" + }, + "@std/async@1.0.15": { + "integrity": "55d1d9d04f99403fe5730ab16bdcc3c47f658a6bf054cafb38a50f046238116e" + }, + "@std/cli@0.224.7": { + "integrity": "654ca6477518e5e3a0d3fabafb2789e92b8c0febf1a1d24ba4b567aba94b5977" + }, + "@std/crypto@0.224.0": { + "integrity": "154ef3ff08ef535562ef1a718718c5b2c5fc3808f0f9100daad69e829bfcdf2d", + "dependencies": [ + "jsr:@std/assert", + "jsr:@std/encoding@0.224" + ] + }, + "@std/encoding@0.224.3": { + "integrity": "5e861b6d81be5359fad4155e591acf17c0207b595112d1840998bb9f476dbdaf" + }, + "@std/encoding@1.0.0-rc.2": { + "integrity": "160d7674a20ebfbccdf610b3801fee91cf6e42d1c106dd46bbaf46e395cd35ef" + }, + "@std/fmt@0.225.6": { + "integrity": "aba6aea27f66813cecfd9484e074a9e9845782ab0685c030e453a8a70b37afc8" + }, + "@std/fs@0.224.0": { + "integrity": "52a5ec89731ac0ca8f971079339286f88c571a4d61686acf75833f03a89d8e69", + "dependencies": [ + "jsr:@std/assert", + "jsr:@std/path@0.224" + ] + }, + "@std/http@0.224.5": { + "integrity": "b03b5d1529f6c423badfb82f6640f9f2557b4034cd7c30655ba5bb447ff750a4", + "dependencies": [ + "jsr:@std/async", + "jsr:@std/cli", + "jsr:@std/encoding@1.0.0-rc.2", + "jsr:@std/fmt", + "jsr:@std/media-types", + "jsr:@std/net", + "jsr:@std/path@1.0.0-rc.2", + "jsr:@std/streams" + ] + }, + "@std/media-types@1.1.0": { + "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" + }, + "@std/net@0.224.5": { + "integrity": "9c2ae90a5c3dc7771da5ae5e13b6f7d5d0b316c1954c5d53f2bfc1129fb757ff" + }, + "@std/path@0.224.0": { + "integrity": "55bca6361e5a6d158b9380e82d4981d82d338ec587de02951e2b7c3a24910ee6", + "dependencies": [ + "jsr:@std/assert" + ] + }, + "@std/path@1.0.0-rc.2": { + "integrity": "39f20d37a44d1867abac8d91c169359ea6e942237a45a99ee1e091b32b921c7d" + }, + "@std/streams@0.224.5": { + "integrity": "bcde7818dd5460d474cdbd674b15f6638b9cd73cd64e52bd852fba2bd4d8ec91" + } + }, + "npm": { + "astring@1.9.0": { + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "bin": true + }, + "meriyah@6.1.4": { + "integrity": "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==" + } + }, + "workspace": { + "dependencies": [ + "jsr:@std/crypto@0.224", + "jsr:@std/fs@0.224", + "jsr:@std/http@~0.224.5", + "jsr:@std/path@0.224", + "npm:astring@^1.9.0", + "npm:meriyah@^6.1.4" + ] + } +} From 0d20392e291a7b761616c43b99d32c9bc4047619 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 26 Nov 2025 15:32:29 -0500 Subject: [PATCH 14/71] Update Dockerfile - Specify Debian for builder - Use `tini` for proper signals - Create appropriate cache directories --- Dockerfile | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9cac9518..10d15b86 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,11 @@ -FROM denoland/deno:latest AS builder +FROM denoland/deno:debian AS builder WORKDIR /usr/src/app -RUN apt-get update && apt-get install -y git npm +RUN install -v -d -m 1777 /cache && \ + install -v -d -o deno -g deno player_cache + +RUN apt-get update && apt-get install -y git COPY . . @@ -14,17 +17,17 @@ RUN deno compile \ --include worker.ts \ server.ts -RUN mkdir -p /usr/src/app/player_cache && \ - chown -R deno:deno /usr/src/app/player_cache - FROM gcr.io/distroless/cc-debian12 WORKDIR /app +COPY --from=builder /tini /tini COPY --from=builder /usr/src/app/server /app/server +COPY --from=builder /cache /cache COPY --from=builder --chown=nonroot:nonroot /usr/src/app/player_cache /app/player_cache +COPY --from=builder --chown=nonroot:nonroot /usr/src/app/player_cache /home/nonroot/.cache USER nonroot EXPOSE 8001 -ENTRYPOINT ["/app/server"] +ENTRYPOINT ["/tini", "--", "/app/server"] From aa23c49085b045b382f2707490ed4c3ba74176d1 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 26 Nov 2025 17:32:06 -0500 Subject: [PATCH 15/71] Handle HOME returning undefined --- src/playerCache.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/playerCache.ts b/src/playerCache.ts index ad7341e2..d4c32db2 100644 --- a/src/playerCache.ts +++ b/src/playerCache.ts @@ -3,8 +3,13 @@ import { ensureDir } from "@std/fs"; import { join } from "@std/path"; import { cacheSize, playerScriptFetches } from "./metrics.ts"; -export const CACHE_HOME = Deno.env.get("XDG_CACHE_HOME") || join(Deno.env.get("HOME"), '.cache'); -export const CACHE_DIR = join(CACHE_HOME, 'yt-cipher', 'player_cache'); +let cache_prefix = Deno.cwd(); +const HOME = Deno.env.get("HOME"); +const CACHE_HOME = Deno.env.get("XDG_CACHE_HOME"); +if ( HOME !== undefined ) { + cache_prefix = join(CACHE_HOME || join(HOME, '.cache'), 'yt-cipher'); +} +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 # From 23d783d26f38a6084c02c843adcc521cb7cb3e92 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 26 Nov 2025 18:01:42 -0500 Subject: [PATCH 16/71] Set the owner with COPY --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 10d15b86..ff065e56 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,7 @@ WORKDIR /app COPY --from=builder /tini /tini COPY --from=builder /usr/src/app/server /app/server -COPY --from=builder /cache /cache +COPY --from=builder --chown=nonroot:nonroot /cache /cache COPY --from=builder --chown=nonroot:nonroot /usr/src/app/player_cache /app/player_cache COPY --from=builder --chown=nonroot:nonroot /usr/src/app/player_cache /home/nonroot/.cache From 841fc81756be251e7ff7b5de211f7b3b544feedb Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 26 Nov 2025 22:17:57 -0500 Subject: [PATCH 17/71] Re-work cache_prefix logic --- src/playerCache.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/playerCache.ts b/src/playerCache.ts index d4c32db2..b1fd3c47 100644 --- a/src/playerCache.ts +++ b/src/playerCache.ts @@ -5,9 +5,12 @@ 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'); +} const CACHE_HOME = Deno.env.get("XDG_CACHE_HOME"); -if ( HOME !== undefined ) { - cache_prefix = join(CACHE_HOME || join(HOME, '.cache'), 'yt-cipher'); +if ( CACHE_HOME ) { + cache_prefix = join(CACHE_HOME, 'yt-cipher'); } export const CACHE_DIR = join(cache_prefix, 'player_cache'); From fb889f849fa93edcb79aa91742b77efd27cc94f4 Mon Sep 17 00:00:00 2001 From: tcely Date: Thu, 27 Nov 2025 01:53:06 -0500 Subject: [PATCH 18/71] Switch to the debian13 image All the other images used are on 13 now anyway. Also, use the debug tag so that busybox is included. We can use that to run shell commands to avoid issues with COPY. --- Dockerfile | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index ff065e56..6b1de72e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,14 +2,11 @@ FROM denoland/deno:debian AS builder WORKDIR /usr/src/app -RUN install -v -d -m 1777 /cache && \ - install -v -d -o deno -g deno player_cache - -RUN apt-get update && apt-get install -y git - COPY . . -RUN git submodule update --init --recursive +# needs --build-arg BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 when using a URL +#RUN apt-get update && apt-get install -y git && \ +# git submodule update --init --recursive RUN deno compile \ --output server \ @@ -17,17 +14,17 @@ RUN deno compile \ --include worker.ts \ server.ts -FROM gcr.io/distroless/cc-debian12 +FROM gcr.io/distroless/cc-debian13:debug +SHELL ["/busybox/busybox", "sh", "-c"] WORKDIR /app COPY --from=builder /tini /tini COPY --from=builder /usr/src/app/server /app/server -COPY --from=builder --chown=nonroot:nonroot /cache /cache -COPY --from=builder --chown=nonroot:nonroot /usr/src/app/player_cache /app/player_cache -COPY --from=builder --chown=nonroot:nonroot /usr/src/app/player_cache /home/nonroot/.cache +RUN install -v -d -m 1777 /cache && \ + install -v -d -o nonroot -g nonroot -m 750 /app/player_cache /home/nonroot/.cache -USER nonroot EXPOSE 8001 -ENTRYPOINT ["/tini", "--", "/app/server"] +ENTRYPOINT ["/tini", "--"] +CMD ["/busybox/busybox", "su", "-s", "/app/server", "nonroot"] From a21e6af2885b9e4c6703243e3da504aba2b44230 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 17:16:34 -0500 Subject: [PATCH 19/71] Adjust Dockerfile based on review --- Dockerfile | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6b1de72e..29d1c8fa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,11 @@ +ARG XDG_CACHE_HOME + FROM denoland/deno:debian AS builder WORKDIR /usr/src/app COPY . . -# needs --build-arg BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 when using a URL -#RUN apt-get update && apt-get install -y git && \ -# git submodule update --init --recursive - RUN deno compile \ --output server \ --allow-net --allow-read --allow-write --allow-env \ @@ -22,9 +20,16 @@ WORKDIR /app COPY --from=builder /tini /tini COPY --from=builder /usr/src/app/server /app/server -RUN install -v -d -m 1777 /cache && \ - install -v -d -o nonroot -g nonroot -m 750 /app/player_cache /home/nonroot/.cache +ARG XDG_CACHE_HOME +ENV XDG_CACHE_HOME="${XDG_CACHE_HOME}" +# Create the fall-back cache directories +RUN install -v -d -o nonroot -g nonroot -m 750 \ + /app/player_cache /home/nonroot/.cache && \ + test -z "${XDG_CACHE_HOME}" || install -v -d -m 1777 "${XDG_CACHE_HOME}" EXPOSE 8001 +USER nonroot ENTRYPOINT ["/tini", "--"] -CMD ["/busybox/busybox", "su", "-s", "/app/server", "nonroot"] +# Run the server as nonroot even when /tini runs as root +# CMD ["/busybox/busybox", "su", "-s", "/app/server", "nonroot"] +CMD ["/app/server"] From 2aa321a0df4ff9b1c414474f7c0d4580ecd76a39 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 19:08:11 -0500 Subject: [PATCH 20/71] Create deno.yml --- .github/workflows/deno.yml | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/deno.yml diff --git a/.github/workflows/deno.yml b/.github/workflows/deno.yml new file mode 100644 index 00000000..b3e35a83 --- /dev/null +++ b/.github/workflows/deno.yml @@ -0,0 +1,41 @@ +# 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` and `deno lint`. +# For more information see: https://github.com/denoland/setup-deno + +name: Deno + +on: + push: + branches: ["master"] + pull_request: + branches: ["master"] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Setup repo + uses: actions/checkout@v6 + + - name: Setup Deno + uses: denoland/setup-deno@v2 + with: + deno-version: latest + cache: true + + - name: Verify formatting + run: deno fmt --check --indent-width=4 src *.ts + + - name: Run linter + run: deno lint src *.ts + + # - name: Run tests + # run: deno test -A From ec51e356c59addec16fd5c21180cddb24f09b173 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 18:12:21 -0500 Subject: [PATCH 21/71] deno fmt: worker.ts --- worker.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/worker.ts b/worker.ts index 7448192c..748a7c8d 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 +}; From 06a517238c10eaa40582012ee6cc291a7ef0a6b2 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 18:12:42 -0500 Subject: [PATCH 22/71] deno fmt: server.ts --- server.ts | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/server.ts b/server.ts index baf42ba1..713a7ecb 100644 --- a/server.ts +++ b/server.ts @@ -15,13 +15,16 @@ async function baseHandler(req: Request): Promise { const { pathname } = new URL(req.url); if (req.method === "GET" && pathname === "/") { - return new Response("There is no endpoint here, you can read the API spec at https://github.com/kikkia/yt-cipher?tab=readme-ov-file#api-specification. If you are using yt-source/lavalink, use this url for your remote cipher url", { - status: 200, - headers: { "Content-Type": "text/plain" }, - }); + return new Response( + "There is no endpoint here, you can read the API spec at https://github.com/kikkia/yt-cipher?tab=readme-ov-file#api-specification. If you are using yt-source/lavalink, use this url for your remote cipher url", + { + status: 200, + headers: { "Content-Type": "text/plain" }, + }, + ); } - if (pathname === '/metrics') { + if (pathname === "/metrics") { return new Response(registry.metrics(), { headers: { "Content-Type": "text/plain" }, }); @@ -30,28 +33,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 }; @@ -62,10 +76,10 @@ 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 host = Deno.env.get("HOST") || "0.0.0.0"; await initializeCache(); initializeWorkers(); console.log(`Server listening on http://${host}:${port}`); -await serve(handler, { port: Number(port), hostname: host }); \ No newline at end of file +await serve(handler, { port: Number(port), hostname: host }); From 2b2f20f35cdb2b42bf45454a4e95993abc61e5ca Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 18:14:15 -0500 Subject: [PATCH 23/71] deno fmt: src/instrumentedCache.ts --- src/instrumentedCache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 +} From a4fdb1e9c1ea11f6a4d6d6672f47c3961a60c794 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 18:14:47 -0500 Subject: [PATCH 24/71] deno fmt: src/metrics.ts --- src/metrics.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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 +}); From a9f3b158fc2e2871991849b922b5c15a76924593 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 18:15:07 -0500 Subject: [PATCH 25/71] deno fmt: src/middleware.ts --- src/middleware.ts | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) 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; }; From e81aacbaa2aa02fa2ddc350c741ea5bf11faaa4f Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 18:15:25 -0500 Subject: [PATCH 26/71] deno fmt: src/playerCache.ts --- src/playerCache.ts | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/playerCache.ts b/src/playerCache.ts index 1f38c695..24fdf58c 100644 --- a/src/playerCache.ts +++ b/src/playerCache.ts @@ -3,15 +3,20 @@ import { ensureDir } from "https://deno.land/std@0.224.0/fs/ensure_dir.ts"; import { join } from "https://deno.land/std@0.224.0/path/mod.ts"; import { cacheSize, playerScriptFetches } from "./metrics.ts"; -export const CACHE_HOME = Deno.env.get("XDG_CACHE_HOME") || join(Deno.env.get("HOME"), '.cache'); -export const CACHE_DIR = join(CACHE_HOME, 'yt-cipher', 'player_cache'); +export const CACHE_HOME = Deno.env.get("XDG_CACHE_HOME") || join(Deno.env.get("HOME"), ".cache"); +export const CACHE_DIR = join(CACHE_HOME, "yt-cipher", "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 { @@ -23,9 +28,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); @@ -35,8 +45,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; } @@ -55,7 +65,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); @@ -64,6 +75,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}`); } From 7013cc699ff8e1a769b79185a8fe76c51678dd2f Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 18:15:48 -0500 Subject: [PATCH 27/71] deno fmt: src/preprocessedCache.ts --- src/preprocessedCache.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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, +); From 60a4dd8c167a5de469e6ff299a4e90d8637ff33d Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 18:16:21 -0500 Subject: [PATCH 28/71] deno fmt: src/solver.ts --- src/solver.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/solver.ts b/src/solver.ts index a87e99c7..bbeeaa88 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 +} From bf199f4f738fc0accc1f0a5c43130ba4f0426282 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 18:16:38 -0500 Subject: [PATCH 29/71] deno fmt: src/solverCache.ts --- src/solverCache.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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); From f3980ec299feb567c718171fed0ae58a8cbaf5eb Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 18:17:01 -0500 Subject: [PATCH 30/71] deno fmt: src/stsCache.ts --- src/stsCache.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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); From 59b41e7f90517091963fa38d810d0503b058cad1 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 18:17:24 -0500 Subject: [PATCH 31/71] deno fmt: src/types.ts --- src/types.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/types.ts b/src/types.ts index 1f8c5e93..0d9771d2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,7 @@ -import type { Input as MainInput, Output as MainOutput } from "../ejs/src/yt/solver/main.ts"; +import type { + Input as MainInput, + Output as MainOutput, +} from "../ejs/src/yt/solver/main.ts"; export interface Solvers { n: ((val: string) => string) | null; @@ -49,8 +52,8 @@ export interface Task { 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 +} From e6633d91fdda186d0314b4f534935e2dabf17ef7 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 18:18:02 -0500 Subject: [PATCH 32/71] deno fmt: src/utils.ts --- src/utils.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 43405f1c..1a4db9fc 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}`); } @@ -25,8 +25,8 @@ 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]; } @@ -37,5 +37,5 @@ export function extractPlayerId(playerUrl: string): string { return match[1]; } } - return 'unknown'; -} \ No newline at end of file + return "unknown"; +} From 52bbc1ed77e14017ecc113c12025ed715c5611c1 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 18:18:17 -0500 Subject: [PATCH 33/71] deno fmt: src/validation.ts --- src/validation.ts | 53 +++++++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/src/validation.ts b/src/validation.ts index 097f0a59..bebf74e7 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -7,19 +7,22 @@ type ValidationSchema = { }; 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( + obj: any, + schema: ValidationSchema, +): { isValid: boolean; errors: string[] } { const errors: string[] = []; for (const key in schema) { if (!obj.hasOwnProperty(key) || !schema[key](obj[key])) { @@ -34,38 +37,48 @@ export function withValidation(handler: Next): Next { 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 +} From 2b2d80a0bc2687d9be5527cb2590fef9f0e8cd4f Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 18:18:41 -0500 Subject: [PATCH 34/71] deno fmt: src/workerPool.ts --- src/workerPool.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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 +} From b520d94974e93eab8c11993e21e062e8f666d6fb Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 18:19:21 -0500 Subject: [PATCH 35/71] deno fmt: src/handlers/decryptSignature.ts --- src/handlers/decryptSignature.ts | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) 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 Date: Fri, 28 Nov 2025 18:19:37 -0500 Subject: [PATCH 36/71] deno fmt: src/handlers/getSts.ts --- src/handlers/getSts.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/handlers/getSts.ts b/src/handlers/getSts.ts index ef48d461..fb5994af 100644 --- a/src/handlers/getSts.ts +++ b/src/handlers/getSts.ts @@ -11,7 +11,10 @@ export async function handleGetSts(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 +} From 68733848dde9785cc52acc5a4f89739e9bf3fa6d Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 18:19:51 -0500 Subject: [PATCH 37/71] deno fmt: src/handlers/resolveUrl.ts --- src/handlers/resolveUrl.ts | 52 +++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 9 deletions(-) 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" }, + }); +} From 959e82bd78249232fe4d58dbabb0cd8f871cc470 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 18:22:38 -0500 Subject: [PATCH 38/71] deno lint: src/utils.ts --- src/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 1a4db9fc..76b0a227 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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}`); } @@ -30,7 +30,7 @@ export function extractPlayerId(playerUrl: string): string { 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) { From d26aeacbb106053218bba5d2a123eb1b725092fa Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 18:24:14 -0500 Subject: [PATCH 39/71] deno lint: src/types.ts --- src/types.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/types.ts b/src/types.ts index 0d9771d2..9d9f0755 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,3 @@ -import type { - Input as MainInput, - Output as MainOutput, -} from "../ejs/src/yt/solver/main.ts"; - export interface Solvers { n: ((val: string) => string) | null; sig: ((val: string) => string) | null; From 8cb4e9d27170ce4d11d2e6bd76213b91847211f4 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 18:28:44 -0500 Subject: [PATCH 40/71] Always use a string for port --- server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.ts b/server.ts index 713a7ecb..d9671cf2 100644 --- a/server.ts +++ b/server.ts @@ -75,7 +75,7 @@ async function baseHandler(req: Request): Promise { const handler = baseHandler; -const port = Deno.env.get("PORT") || 8001; +const port = Deno.env.get("PORT") || "8001"; const host = Deno.env.get("HOST") || "0.0.0.0"; await initializeCache(); From 4afb39473eceab0059ccb558a853208b9dc81f9f Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 19:48:44 -0500 Subject: [PATCH 41/71] Ignore the `no-explicit-any` lint results --- src/types.ts | 1 + src/validation.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/types.ts b/src/types.ts index 9d9f0755..2919752b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,6 +41,7 @@ export interface WorkerWithStatus extends Worker { export interface Task { data: string; resolve: (output: string) => void; + // deno-lint-ignore no-explicit-any reject: (error: any) => void; } diff --git a/src/validation.ts b/src/validation.ts index bebf74e7..a4bbe240 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -3,6 +3,7 @@ import { validateAndNormalizePlayerUrl } from "./utils.ts"; type Next = (ctx: RequestContext) => Promise; type ValidationSchema = { + // deno-lint-ignore no-explicit-any [key: string]: (value: any) => boolean; }; @@ -20,6 +21,7 @@ const resolveUrlRequestSchema: ValidationSchema = { }; function validateObject( + // deno-lint-ignore no-explicit-any obj: any, schema: ValidationSchema, ): { isValid: boolean; errors: string[] } { From a156b15274585e839d5cafe32600753e9b08cd10 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 20:14:01 -0500 Subject: [PATCH 42/71] Address `no-prototype-builtins` lint error --- src/validation.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/validation.ts b/src/validation.ts index a4bbe240..18014f30 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -27,7 +27,10 @@ function validateObject( ): { 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`); } } From 1a878d9ef7c15d40ea7c96c4c351190bd2f88fc5 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 20:48:34 -0500 Subject: [PATCH 43/71] Add `deno check` to this workflow --- .github/workflows/deno.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deno.yml b/.github/workflows/deno.yml index b3e35a83..78ee303f 100644 --- a/.github/workflows/deno.yml +++ b/.github/workflows/deno.yml @@ -3,7 +3,10 @@ # separate terms of service, privacy policy, and support # documentation. -# This workflow will install Deno then run `deno fmt` and `deno lint`. +# 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 @@ -37,5 +40,8 @@ jobs: - name: Run linter run: deno lint src *.ts + - name: Check types + run: deno check --frozen=true *.ts + # - name: Run tests # run: deno test -A From f2e1627ceb654490d4c3fcab4b6ecee701a8f221 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 20:58:56 -0500 Subject: [PATCH 44/71] Use a submodule for `ejs` --- .github/workflows/deno.yml | 2 ++ .github/workflows/main.yml | 4 +++- .gitignore | 3 +-- .gitmodules | 3 +++ ejs | 1 + 5 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 .gitmodules create mode 160000 ejs diff --git a/.github/workflows/deno.yml b/.github/workflows/deno.yml index 78ee303f..9723635c 100644 --- a/.github/workflows/deno.yml +++ b/.github/workflows/deno.yml @@ -27,6 +27,8 @@ jobs: steps: - name: Setup repo uses: actions/checkout@v6 + with: + submodules: recursive - name: Setup Deno uses: denoland/setup-deno@v2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0b8eed2f..86124530 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,6 +14,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + submodules: recursive - name: Log in to GitHub Container Registry uses: docker/login-action@v3 @@ -41,4 +43,4 @@ jobs: platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file + labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore index defe4625..1d74e219 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -ejs/ -.vscode/ \ No newline at end of file +.vscode/ 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/ejs b/ejs new file mode 160000 index 00000000..2655b1f5 --- /dev/null +++ b/ejs @@ -0,0 +1 @@ +Subproject commit 2655b1f55f98e5870d4e124704a21f4d793b4e1c From 1b05f3efaa855a9f6612fe6e2685edf12bb3ee9e Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 26 Nov 2025 04:24:08 -0500 Subject: [PATCH 45/71] Create deno.json (cherry picked from commit c8609a3b61e64ceeb7950c3719f58de1b6f44674) --- deno.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 deno.json diff --git a/deno.json b/deno.json new file mode 100644 index 00000000..5fd5a312 --- /dev/null +++ b/deno.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "lib": [ "deno.worker" ] + }, + "imports": { + "astring": "npm:astring@^1.9.0", + "meriyah": "npm:meriyah@^6.1.4" + } +} From b29cd42068e7a61dad11af332f2ad81bbe8904d8 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 26 Nov 2025 07:02:43 -0500 Subject: [PATCH 46/71] Create deno.lock (cherry picked from commit 19b3cb0c308d5cbcaf27d6594b36271346fc730b) --- deno.lock | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 deno.lock diff --git a/deno.lock b/deno.lock new file mode 100644 index 00000000..995c6e6a --- /dev/null +++ b/deno.lock @@ -0,0 +1,105 @@ +{ + "version": "5", + "specifiers": { + "jsr:@std/assert@0.224": "0.224.0", + "jsr:@std/async@^1.0.0-rc.1": "1.0.15", + "jsr:@std/cli@~0.224.7": "0.224.7", + "jsr:@std/crypto@0.224": "0.224.0", + "jsr:@std/encoding@0.224": "0.224.3", + "jsr:@std/encoding@1.0.0-rc.2": "1.0.0-rc.2", + "jsr:@std/fmt@~0.225.4": "0.225.6", + "jsr:@std/fs@0.224": "0.224.0", + "jsr:@std/http@~0.224.5": "0.224.5", + "jsr:@std/media-types@^1.0.0-rc.1": "1.1.0", + "jsr:@std/net@~0.224.3": "0.224.5", + "jsr:@std/path@0.224": "0.224.0", + "jsr:@std/path@1.0.0-rc.2": "1.0.0-rc.2", + "jsr:@std/streams@~0.224.5": "0.224.5", + "npm:astring@^1.9.0": "1.9.0", + "npm:meriyah@^6.1.4": "6.1.4" + }, + "jsr": { + "@std/assert@0.224.0": { + "integrity": "8643233ec7aec38a940a8264a6e3eed9bfa44e7a71cc6b3c8874213ff401967f" + }, + "@std/async@1.0.15": { + "integrity": "55d1d9d04f99403fe5730ab16bdcc3c47f658a6bf054cafb38a50f046238116e" + }, + "@std/cli@0.224.7": { + "integrity": "654ca6477518e5e3a0d3fabafb2789e92b8c0febf1a1d24ba4b567aba94b5977" + }, + "@std/crypto@0.224.0": { + "integrity": "154ef3ff08ef535562ef1a718718c5b2c5fc3808f0f9100daad69e829bfcdf2d", + "dependencies": [ + "jsr:@std/assert", + "jsr:@std/encoding@0.224" + ] + }, + "@std/encoding@0.224.3": { + "integrity": "5e861b6d81be5359fad4155e591acf17c0207b595112d1840998bb9f476dbdaf" + }, + "@std/encoding@1.0.0-rc.2": { + "integrity": "160d7674a20ebfbccdf610b3801fee91cf6e42d1c106dd46bbaf46e395cd35ef" + }, + "@std/fmt@0.225.6": { + "integrity": "aba6aea27f66813cecfd9484e074a9e9845782ab0685c030e453a8a70b37afc8" + }, + "@std/fs@0.224.0": { + "integrity": "52a5ec89731ac0ca8f971079339286f88c571a4d61686acf75833f03a89d8e69", + "dependencies": [ + "jsr:@std/assert", + "jsr:@std/path@0.224" + ] + }, + "@std/http@0.224.5": { + "integrity": "b03b5d1529f6c423badfb82f6640f9f2557b4034cd7c30655ba5bb447ff750a4", + "dependencies": [ + "jsr:@std/async", + "jsr:@std/cli", + "jsr:@std/encoding@1.0.0-rc.2", + "jsr:@std/fmt", + "jsr:@std/media-types", + "jsr:@std/net", + "jsr:@std/path@1.0.0-rc.2", + "jsr:@std/streams" + ] + }, + "@std/media-types@1.1.0": { + "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" + }, + "@std/net@0.224.5": { + "integrity": "9c2ae90a5c3dc7771da5ae5e13b6f7d5d0b316c1954c5d53f2bfc1129fb757ff" + }, + "@std/path@0.224.0": { + "integrity": "55bca6361e5a6d158b9380e82d4981d82d338ec587de02951e2b7c3a24910ee6", + "dependencies": [ + "jsr:@std/assert" + ] + }, + "@std/path@1.0.0-rc.2": { + "integrity": "39f20d37a44d1867abac8d91c169359ea6e942237a45a99ee1e091b32b921c7d" + }, + "@std/streams@0.224.5": { + "integrity": "bcde7818dd5460d474cdbd674b15f6638b9cd73cd64e52bd852fba2bd4d8ec91" + } + }, + "npm": { + "astring@1.9.0": { + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "bin": true + }, + "meriyah@6.1.4": { + "integrity": "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==" + } + }, + "workspace": { + "dependencies": [ + "jsr:@std/crypto@0.224", + "jsr:@std/fs@0.224", + "jsr:@std/http@~0.224.5", + "jsr:@std/path@0.224", + "npm:astring@^1.9.0", + "npm:meriyah@^6.1.4" + ] + } +} From 0772eeef94f33af37b7f9ad0327a63866e9ed1c3 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 21:20:55 -0500 Subject: [PATCH 47/71] Disable frozen flag for now --- .github/workflows/deno.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deno.yml b/.github/workflows/deno.yml index 9723635c..8509d33f 100644 --- a/.github/workflows/deno.yml +++ b/.github/workflows/deno.yml @@ -43,7 +43,7 @@ jobs: run: deno lint src *.ts - name: Check types - run: deno check --frozen=true *.ts + run: deno check --frozen=false *.ts # - name: Run tests # run: deno test -A From f14d3c29c131f998a98e128f9d65c73e507489f0 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 21:13:53 -0500 Subject: [PATCH 48/71] Exclude `no-import-prefix` lint rule for now --- deno.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/deno.json b/deno.json index 5fd5a312..b82e1c2c 100644 --- a/deno.json +++ b/deno.json @@ -5,5 +5,10 @@ "imports": { "astring": "npm:astring@^1.9.0", "meriyah": "npm:meriyah@^6.1.4" + }, + "lint": { + "rules": { + "exclude": [ "no-import-prefix" ] + } } } From 948aaed8fb7b991b9cf188b245556107d8f1ad50 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 21:32:39 -0500 Subject: [PATCH 49/71] Handle HOME returning undefined (cherry picked from commit aa23c49085b045b382f2707490ed4c3ba74176d1) --- src/playerCache.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/playerCache.ts b/src/playerCache.ts index 24fdf58c..b90a1746 100644 --- a/src/playerCache.ts +++ b/src/playerCache.ts @@ -3,8 +3,13 @@ import { ensureDir } from "https://deno.land/std@0.224.0/fs/ensure_dir.ts"; import { join } from "https://deno.land/std@0.224.0/path/mod.ts"; import { cacheSize, playerScriptFetches } from "./metrics.ts"; -export const CACHE_HOME = Deno.env.get("XDG_CACHE_HOME") || join(Deno.env.get("HOME"), ".cache"); -export const CACHE_DIR = join(CACHE_HOME, "yt-cipher", "player_cache"); +let cache_prefix = Deno.cwd(); +const HOME = Deno.env.get("HOME"); +const CACHE_HOME = Deno.env.get("XDG_CACHE_HOME"); +if (HOME !== undefined) { + cache_prefix = join(CACHE_HOME || join(HOME, ".cache"), "yt-cipher"); +} +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 # From ee3994ef8e7bb0c4c52afb49df46b6faae884ca3 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 22:12:23 -0500 Subject: [PATCH 50/71] Ignore the `require-await` lint rule --- src/validation.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/validation.ts b/src/validation.ts index 18014f30..934ba4b0 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -38,6 +38,7 @@ function validateObject( } export function withValidation(handler: Next): Next { + // deno-lint-ignore require-await return async (ctx: RequestContext) => { const { pathname } = new URL(ctx.req.url); From daf7a5e644f62d45b12f2c8286055943d858b657 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 23:34:53 -0500 Subject: [PATCH 51/71] Use configuration for `deno fmt` --- .github/workflows/deno.yml | 2 +- deno.json | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deno.yml b/.github/workflows/deno.yml index 8509d33f..70726daa 100644 --- a/.github/workflows/deno.yml +++ b/.github/workflows/deno.yml @@ -37,7 +37,7 @@ jobs: cache: true - name: Verify formatting - run: deno fmt --check --indent-width=4 src *.ts + run: deno fmt --check - name: Run linter run: deno lint src *.ts diff --git a/deno.json b/deno.json index b82e1c2c..3fa5e450 100644 --- a/deno.json +++ b/deno.json @@ -2,6 +2,10 @@ "compilerOptions": { "lib": [ "deno.worker" ] }, + "fmt": { + "include": ["src/", "*.ts"], + "indentWidth": 4 + }, "imports": { "astring": "npm:astring@^1.9.0", "meriyah": "npm:meriyah@^6.1.4" From 741210594982a037cf8883634ae7c056659c24a8 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 28 Nov 2025 23:35:46 -0500 Subject: [PATCH 52/71] Use configuration for `deno lint` --- .github/workflows/deno.yml | 2 +- deno.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deno.yml b/.github/workflows/deno.yml index 70726daa..9b1dee8e 100644 --- a/.github/workflows/deno.yml +++ b/.github/workflows/deno.yml @@ -40,7 +40,7 @@ jobs: run: deno fmt --check - name: Run linter - run: deno lint src *.ts + run: deno lint - name: Check types run: deno check --frozen=false *.ts diff --git a/deno.json b/deno.json index 3fa5e450..06afd778 100644 --- a/deno.json +++ b/deno.json @@ -11,6 +11,7 @@ "meriyah": "npm:meriyah@^6.1.4" }, "lint": { + "include": ["src/", "*.ts"], "rules": { "exclude": [ "no-import-prefix" ] } From cea36e28e5e5a3301beb150c3b82692e49faf07c Mon Sep 17 00:00:00 2001 From: PadowYT2 Date: Mon, 1 Dec 2025 22:57:10 +0300 Subject: [PATCH 53/71] update lock --- deno.lock | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/deno.lock b/deno.lock index 995c6e6a..dc8f8dc3 100644 --- a/deno.lock +++ b/deno.lock @@ -9,7 +9,7 @@ "jsr:@std/encoding@1.0.0-rc.2": "1.0.0-rc.2", "jsr:@std/fmt@~0.225.4": "0.225.6", "jsr:@std/fs@0.224": "0.224.0", - "jsr:@std/http@~0.224.5": "0.224.5", + "jsr:@std/http@0.224.5": "0.224.5", "jsr:@std/media-types@^1.0.0-rc.1": "1.1.0", "jsr:@std/net@~0.224.3": "0.224.5", "jsr:@std/path@0.224": "0.224.0", @@ -92,11 +92,24 @@ "integrity": "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==" } }, + "redirects": { + "https://deno.land/x/ts_prometheus/mod.ts": "https://deno.land/x/ts_prometheus@v0.3.0/mod.ts" + }, + "remote": { + "https://deno.land/x/lru@1.0.2/mod.ts": "1d44b87c4d40ff33749ae5fd85fe234344e0dace835fdfeb48413edea9461159", + "https://deno.land/x/ts_prometheus@v0.3.0/collector.ts": "12305e262e60de3b9b2db22670f95e388b6f4c0ff8da0fdbaf4cd4758bb5b1b6", + "https://deno.land/x/ts_prometheus@v0.3.0/counter.ts": "c6a03fc6ceb732a70728e2633a6781f607615c5ce5e6ee46fdff34b37bde0ef5", + "https://deno.land/x/ts_prometheus@v0.3.0/gauge.ts": "d2d3b79df3fae07652ee3b72c118cf6e96c834c82c81eb2ebf52e8f136b24fa5", + "https://deno.land/x/ts_prometheus@v0.3.0/histogram.ts": "7585024285ef52b29054adc02d480f5ac4595e0037a8ad5bd1d75ddc205a3fd0", + "https://deno.land/x/ts_prometheus@v0.3.0/metric.ts": "c7635f8b4ec92742e01244712bb8264c32ce91c03f3a8332b9119926dab027cb", + "https://deno.land/x/ts_prometheus@v0.3.0/mod.ts": "9fef6a6c301da262dfa38d111cb4011e96d0c6b1c9f1a233526506c8ce8723bb", + "https://deno.land/x/ts_prometheus@v0.3.0/registry.ts": "b7d4b4b6e008d7ffb8b4acff6a1a56b27463bfb4ecec4f0455562187f7941891", + "https://deno.land/x/ts_prometheus@v0.3.0/summary.ts": "d1ef0341e265fa8d2a2bc42e8d732428d8aadbfa0f60ce4c2baa0486910590df", + }, "workspace": { "dependencies": [ "jsr:@std/crypto@0.224", "jsr:@std/fs@0.224", - "jsr:@std/http@~0.224.5", "jsr:@std/path@0.224", "npm:astring@^1.9.0", "npm:meriyah@^6.1.4" From 646fc4bfd1a52f27ff20054e0ced75f8742d1a16 Mon Sep 17 00:00:00 2001 From: PadowYT2 Date: Mon, 1 Dec 2025 22:57:49 +0300 Subject: [PATCH 54/71] use esm.sh instead of a submodule for ejs --- .gitmodules | 3 --- deno.json | 2 ++ deno.lock | 4 ++++ ejs | 1 - src/solver.ts | 2 +- src/types.ts | 2 -- worker.ts | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) delete mode 100644 .gitmodules delete mode 160000 ejs diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index da970314..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "ejs"] - path = ejs - url = https://github.com/yt-dlp/ejs.git diff --git a/deno.json b/deno.json index 7416ae14..fefbef81 100644 --- a/deno.json +++ b/deno.json @@ -7,6 +7,8 @@ "@std/fs": "jsr:@std/fs@^0.224.0", "@std/path": "jsr:@std/path@^0.224.0", "astring": "npm:astring@^1.9.0", + "ejs": "https://esm.sh/gh/yt-dlp/ejs@0.3.0?standalone", + "ejs/": "https://esm.sh/gh/yt-dlp/ejs@0.3.0&standalone/", "meriyah": "npm:meriyah@^6.1.4" } } diff --git a/deno.lock b/deno.lock index dc8f8dc3..15b4b5c1 100644 --- a/deno.lock +++ b/deno.lock @@ -105,6 +105,10 @@ "https://deno.land/x/ts_prometheus@v0.3.0/mod.ts": "9fef6a6c301da262dfa38d111cb4011e96d0c6b1c9f1a233526506c8ce8723bb", "https://deno.land/x/ts_prometheus@v0.3.0/registry.ts": "b7d4b4b6e008d7ffb8b4acff6a1a56b27463bfb4ecec4f0455562187f7941891", "https://deno.land/x/ts_prometheus@v0.3.0/summary.ts": "d1ef0341e265fa8d2a2bc42e8d732428d8aadbfa0f60ce4c2baa0486910590df", + "https://esm.sh/gh/yt-dlp/ejs@0.3.0&standalone/src/yt/solver/main.ts": "8b2cdc4da906b66f79df57d9cc715badfe92a793faa59841e6ccb641c604ea55", + "https://esm.sh/gh/yt-dlp/ejs@0.3.0&standalone/src/yt/solver/solvers.ts": "269a7e74a2f021621402fbee5872168564196151df892df8fb17229b593dd0b3", + "https://esm.sh/gh/yt-dlp/ejs@0.3.0/denonext/src/yt/solver/main.ts.bundle.mjs": "1dcc16eb6921cac07e440b991c74fb928f49a22554bd422df3aa8ddd09c46e26", + "https://esm.sh/gh/yt-dlp/ejs@0.3.0/denonext/src/yt/solver/solvers.ts.bundle.mjs": "88c7e36c180c2f9416fd26ba9a7db344d8008a7f1069e5d7a91c0bdde44a5656" }, "workspace": { "dependencies": [ diff --git a/ejs b/ejs deleted file mode 160000 index 2655b1f5..00000000 --- a/ejs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2655b1f55f98e5870d4e124704a21f4d793b4e1c diff --git a/src/solver.ts b/src/solver.ts index a87e99c7..dc617fbb 100644 --- a/src/solver.ts +++ b/src/solver.ts @@ -2,7 +2,7 @@ import { execInPool } from "./workerPool.ts"; import { getPlayerFilePath } from "./playerCache.ts"; import { preprocessedCache } from "./preprocessedCache.ts"; import { solverCache } from "./solverCache.ts"; -import { getFromPrepared } from "../ejs/src/yt/solver/solvers.ts"; +import { getFromPrepared } from "ejs/src/yt/solver/solvers.ts"; import type { Solvers } from "./types.ts"; import { workerErrors } from "./metrics.ts"; import { extractPlayerId } from "./utils.ts"; diff --git a/src/types.ts b/src/types.ts index 1f8c5e93..c82a275c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,3 @@ -import type { Input as MainInput, Output as MainOutput } from "../ejs/src/yt/solver/main.ts"; - export interface Solvers { n: ((val: string) => string) | null; sig: ((val: string) => string) | null; diff --git a/worker.ts b/worker.ts index 7448192c..6a35aae2 100644 --- a/worker.ts +++ b/worker.ts @@ -1,4 +1,4 @@ -import { preprocessPlayer } from "./ejs/src/yt/solver/solvers.ts"; +import { preprocessPlayer } from "ejs/src/yt/solver/solvers.ts"; self.onmessage = (e: MessageEvent) => { try { From aa90b1042d8f5accc4454487662272070a430e45 Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 1 Dec 2025 15:34:52 -0500 Subject: [PATCH 55/71] Clean up imports from the ejs submodule --- deno.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/deno.json b/deno.json index fefbef81..14e7e38e 100644 --- a/deno.json +++ b/deno.json @@ -5,10 +5,8 @@ "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", - "astring": "npm:astring@^1.9.0", - "ejs": "https://esm.sh/gh/yt-dlp/ejs@0.3.0?standalone", - "ejs/": "https://esm.sh/gh/yt-dlp/ejs@0.3.0&standalone/", - "meriyah": "npm:meriyah@^6.1.4" + "ejs/": "https://esm.sh/gh/yt-dlp/ejs@0.3.0&standalone/" } } From 99b900247bc9838e74409bab45e98dcb7a1bf251 Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 1 Dec 2025 15:36:26 -0500 Subject: [PATCH 56/71] Use the mapped import in server.ts --- server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.ts b/server.ts index 58c1f72c..c7296e05 100644 --- a/server.ts +++ b/server.ts @@ -1,4 +1,4 @@ -import { serve } from "jsr:@std/http@0.224.5"; +import { serve } from "@std/http"; import { initializeWorkers } from "./src/workerPool.ts"; import { initializeCache } from "./src/playerCache.ts"; import { handleDecryptSignature } from "./src/handlers/decryptSignature.ts"; From aa113066988e415629a328c872169848c712ec89 Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 1 Dec 2025 15:58:26 -0500 Subject: [PATCH 57/71] Update deno.lock --- deno.lock | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/deno.lock b/deno.lock index 15b4b5c1..40cb7ba7 100644 --- a/deno.lock +++ b/deno.lock @@ -14,9 +14,7 @@ "jsr:@std/net@~0.224.3": "0.224.5", "jsr:@std/path@0.224": "0.224.0", "jsr:@std/path@1.0.0-rc.2": "1.0.0-rc.2", - "jsr:@std/streams@~0.224.5": "0.224.5", - "npm:astring@^1.9.0": "1.9.0", - "npm:meriyah@^6.1.4": "6.1.4" + "jsr:@std/streams@~0.224.5": "0.224.5" }, "jsr": { "@std/assert@0.224.0": { @@ -83,15 +81,6 @@ "integrity": "bcde7818dd5460d474cdbd674b15f6638b9cd73cd64e52bd852fba2bd4d8ec91" } }, - "npm": { - "astring@1.9.0": { - "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", - "bin": true - }, - "meriyah@6.1.4": { - "integrity": "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==" - } - }, "redirects": { "https://deno.land/x/ts_prometheus/mod.ts": "https://deno.land/x/ts_prometheus@v0.3.0/mod.ts" }, @@ -114,9 +103,8 @@ "dependencies": [ "jsr:@std/crypto@0.224", "jsr:@std/fs@0.224", - "jsr:@std/path@0.224", - "npm:astring@^1.9.0", - "npm:meriyah@^6.1.4" + "jsr:@std/http@0.224.5", + "jsr:@std/path@0.224" ] } } From 39c7ccac09a37b96cfd964154f496e590fa5954a Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 1 Dec 2025 16:16:14 -0500 Subject: [PATCH 58/71] Update README.md --- README.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c7357fee..3928da1e 100644 --- a/README.md +++ b/README.md @@ -39,19 +39,16 @@ docker-compose up If you have Deno installed, you can run the service directly. -Clone the repository and patch the `ejs` dependency: +Clone the repository: ```bash git clone https://github.com/kikkia/yt-cipher.git -cd yt-cipher -git clone https://github.com/yt-dlp/ejs.git -cd ejs -git checkout 5d7bf090bb9a2a8f3e2dd13ded4a21a009224f87 -cd .. -deno run --allow-read --allow-write ./scripts/patch-ejs.ts ``` +Run the server: + ```bash +cd yt-cipher && \ deno run --allow-net --allow-read --allow-write --allow-env server.ts ``` NOTE: If using an `.env` file then also add the `--env` flag From 6a199261bcf034674b3e13a8c7fccbdd775ade7b Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 1 Dec 2025 16:23:52 -0500 Subject: [PATCH 59/71] Rename from `test` to `check` in deno.yml --- .github/workflows/deno.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deno.yml b/.github/workflows/deno.yml index 9b1dee8e..16068ce6 100644 --- a/.github/workflows/deno.yml +++ b/.github/workflows/deno.yml @@ -21,7 +21,7 @@ permissions: contents: read jobs: - test: + check: runs-on: ubuntu-latest steps: From 857c6d2d65d0d1a1423491fe664b65b6e05dc7f1 Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 1 Dec 2025 16:43:13 -0500 Subject: [PATCH 60/71] Enable frozen flag for `deno check` in deno.yml --- .github/workflows/deno.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deno.yml b/.github/workflows/deno.yml index 16068ce6..5de8db2f 100644 --- a/.github/workflows/deno.yml +++ b/.github/workflows/deno.yml @@ -43,7 +43,7 @@ jobs: run: deno lint - name: Check types - run: deno check --frozen=false *.ts + run: deno check --frozen=true *.ts # - name: Run tests # run: deno test -A From 0978bed6e95a12923904e9d1fb1fc7716ccb429e Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 1 Dec 2025 16:53:06 -0500 Subject: [PATCH 61/71] Remove submodule for ejs --- .gitmodules | 3 --- ejs | 1 - 2 files changed, 4 deletions(-) delete mode 100644 .gitmodules delete mode 160000 ejs diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index da970314..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "ejs"] - path = ejs - url = https://github.com/yt-dlp/ejs.git diff --git a/ejs b/ejs deleted file mode 160000 index 2655b1f5..00000000 --- a/ejs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2655b1f55f98e5870d4e124704a21f4d793b4e1c From f18dab81a68b34ee6a02bddd1abb69615e3b14e9 Mon Sep 17 00:00:00 2001 From: tcely Date: Tue, 2 Dec 2025 19:56:47 -0500 Subject: [PATCH 62/71] Map marcopacini/ts_prometheus from GitHub --- deno.json | 3 ++- src/metrics.ts | 7 +------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/deno.json b/deno.json index 01bbf68b..05137951 100644 --- a/deno.json +++ b/deno.json @@ -11,7 +11,8 @@ "@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/" + "ejs/": "https://esm.sh/gh/yt-dlp/ejs@0.3.0&standalone/", + "ts_prometheus/": "https://esm.sh/gh/marcopacini/ts_prometheus@v0.3.0/" }, "lint": { "include": ["src/", "*.ts"], diff --git a/src/metrics.ts b/src/metrics.ts index f4407a05..3c850703 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -1,9 +1,4 @@ -import { - Counter, - Gauge, - Histogram, - Registry, -} from "https://deno.land/x/ts_prometheus/mod.ts"; +import { Counter, Gauge, Histogram, Registry } from "ts_prometheus/mod.ts"; export const registry = new Registry(); From 10010fb84a690a92e0afa00976bd20c59a9fca96 Mon Sep 17 00:00:00 2001 From: tcely Date: Tue, 2 Dec 2025 20:22:38 -0500 Subject: [PATCH 63/71] Map lru from deno.land --- deno.json | 1 + src/instrumentedCache.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 05137951..bcf67bac 100644 --- a/deno.json +++ b/deno.json @@ -12,6 +12,7 @@ "@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/", + "lru/": "https://deno.land/x/lru@1.0.2/", "ts_prometheus/": "https://esm.sh/gh/marcopacini/ts_prometheus@v0.3.0/" }, "lint": { diff --git a/src/instrumentedCache.ts b/src/instrumentedCache.ts index 2c16bd1a..8ac20028 100644 --- a/src/instrumentedCache.ts +++ b/src/instrumentedCache.ts @@ -1,5 +1,5 @@ import { cacheSize } from "./metrics.ts"; -import { LRU } from "https://deno.land/x/lru@1.0.2/mod.ts"; +import { LRU } from "lru/mod.ts"; export class InstrumentedLRU extends LRU { constructor(private cacheName: string, maxSize: number) { From 7c84da08efa42587827f8fe9987cc4439fdf31b1 Mon Sep 17 00:00:00 2001 From: tcely Date: Tue, 2 Dec 2025 20:30:20 -0500 Subject: [PATCH 64/71] Update deno.lock --- deno.lock | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/deno.lock b/deno.lock index 40cb7ba7..1c732bb3 100644 --- a/deno.lock +++ b/deno.lock @@ -81,22 +81,11 @@ "integrity": "bcde7818dd5460d474cdbd674b15f6638b9cd73cd64e52bd852fba2bd4d8ec91" } }, - "redirects": { - "https://deno.land/x/ts_prometheus/mod.ts": "https://deno.land/x/ts_prometheus@v0.3.0/mod.ts" - }, "remote": { "https://deno.land/x/lru@1.0.2/mod.ts": "1d44b87c4d40ff33749ae5fd85fe234344e0dace835fdfeb48413edea9461159", - "https://deno.land/x/ts_prometheus@v0.3.0/collector.ts": "12305e262e60de3b9b2db22670f95e388b6f4c0ff8da0fdbaf4cd4758bb5b1b6", - "https://deno.land/x/ts_prometheus@v0.3.0/counter.ts": "c6a03fc6ceb732a70728e2633a6781f607615c5ce5e6ee46fdff34b37bde0ef5", - "https://deno.land/x/ts_prometheus@v0.3.0/gauge.ts": "d2d3b79df3fae07652ee3b72c118cf6e96c834c82c81eb2ebf52e8f136b24fa5", - "https://deno.land/x/ts_prometheus@v0.3.0/histogram.ts": "7585024285ef52b29054adc02d480f5ac4595e0037a8ad5bd1d75ddc205a3fd0", - "https://deno.land/x/ts_prometheus@v0.3.0/metric.ts": "c7635f8b4ec92742e01244712bb8264c32ce91c03f3a8332b9119926dab027cb", - "https://deno.land/x/ts_prometheus@v0.3.0/mod.ts": "9fef6a6c301da262dfa38d111cb4011e96d0c6b1c9f1a233526506c8ce8723bb", - "https://deno.land/x/ts_prometheus@v0.3.0/registry.ts": "b7d4b4b6e008d7ffb8b4acff6a1a56b27463bfb4ecec4f0455562187f7941891", - "https://deno.land/x/ts_prometheus@v0.3.0/summary.ts": "d1ef0341e265fa8d2a2bc42e8d732428d8aadbfa0f60ce4c2baa0486910590df", - "https://esm.sh/gh/yt-dlp/ejs@0.3.0&standalone/src/yt/solver/main.ts": "8b2cdc4da906b66f79df57d9cc715badfe92a793faa59841e6ccb641c604ea55", + "https://esm.sh/gh/marcopacini/ts_prometheus@v0.3.0/denonext/mod.ts.mjs": "3a2e38e83add647f9758c140a30b3ef84f326cbc00ef75ca6c2331c81415ae58", + "https://esm.sh/gh/marcopacini/ts_prometheus@v0.3.0/mod.ts": "2a754040ada85e5dc78e77c1c619cca9992be7d63a90994861aeaec49ed467f3", "https://esm.sh/gh/yt-dlp/ejs@0.3.0&standalone/src/yt/solver/solvers.ts": "269a7e74a2f021621402fbee5872168564196151df892df8fb17229b593dd0b3", - "https://esm.sh/gh/yt-dlp/ejs@0.3.0/denonext/src/yt/solver/main.ts.bundle.mjs": "1dcc16eb6921cac07e440b991c74fb928f49a22554bd422df3aa8ddd09c46e26", "https://esm.sh/gh/yt-dlp/ejs@0.3.0/denonext/src/yt/solver/solvers.ts.bundle.mjs": "88c7e36c180c2f9416fd26ba9a7db344d8008a7f1069e5d7a91c0bdde44a5656" }, "workspace": { From 63e414af0ad5d172a643ae29a4764eea6620e29f Mon Sep 17 00:00:00 2001 From: tcely Date: Tue, 2 Dec 2025 20:31:17 -0500 Subject: [PATCH 65/71] Include all lint rules --- deno.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/deno.json b/deno.json index bcf67bac..0f33ac0f 100644 --- a/deno.json +++ b/deno.json @@ -16,9 +16,6 @@ "ts_prometheus/": "https://esm.sh/gh/marcopacini/ts_prometheus@v0.3.0/" }, "lint": { - "include": ["src/", "*.ts"], - "rules": { - "exclude": [ "no-import-prefix" ] - } + "include": ["src/", "*.ts"] } } From 10fe855bca022ba47ab38ca484be2303d02d5001 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 5 Dec 2025 22:39:17 -0500 Subject: [PATCH 66/71] Replace LRU with LruCache --- deno.json | 2 +- deno.lock | 6 +++++- src/instrumentedCache.ts | 18 ++++++++++++++---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/deno.json b/deno.json index 0f33ac0f..373e5455 100644 --- a/deno.json +++ b/deno.json @@ -7,12 +7,12 @@ "indentWidth": 4 }, "imports": { + "@std/cache": "jsr:@std/cache@^0.2.1", "@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/", - "lru/": "https://deno.land/x/lru@1.0.2/", "ts_prometheus/": "https://esm.sh/gh/marcopacini/ts_prometheus@v0.3.0/" }, "lint": { diff --git a/deno.lock b/deno.lock index 1c732bb3..9c008d95 100644 --- a/deno.lock +++ b/deno.lock @@ -3,6 +3,7 @@ "specifiers": { "jsr:@std/assert@0.224": "0.224.0", "jsr:@std/async@^1.0.0-rc.1": "1.0.15", + "jsr:@std/cache@~0.2.1": "0.2.1", "jsr:@std/cli@~0.224.7": "0.224.7", "jsr:@std/crypto@0.224": "0.224.0", "jsr:@std/encoding@0.224": "0.224.3", @@ -23,6 +24,9 @@ "@std/async@1.0.15": { "integrity": "55d1d9d04f99403fe5730ab16bdcc3c47f658a6bf054cafb38a50f046238116e" }, + "@std/cache@0.2.1": { + "integrity": "b6f1abfd118d35b1c4ca90f2b3f4c709a2014ae368f244bdc7533bf1c169d759" + }, "@std/cli@0.224.7": { "integrity": "654ca6477518e5e3a0d3fabafb2789e92b8c0febf1a1d24ba4b567aba94b5977" }, @@ -82,7 +86,6 @@ } }, "remote": { - "https://deno.land/x/lru@1.0.2/mod.ts": "1d44b87c4d40ff33749ae5fd85fe234344e0dace835fdfeb48413edea9461159", "https://esm.sh/gh/marcopacini/ts_prometheus@v0.3.0/denonext/mod.ts.mjs": "3a2e38e83add647f9758c140a30b3ef84f326cbc00ef75ca6c2331c81415ae58", "https://esm.sh/gh/marcopacini/ts_prometheus@v0.3.0/mod.ts": "2a754040ada85e5dc78e77c1c619cca9992be7d63a90994861aeaec49ed467f3", "https://esm.sh/gh/yt-dlp/ejs@0.3.0&standalone/src/yt/solver/solvers.ts": "269a7e74a2f021621402fbee5872168564196151df892df8fb17229b593dd0b3", @@ -90,6 +93,7 @@ }, "workspace": { "dependencies": [ + "jsr:@std/cache@~0.2.1", "jsr:@std/crypto@0.224", "jsr:@std/fs@0.224", "jsr:@std/http@0.224.5", diff --git a/src/instrumentedCache.ts b/src/instrumentedCache.ts index 8ac20028..2f950fcd 100644 --- a/src/instrumentedCache.ts +++ b/src/instrumentedCache.ts @@ -1,7 +1,7 @@ import { cacheSize } from "./metrics.ts"; -import { LRU } from "lru/mod.ts"; +import { LruCache } from "@std/cache"; -export class InstrumentedLRU extends LRU { +export class InstrumentedLRU extends LruCache { constructor(private cacheName: string, maxSize: number) { super(maxSize); } @@ -12,8 +12,18 @@ export class InstrumentedLRU extends LRU { return this; } - override remove(key: string) { - super.remove(key); + override delete(key: string): boolean { + const result = super.delete(key); cacheSize.labels({ cache_name: this.cacheName }).set(this.size); + return result; + } + + override clear(): void { + super.clear(); + cacheSize.labels({ cache_name: this.cacheName }).set(this.size); + } + + public remove(key: string): void { + this.delete(key); } } From 72954d32a50b584f6c914019af478608dc637ae5 Mon Sep 17 00:00:00 2001 From: tcely Date: Mon, 8 Dec 2025 22:54:12 -0500 Subject: [PATCH 67/71] Upgrade ejs to: 0.3.2 --- deno.json | 2 +- deno.lock | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/deno.json b/deno.json index 373e5455..9c02f708 100644 --- a/deno.json +++ b/deno.json @@ -12,7 +12,7 @@ "@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/", + "ejs/": "https://esm.sh/gh/yt-dlp/ejs@0.3.2&standalone/", "ts_prometheus/": "https://esm.sh/gh/marcopacini/ts_prometheus@v0.3.0/" }, "lint": { diff --git a/deno.lock b/deno.lock index 9c008d95..5773e2d5 100644 --- a/deno.lock +++ b/deno.lock @@ -89,7 +89,9 @@ "https://esm.sh/gh/marcopacini/ts_prometheus@v0.3.0/denonext/mod.ts.mjs": "3a2e38e83add647f9758c140a30b3ef84f326cbc00ef75ca6c2331c81415ae58", "https://esm.sh/gh/marcopacini/ts_prometheus@v0.3.0/mod.ts": "2a754040ada85e5dc78e77c1c619cca9992be7d63a90994861aeaec49ed467f3", "https://esm.sh/gh/yt-dlp/ejs@0.3.0&standalone/src/yt/solver/solvers.ts": "269a7e74a2f021621402fbee5872168564196151df892df8fb17229b593dd0b3", - "https://esm.sh/gh/yt-dlp/ejs@0.3.0/denonext/src/yt/solver/solvers.ts.bundle.mjs": "88c7e36c180c2f9416fd26ba9a7db344d8008a7f1069e5d7a91c0bdde44a5656" + "https://esm.sh/gh/yt-dlp/ejs@0.3.0/denonext/src/yt/solver/solvers.ts.bundle.mjs": "88c7e36c180c2f9416fd26ba9a7db344d8008a7f1069e5d7a91c0bdde44a5656", + "https://esm.sh/gh/yt-dlp/ejs@0.3.2&standalone/src/yt/solver/solvers.ts": "72a4211a3a6a3d0f0da77a32080d20743ebc90971aa7298fcc2b3eb099315157", + "https://esm.sh/gh/yt-dlp/ejs@0.3.2/denonext/src/yt/solver/solvers.ts.bundle.mjs": "f4d41475fd3dd459ab5f9bdaac55fffe57239a13554234a0ae3217949baad899" }, "workspace": { "dependencies": [ From 75446d7099c32ac0b7ecd97e7957cb4a1b3fb384 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 12 Dec 2025 17:49:11 -0500 Subject: [PATCH 68/71] Improve cache_prefix assignment Refactor cache_prefix initialization to handle different environment variables more clearly. --- src/playerCache.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/playerCache.ts b/src/playerCache.ts index 8e0cda7a..85f1fedd 100644 --- a/src/playerCache.ts +++ b/src/playerCache.ts @@ -3,14 +3,15 @@ import { ensureDir } from "@std/fs"; import { join } from "@std/path"; import { cacheSize, playerScriptFetches } from "./metrics.ts"; -let cache_prefix = Deno.cwd(); +let cache_prefix: string; const HOME = Deno.env.get("HOME"); -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"); +} else if (HOME) { + cache_prefix = join(HOME, ".cache", "yt-cipher"); +} else { + cache_prefix = Deno.cwd(); } export const CACHE_DIR = join(cache_prefix, "player_cache"); From 2e2f6923e3e15be09bb3d28ae5e8d0542ccd4069 Mon Sep 17 00:00:00 2001 From: MoonCarli Date: Wed, 24 Dec 2025 12:35:08 +0530 Subject: [PATCH 69/71] refactor: improve cache handling and logging --- scripts/patch-ejs.ts | 12 +++++++----- server.ts | 4 +++- src/playerCache.ts | 37 ++++++++++++++++++++++++++++++------- src/workerPool.ts | 3 ++- 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/scripts/patch-ejs.ts b/scripts/patch-ejs.ts index 11f6890d..b0094d91 100644 --- a/scripts/patch-ejs.ts +++ b/scripts/patch-ejs.ts @@ -3,6 +3,10 @@ import { join } from "https://deno.land/std@0.224.0/path/mod.ts"; const EJS_SRC_DIR = join(Deno.cwd(), "ejs/src"); +function timestamp() { + return new Date().toISOString().slice(5, 19).replace("T", " "); +} + async function patchFile(path: string) { let content = await Deno.readTextFile(path); let changed = false; @@ -21,16 +25,14 @@ async function patchFile(path: string) { if (changed) { await Deno.writeTextFile(path, content); - console.log(`Patched ${path}`); + console.log(`[${timestamp()}] Patched ${path}`); } } - -console.log(`Starting to patch files in ${EJS_SRC_DIR}...`); +console.log(`[${timestamp()}] Starting to patch files in ${EJS_SRC_DIR}...`); for await (const entry of walk(EJS_SRC_DIR, { exts: [".ts"] })) { if (entry.isFile) { await patchFile(entry.path); } } - -console.log("Patching complete."); \ No newline at end of file +console.log(`[${timestamp()}] Patching complete.`); \ No newline at end of file diff --git a/server.ts b/server.ts index cafe62f6..a6c018d1 100644 --- a/server.ts +++ b/server.ts @@ -94,5 +94,7 @@ const host = Deno.env.get("HOST") || '0.0.0.0'; await initializeCache(); initializeWorkers(); -console.log(`Server listening on http://${host}:${port}`); +const ts = new Date().toISOString().slice(5, 19).replace("T", " "); +console.log(`[${ts}] Server listening on http://${host}:${port}`); + await serve(handler, { port: Number(port), hostname: host }); \ No newline at end of file diff --git a/src/playerCache.ts b/src/playerCache.ts index 994858ef..e8761073 100644 --- a/src/playerCache.ts +++ b/src/playerCache.ts @@ -6,8 +6,31 @@ import { extractPlayerId } from "./utils.ts"; const ignorePlayerScriptRegion = Deno.env.get("IGNORE_SCRIPT_REGION") === "true"; -export const CACHE_HOME = Deno.env.get("XDG_CACHE_HOME") || join(Deno.env.get("HOME"), '.cache'); -export const CACHE_DIR = join(CACHE_HOME, 'yt-cipher', 'player_cache'); +function getCacheHome(): string { + // XDG standard (Linux, optional on others) + const xdg = Deno.env.get("XDG_CACHE_HOME"); + if (xdg) return xdg; + + // Windows + if (Deno.build.os === "windows") { + const localAppData = Deno.env.get("LOCALAPPDATA"); + if (localAppData) return join(localAppData, "cache"); + } + + // macOS / Linux fallback + const home = Deno.env.get("HOME") ?? Deno.env.get("USERPROFILE"); + if (home) return join(home, ".cache"); + + throw new Error("Unable to determine cache directory"); +} + +export const CACHE_HOME = getCacheHome(); +export const CACHE_DIR = join(CACHE_HOME, "yt-cipher", "player_cache"); + +// This is used for better logging +function timestamp() { + return new Date().toISOString().slice(5, 19).replace("T", " "); +} export async function getPlayerFilePath(playerUrl: string): Promise { let cacheKey: string; @@ -30,7 +53,7 @@ export async function getPlayerFilePath(playerUrl: string): Promise { return filePath; } catch (error) { if (error instanceof Deno.errors.NotFound) { - console.log(`Cache miss for player: ${playerUrl}. Fetching...`); + console.log(`[${timestamp()}] Cache miss for player: ${playerUrl}. Fetching...`); const response = await fetch(playerUrl); playerScriptFetches.labels({ player_url: playerUrl, status: response.statusText }).inc(); if (!response.ok) { @@ -46,7 +69,7 @@ export async function getPlayerFilePath(playerUrl: string): Promise { } cacheSize.labels({ cache_name: 'player' }).set(fileCount); - console.log(`Saved player to cache: ${filePath}`); + console.log(`[${timestamp()}] Saved player to cache: ${filePath}`); return filePath; } throw error; @@ -59,14 +82,14 @@ export async function initializeCache() { // Since these accumulate over time just cleanout 14 day unused ones let fileCount = 0; const thirtyDays = 14 * 24 * 60 * 60 * 1000; - console.log(`Cleaning up player cache directory: ${CACHE_DIR}`); + console.log(`[${timestamp()}] Cleaning up player cache directory: ${CACHE_DIR}`); for await (const dirEntry of Deno.readDir(CACHE_DIR)) { 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(); if (lastAccessed && (Date.now() - lastAccessed > thirtyDays)) { - console.log(`Deleting stale player cache file: ${filePath}`); + console.log(`[${timestamp()}] Deleting stale player cache file: ${filePath}`); await Deno.remove(filePath); } else { fileCount++; @@ -74,5 +97,5 @@ export async function initializeCache() { } } cacheSize.labels({ cache_name: 'player' }).set(fileCount); - console.log(`Player cache directory ensured at: ${CACHE_DIR}`); + console.log(`[${timestamp()}] Player cache directory ensured at: ${CACHE_DIR}`); } diff --git a/src/workerPool.ts b/src/workerPool.ts index 79b6a9b2..a1013508 100644 --- a/src/workerPool.ts +++ b/src/workerPool.ts @@ -47,5 +47,6 @@ export function initializeWorkers() { worker.isIdle = true; workers.push(worker); } - console.log(`Initialized ${CONCURRENCY} workers`); + const ts = new Date().toISOString().slice(5, 19).replace("T", " "); + console.log(`[${ts}] Initialized ${CONCURRENCY} workers`); } \ No newline at end of file From 904017398a82303fdd40cddf4e7ecf1b02ac57f9 Mon Sep 17 00:00:00 2001 From: tcely Date: Thu, 1 Jan 2026 07:10:41 -0500 Subject: [PATCH 70/71] Integrate upstream master and pull request 25 --- server.ts | 4 +- src/playerCache.ts | 146 ++++++++++++++++++++++++++++++++------------- src/utils.ts | 16 +++++ src/workerPool.ts | 4 +- 4 files changed, 126 insertions(+), 44 deletions(-) diff --git a/server.ts b/server.ts index 0bc48505..1169256e 100644 --- a/server.ts +++ b/server.ts @@ -8,6 +8,7 @@ import { withMetrics } from "./src/middleware.ts"; import { withValidation } from "./src/validation.ts"; import { registry } from "./src/metrics.ts"; import type { ApiRequest, RequestContext } from "./src/types.ts"; +import { getTimestamp } from "./src/utils.ts"; const API_TOKEN = Deno.env.get("API_TOKEN"); @@ -105,7 +106,6 @@ const host = Deno.env.get("HOST") || "0.0.0.0"; await initializeCache(); initializeWorkers(); -const ts = new Date().toISOString().slice(5, 19).replace("T", " "); -console.log(`[${ts}] Server listening on http://${host}:${port}`); +console.log(`[${getTimestamp()}] Server listening on http://${host}:${port}`); await serve(handler, { port: Number(port), hostname: host }); diff --git a/src/playerCache.ts b/src/playerCache.ts index 2ebe4196..edf04201 100644 --- a/src/playerCache.ts +++ b/src/playerCache.ts @@ -1,26 +1,30 @@ -import { crypto } from "@std/crypto"; import { ensureDir } from "@std/fs"; import { join } from "@std/path"; import { cacheSize, playerScriptFetches } from "./metrics.ts"; -import { extractPlayerId } from "./utils.ts"; +import { digestPlayerUrl, extractPlayerId, getTimestamp } from "./utils.ts"; -const ignorePlayerScriptRegion = Deno.env.get("IGNORE_SCRIPT_REGION") === "true"; +const inFlightPlayerFetches = new Map>(); +const ignorePlayerScriptRegion = ["1", "true", "yes", "on"].includes( + (Deno.env.get("IGNORE_SCRIPT_REGION") ?? "").trim().toLowerCase(), +); function getCachePrefix(): string { // Windows if (Deno.build.os === "windows") { - const TMP = Deno.env.get("TMP"); - const TEMP = Deno.env.get("TEMP"); const localAppData = Deno.env.get("LOCALAPPDATA"); - if (TEMP) { - return join(TEMP, "yt-cipher"); - } else if (TMP) { - return join(TMP, "yt-cipher"); - } else if (localAppData) { - return join(localAppData, "yt-cipher"); + const userProfile = Deno.env.get("USERPROFILE"); + const TEMP = Deno.env.get("TEMP"); + const TMP = Deno.env.get("TMP"); + + if (localAppData) return join(localAppData, "yt-cipher"); + if (userProfile) { + return join(userProfile, "AppData", "Local", "yt-cipher"); } + if (TEMP) return join(TEMP, "yt-cipher"); + if (TMP) return join(TMP, "yt-cipher"); - throw new Error("Unable to determine cache directory"); + // Last-resort fallback to avoid hard crash. + return join(Deno.cwd(), "yt-cipher"); } // XDG standard (Linux, optional on others) @@ -38,30 +42,28 @@ function getCachePrefix(): string { return Deno.cwd(); } -let cache_prefix = getCachePrefix(); -export const CACHE_DIR = join(cache_prefix, "player_cache"); - -// This is used for better logging -function timestamp() { - return new Date().toISOString().slice(5, 19).replace("T", " "); -} +export const CACHE_DIR = join(getCachePrefix(), "player_cache"); export async function getPlayerFilePath(playerUrl: string): Promise { let cacheKey: string; if (ignorePlayerScriptRegion) { // I have not seen any scripts that differ between regions so this should be safe - cacheKey = extractPlayerId(playerUrl); + const playerId = extractPlayerId(playerUrl); + // If we can't reliably extract an id, fall back to hashing the full URL to avoid cache key collisions. + if (playerId === "unknown") { + cacheKey = await digestPlayerUrl(playerUrl); + } else { + cacheKey = playerId.replace(/[^a-zA-Z0-9_-]/g, "_"); + // Ensure that the file name is below the 128 character limit + if (cacheKey.length > 120) { + cacheKey = await digestPlayerUrl(playerUrl); + } + } } else { // 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), - ); - cacheKey = Array.from(new Uint8Array(hashBuffer)).map((b) => - b.toString(16).padStart(2, "0") - ).join(""); + cacheKey = await digestPlayerUrl(playerUrl); } const filePath = join(CACHE_DIR, `${cacheKey}.js`); @@ -71,9 +73,30 @@ export async function getPlayerFilePath(playerUrl: string): Promise { await Deno.utime(filePath, new Date(), stat.mtime ?? new Date()); return filePath; } catch (error) { - if (error instanceof Deno.errors.NotFound) { - console.log(`[${timestamp()}] Cache miss for player: ${playerUrl}. Fetching...`); - const response = await fetch(playerUrl); + if (!(error instanceof Deno.errors.NotFound)) throw error; + + const existing = inFlightPlayerFetches.get(filePath); + if (existing) { + try { + return await existing; + } catch (err) { + console.warn( + `[${getTimestamp()}] Previous fetch failed for player: ${playerUrl} (${filePath}); retrying...`, + err, + ); + + // Allow a retry if the shared fetch failed. + inFlightPlayerFetches.delete(filePath); + } + } + + const fetchPromise = (async () => { + console.log( + `[${getTimestamp()}] Cache miss for player: ${playerUrl}. Fetching...`, + ); + const response = await fetch(playerUrl, { + signal: AbortSignal.timeout(60_000), + }); playerScriptFetches.labels({ player_url: playerUrl, status: response.statusText, @@ -84,7 +107,27 @@ export async function getPlayerFilePath(playerUrl: string): Promise { ); } const playerContent = await response.text(); - await Deno.writeTextFile(filePath, playerContent); + + // Ensure cache dir still exists (it may be deleted between startup and a request). + await ensureDir(CACHE_DIR); + + // use a temporary directory to allow atomic file updates + const tempDirPath = await Deno.makeTempDir({ dir: CACHE_DIR }); + const tempFilePath = join(tempDirPath, "file.js"); + try { + await Deno.writeTextFile(tempFilePath, playerContent); + + // Remove anything that might be there now. + await Deno.remove(filePath, { recursive: true }).catch( + () => {}, + ); + // Now this rename either succeeds or fails. + await Deno.rename(tempFilePath, filePath); + } finally { + await Deno.remove(tempDirPath, { recursive: true }).catch( + () => {}, + ); + } // Update cache size for metrics let fileCount = 0; @@ -93,10 +136,19 @@ export async function getPlayerFilePath(playerUrl: string): Promise { } cacheSize.labels({ cache_name: "player" }).set(fileCount); - console.log(`[${timestamp()}] Saved player to cache: ${filePath}`); + console.log( + `[${getTimestamp()}] Saved player to cache: ${filePath}`, + ); return filePath; + })(); + + inFlightPlayerFetches.set(filePath, fetchPromise); + try { + return await fetchPromise; + } finally { + // ensure map doesn’t leak entries on success/failure + inFlightPlayerFetches.delete(filePath); } - throw error; } } @@ -105,22 +157,36 @@ export async function initializeCache() { // Since these accumulate over time just cleanout 14 day unused ones let fileCount = 0; - const thirtyDays = 14 * 24 * 60 * 60 * 1000; - console.log(`[${timestamp()}] Cleaning up player cache directory: ${CACHE_DIR}`); + const twoWeeks = 14 * 24 * 60 * 60 * 1000; + console.log( + `[${getTimestamp()}] Cleaning up player cache directory: ${CACHE_DIR}`, + ); for await (const dirEntry of Deno.readDir(CACHE_DIR)) { - if (dirEntry.isFile) { - const filePath = join(CACHE_DIR, dirEntry.name); + if (!dirEntry.isFile) continue; + const filePath = join(CACHE_DIR, dirEntry.name); + try { const stat = await Deno.stat(filePath); const lastAccessed = stat.atime?.getTime() ?? stat.mtime?.getTime() ?? stat.birthtime?.getTime(); - if (lastAccessed && (Date.now() - lastAccessed > thirtyDays)) { - console.log(`[${timestamp()}] Deleting stale player cache file: ${filePath}`); + if (lastAccessed && (Date.now() - lastAccessed > twoWeeks)) { + console.log( + `[${getTimestamp()}] Deleting stale player cache file: ${filePath}`, + ); await Deno.remove(filePath); } else { fileCount++; } + } catch (err) { + // File may have disappeared or be unreadable; don't fail startup. + console.warn( + `[${getTimestamp()}] Skipping cache entry during cleanup: ${filePath}`, + err, + ); + continue; } } cacheSize.labels({ cache_name: "player" }).set(fileCount); - console.log(`[${timestamp()}] Player cache directory ensured at: ${CACHE_DIR}`); + console.log( + `[${getTimestamp()}] Player cache directory ensured at: ${CACHE_DIR}`, + ); } diff --git a/src/utils.ts b/src/utils.ts index 76b0a227..e5ab5973 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,5 @@ +import { crypto } from "@std/crypto"; + const ALLOWED_HOSTNAMES = ["youtube.com", "www.youtube.com", "m.youtube.com"]; export function validateAndNormalizePlayerUrl(playerUrl: string): string { @@ -39,3 +41,17 @@ export function extractPlayerId(playerUrl: string): string { } return "unknown"; } + +export async function digestPlayerUrl(playerUrl: string): Promise { + const hashBuffer = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(playerUrl), + ); + return Array.from(new Uint8Array(hashBuffer)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); +} + +export function getTimestamp() { + return new Date().toISOString().slice(5, 19).replace("T", " "); +} diff --git a/src/workerPool.ts b/src/workerPool.ts index 85c30548..77c9bf4b 100644 --- a/src/workerPool.ts +++ b/src/workerPool.ts @@ -1,4 +1,5 @@ import type { Task, WorkerWithStatus } from "./types.ts"; +import { getTimestamp } from "./utils.ts"; const CONCURRENCY = parseInt(Deno.env.get("MAX_THREADS") || "", 10) || navigator.hardwareConcurrency || 1; @@ -51,6 +52,5 @@ export function initializeWorkers() { worker.isIdle = true; workers.push(worker); } - const ts = new Date().toISOString().slice(5, 19).replace("T", " "); - console.log(`[${ts}] Initialized ${CONCURRENCY} workers`); + console.log(`[${getTimestamp()}] Initialized ${CONCURRENCY} workers`); } From 2ee32044048f3ecda8bff674c1708e92e0ae3ec7 Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 2 Jan 2026 04:07:40 -0500 Subject: [PATCH 71/71] Switch `/tini` to a verified static binary --- Dockerfile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 820aa22e..6e1086a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,12 +12,17 @@ RUN deno compile \ --include worker.ts \ server.ts +FROM ghcr.io/tcely/docker-tini:main@sha256:0b16fede939249d3783966eba573bac03cf106721df5a63e3555f6b8b0cef074 AS tini-bin +FROM scratch AS tini +ARG TARGETARCH TINI_VERSION="0.19.0" +COPY --from=tini-bin "/releases/v${TINI_VERSION}/tini-static-${TARGETARCH}" /tini + FROM gcr.io/distroless/cc-debian13:debug SHELL ["/busybox/busybox", "sh", "-c"] WORKDIR /app -COPY --from=builder /tini /tini +COPY --from=tini /tini /tini COPY --from=builder /usr/src/app/server /app/server COPY --from=builder --chown=nonroot:nonroot /usr/src/app/docs /app/docs