From a54e90cd77d8ecbe12c243256891e7fb5e68ac34 Mon Sep 17 00:00:00 2001 From: Simon Sawicki Date: Sun, 24 Aug 2025 08:01:07 +0200 Subject: [PATCH] Initial POC --- .gitignore | 4 + README.md | 9 ++ bundle.ts | 12 ++ deno.jsonc | 10 ++ deno.lock | 263 ++++++++++++++++++++++++++++++++++++ pyproject.toml | 43 ++++++ src/main.ts | 44 ++++++ src/nsig.ts | 113 ++++++++++++++++ src/sig.ts | 122 +++++++++++++++++ src/solvers.ts | 103 ++++++++++++++ src/solvers_test.ts | 22 +++ src/types.ts | 4 + src/utils.ts | 49 +++++++ tests/download.ts | 27 ++++ tests/players/.gitignore | 2 + tests/tests.ts | 71 ++++++++++ tests/utils.ts | 5 + yt_dlp_jsc_deno/__init__.py | 20 +++ 18 files changed, 923 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 bundle.ts create mode 100644 deno.jsonc create mode 100644 deno.lock create mode 100644 pyproject.toml create mode 100644 src/main.ts create mode 100644 src/nsig.ts create mode 100644 src/sig.ts create mode 100755 src/solvers.ts create mode 100644 src/solvers_test.ts create mode 100644 src/types.ts create mode 100644 src/utils.ts create mode 100644 tests/download.ts create mode 100644 tests/players/.gitignore create mode 100644 tests/tests.ts create mode 100644 tests/utils.ts create mode 100644 yt_dlp_jsc_deno/__init__.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95d3bb5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/dist +*.py[cd] +/yt_dlp_jsc_deno/dist +/yt_dlp_jsc_deno/__about__.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..0dc1319 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# yt-dlp-jsc-deno +Deno backend of builtin JavaScript Challenge Provider for yt-dlp + +## Installation +In the yt-dlp repository, install the """python""" package, either by doing +```console +pip install ../yt-dlp-jsp-deno +``` +or from the url. diff --git a/bundle.ts b/bundle.ts new file mode 100644 index 0000000..9316f50 --- /dev/null +++ b/bundle.ts @@ -0,0 +1,12 @@ +import * as esbuild from "npm:esbuild@0.25.5"; +import { denoPlugins } from "jsr:@luca/esbuild-deno-loader@0.11.1"; + +await esbuild.build({ + plugins: [...denoPlugins()], + entryPoints: ["./src/main.ts"], + outfile: "./dist/jsc-deno.js", + bundle: true, + format: "esm", + sourcemap: false, +}); +esbuild.stop(); diff --git a/deno.jsonc b/deno.jsonc new file mode 100644 index 0000000..62c0112 --- /dev/null +++ b/deno.jsonc @@ -0,0 +1,10 @@ +{ + "test": { + "exclude": ["./dist"] + }, + "tasks": { + "download": "deno run --allow-read --allow-write --allow-net=www.youtube.com tests/download.ts", + "test": "deno test --allow-read --allow-env=BABEL_TYPES_8_BREAKING --location 'https://www.youtube.com/watch?v=yt-dlp-wins'", + "bundle": "deno run --allow-env --allow-read --allow-run --allow-write bundle.ts" + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..016d662 --- /dev/null +++ b/deno.lock @@ -0,0 +1,263 @@ +{ + "version": "5", + "specifiers": { + "jsr:@luca/esbuild-deno-loader@0.11.1": "0.11.1", + "jsr:@std/assert@1": "1.0.14", + "jsr:@std/bytes@^1.0.2": "1.0.6", + "jsr:@std/encoding@^1.0.5": "1.0.10", + "jsr:@std/fs@*": "1.0.19", + "jsr:@std/internal@^1.0.10": "1.0.10", + "jsr:@std/path@^1.0.6": "1.1.2", + "npm:@babel/generator@7.28.3": "7.28.3", + "npm:@babel/parser@7.28.3": "7.28.3", + "npm:@babel/types@7.28.2": "7.28.2", + "npm:esbuild@0.25.5": "0.25.5" + }, + "jsr": { + "@luca/esbuild-deno-loader@0.11.1": { + "integrity": "dc020d16d75b591f679f6b9288b10f38bdb4f24345edb2f5732affa1d9885267", + "dependencies": [ + "jsr:@std/bytes", + "jsr:@std/encoding", + "jsr:@std/path" + ] + }, + "@std/assert@1.0.14": { + "integrity": "68d0d4a43b365abc927f45a9b85c639ea18a9fab96ad92281e493e4ed84abaa4", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/bytes@1.0.6": { + "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" + }, + "@std/encoding@1.0.10": { + "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" + }, + "@std/fs@1.0.19": { + "integrity": "051968c2b1eae4d2ea9f79a08a3845740ef6af10356aff43d3e2ef11ed09fb06" + }, + "@std/internal@1.0.10": { + "integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7" + }, + "@std/path@1.1.2": { + "integrity": "c0b13b97dfe06546d5e16bf3966b1cadf92e1cc83e56ba5476ad8b498d9e3038", + "dependencies": [ + "jsr:@std/internal" + ] + } + }, + "npm": { + "@babel/generator@7.28.3": { + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dependencies": [ + "@babel/parser", + "@babel/types", + "@jridgewell/gen-mapping", + "@jridgewell/trace-mapping", + "jsesc" + ] + }, + "@babel/helper-string-parser@7.27.1": { + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==" + }, + "@babel/helper-validator-identifier@7.27.1": { + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==" + }, + "@babel/parser@7.28.3": { + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "dependencies": [ + "@babel/types" + ], + "bin": true + }, + "@babel/types@7.28.2": { + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dependencies": [ + "@babel/helper-string-parser", + "@babel/helper-validator-identifier" + ] + }, + "@esbuild/aix-ppc64@0.25.5": { + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "os": ["aix"], + "cpu": ["ppc64"] + }, + "@esbuild/android-arm64@0.25.5": { + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@esbuild/android-arm@0.25.5": { + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "os": ["android"], + "cpu": ["arm"] + }, + "@esbuild/android-x64@0.25.5": { + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "os": ["android"], + "cpu": ["x64"] + }, + "@esbuild/darwin-arm64@0.25.5": { + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@esbuild/darwin-x64@0.25.5": { + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@esbuild/freebsd-arm64@0.25.5": { + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "os": ["freebsd"], + "cpu": ["arm64"] + }, + "@esbuild/freebsd-x64@0.25.5": { + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@esbuild/linux-arm64@0.25.5": { + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@esbuild/linux-arm@0.25.5": { + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@esbuild/linux-ia32@0.25.5": { + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "os": ["linux"], + "cpu": ["ia32"] + }, + "@esbuild/linux-loong64@0.25.5": { + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@esbuild/linux-mips64el@0.25.5": { + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "os": ["linux"], + "cpu": ["mips64el"] + }, + "@esbuild/linux-ppc64@0.25.5": { + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@esbuild/linux-riscv64@0.25.5": { + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@esbuild/linux-s390x@0.25.5": { + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@esbuild/linux-x64@0.25.5": { + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@esbuild/netbsd-arm64@0.25.5": { + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "os": ["netbsd"], + "cpu": ["arm64"] + }, + "@esbuild/netbsd-x64@0.25.5": { + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "os": ["netbsd"], + "cpu": ["x64"] + }, + "@esbuild/openbsd-arm64@0.25.5": { + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "os": ["openbsd"], + "cpu": ["arm64"] + }, + "@esbuild/openbsd-x64@0.25.5": { + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "os": ["openbsd"], + "cpu": ["x64"] + }, + "@esbuild/sunos-x64@0.25.5": { + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "os": ["sunos"], + "cpu": ["x64"] + }, + "@esbuild/win32-arm64@0.25.5": { + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@esbuild/win32-ia32@0.25.5": { + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "os": ["win32"], + "cpu": ["ia32"] + }, + "@esbuild/win32-x64@0.25.5": { + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@jridgewell/gen-mapping@0.3.13": { + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dependencies": [ + "@jridgewell/sourcemap-codec", + "@jridgewell/trace-mapping" + ] + }, + "@jridgewell/resolve-uri@3.1.2": { + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" + }, + "@jridgewell/sourcemap-codec@1.5.5": { + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "@jridgewell/trace-mapping@0.3.30": { + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dependencies": [ + "@jridgewell/resolve-uri", + "@jridgewell/sourcemap-codec" + ] + }, + "esbuild@0.25.5": { + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "optionalDependencies": [ + "@esbuild/aix-ppc64", + "@esbuild/android-arm", + "@esbuild/android-arm64", + "@esbuild/android-x64", + "@esbuild/darwin-arm64", + "@esbuild/darwin-x64", + "@esbuild/freebsd-arm64", + "@esbuild/freebsd-x64", + "@esbuild/linux-arm", + "@esbuild/linux-arm64", + "@esbuild/linux-ia32", + "@esbuild/linux-loong64", + "@esbuild/linux-mips64el", + "@esbuild/linux-ppc64", + "@esbuild/linux-riscv64", + "@esbuild/linux-s390x", + "@esbuild/linux-x64", + "@esbuild/netbsd-arm64", + "@esbuild/netbsd-x64", + "@esbuild/openbsd-arm64", + "@esbuild/openbsd-x64", + "@esbuild/sunos-x64", + "@esbuild/win32-arm64", + "@esbuild/win32-ia32", + "@esbuild/win32-x64" + ], + "scripts": true, + "bin": true + }, + "jsesc@3.1.0": { + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "bin": true + } + } +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..360fd37 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,43 @@ +[build-system] +requires = ["hatchling", "hatch-build-scripts"] +build-backend = "hatchling.build" + +[project] +name = "yt-dlp-jsc-deno" +version = "0.0.1" +description = "JavaScript Challenge Provider for yt-dlp using Deno" +readme = "README.md" +requires-python = ">=3.8" +license = "Unlicense" +keywords = [] +authors = [ + { name = "Simon Sawicki", email = "contact@grub4k.dev" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dependencies = [] + +[project.urls] +Documentation = "https://github.com/yt-dlp/yt-dlp-jsc-deno#readme" +Issues = "https://github.com/yt-dlp/yt-dlp-jsc-deno/issues" +Source = "https://github.com/yt-dlp/yt-dlp-jsc-deno" + +[[tool.hatch.build.hooks.build-scripts.scripts]] +out_dir = "yt_dlp_jsc_deno" +commands = [ + "deno task bundle", +] +artifacts = [ + "dist/jsc-deno.js", +] diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..431803b --- /dev/null +++ b/src/main.ts @@ -0,0 +1,44 @@ +import { getSolvers } from "./solvers.ts"; +import { isOneOf } from "./utils.ts"; + +if (Deno.stdin.isTerminal()) { + console.error("Expected player content on stdin"); + Deno.exit(9); +} +const stdin = await new Response(Deno.stdin.readable).text(); +if (!stdin) { + console.error("Expected player content on stdin"); + Deno.exit(9); +} +if (Deno.args.length < 1) { + console.error("Expected one argument, `solver nsig:... nsig:... sig:...`"); + Deno.exit(9); +} + +const solveList: { + mode: "nsig" | "sig"; + value: string; +}[] = []; +for (const arg of Deno.args) { + const split = arg.split(":", 2); + if (split.length === 1) { + console.error(`Missing mode: ${arg}`); + Deno.exit(1); + } + const [mode, value] = split; + if (!isOneOf(mode, "sig", "nsig")) { + console.error(`Invalid mode, expected "nsig:..." or "sig:...": ${mode}`); + Deno.exit(1); + } + solveList.push({ mode, value }); +} + +const solvers = getSolvers(stdin); +for (const solve of solveList) { + const solver = solvers[solve.mode]; + if (!solver) { + console.error(`Did not set ${solve.mode} function`); + Deno.exit(1); + } + console.log(solver(solve.value)); +} diff --git a/src/nsig.ts b/src/nsig.ts new file mode 100644 index 0000000..a93d502 --- /dev/null +++ b/src/nsig.ts @@ -0,0 +1,113 @@ +import { + type ArrowFunctionExpression, + type Node, +} from "npm:@babel/types@7.28.2"; +import { matchesStructure } from "./utils.ts"; +import { type DeepPartial } from "./types.ts"; + +const identifier: DeepPartial = { + type: "AssignmentExpression", + operator: "=", + left: { + type: "Identifier", + }, + right: { + type: "FunctionExpression", + params: [{}], + body: { + type: "BlockStatement", + body: [ + { + type: "ReturnStatement", + // { + argument: + // or: [ + { + type: "CallExpression", + callee: { + type: "MemberExpression", + object: { + type: "Identifier", + // XXX: get switch function identifier, use here + // name: "Lb", + }, + property: { + type: "MemberExpression", + object: { + type: "Identifier", + // XXX: get global string store identifier, use here + // name: "Y", + }, + property: { + type: "NumericLiteral", + }, + }, + }, + arguments: [ + { type: "ThisExpression" }, + { type: "NumericLiteral" }, + { + type: "Identifier", + // XXX: get parameter name, use here + // name: "Y", + }, + ], + }, + // XXX: possible to be a direct call, ignore that for now + // { + // type: "CallExpression", + // callee: { + // type: "Identifier", + // // XXX: get global string store identifier, use here + // // name: "Y", + // }, + // arguments: [ + // { type: "NumericLiteral" }, + // { + // type: "Identifier", + // // XXX: get parameter name, use here + // // name: "Y", + // }, + // ], + // }, + // ], + // }, + }, + ], + }, + }, +}; + +export function extract(node: Node): ArrowFunctionExpression | null { + if (!matchesStructure(node, identifier)) { + return null; + } + if (node.type !== "AssignmentExpression" || node.left.type !== "Identifier") { + return null; + } + // TODO: verify identifiers here + return { + type: "ArrowFunctionExpression", + params: [ + { + type: "Identifier", + name: "nsig", + }, + ], + async: false, + expression: true, + body: { + type: "CallExpression", + callee: { + type: "Identifier", + name: node.left.name, + }, + arguments: [ + { + type: "Identifier", + name: "nsig", + }, + ], + }, + }; +} diff --git a/src/sig.ts b/src/sig.ts new file mode 100644 index 0000000..f288b8c --- /dev/null +++ b/src/sig.ts @@ -0,0 +1,122 @@ +import { + type ArrowFunctionExpression, + type Node, +} from "npm:@babel/types@7.28.2"; +import { matchesStructure } from "./utils.ts"; +import { type DeepPartial } from "./types.ts"; + +const identifier: DeepPartial = { + type: "AssignmentExpression", + operator: "=", + left: { + type: "Identifier", + }, + right: { + type: "FunctionExpression", + params: [{}, {}, {}], + body: { + type: "BlockStatement", + body: [ + { type: "ExpressionStatement" }, + { type: "ExpressionStatement" }, + { type: "ExpressionStatement" }, + { type: "ExpressionStatement" }, + { + type: "ExpressionStatement", + expression: { + type: "LogicalExpression", + left: { + type: "Identifier", + // name: "M", + }, + operator: "&&", + right: { + type: "SequenceExpression", + expressions: [ + { + type: "AssignmentExpression", + operator: "=", + left: { + type: "Identifier", + }, + right: { + type: "CallExpression", + callee: { + type: "Identifier", + }, + arguments: [ + { type: "NumericLiteral" }, + { + type: "CallExpression", + callee: { + type: "Identifier", + name: "decodeURIComponent", + }, + arguments: [{ type: "Identifier" }], + }, + ], + }, + }, + { + type: "CallExpression", + }, + ], + extra: { + parenthesized: true, + }, + }, + }, + }, + { type: "ReturnStatement" }, + ], + }, + }, +}; + +export function extract(node: Node): ArrowFunctionExpression | null { + if (!matchesStructure(node, identifier)) { + return null; + } + // shut the type checker up + if ( + node.type !== "AssignmentExpression" || + node.right.type !== "FunctionExpression" || + node.right.body.body[4].type !== "ExpressionStatement" || + node.right.body.body[4].expression.type !== "LogicalExpression" || + node.right.body.body[4].expression.right.type !== "SequenceExpression" || + node.right.body.body[4].expression.right.expressions[0].type !== + "AssignmentExpression" + ) { + return null; + } + const call = node.right.body.body[4].expression.right.expressions[0].right; + if (call.type !== "CallExpression" || call.callee.type !== "Identifier") { + return null; + } + // TODO: verify identifiers here + return { + type: "ArrowFunctionExpression", + params: [ + { + type: "Identifier", + name: "sig", + }, + ], + async: false, + expression: true, + body: { + type: "CallExpression", + callee: { + type: "Identifier", + name: call.callee.name, + }, + arguments: [ + call.arguments[0], + { + type: "Identifier", + name: "sig", + }, + ], + }, + }; +} diff --git a/src/solvers.ts b/src/solvers.ts new file mode 100755 index 0000000..ab5c136 --- /dev/null +++ b/src/solvers.ts @@ -0,0 +1,103 @@ +import { parse } from "npm:@babel/parser@7.28.3"; +import { generate } from "npm:@babel/generator@7.28.3"; +import { type ArrowFunctionExpression } from "npm:@babel/types@7.28.2"; +import { getFunctionNodes } from "./utils.ts"; +import { extract as extractSig } from "./sig.ts"; +import { extract as extractNsig } from "./nsig.ts"; + +function setup() { + // @ts-ignore: This is used in the babel generated js + globalThis.XMLHttpRequest = { prototype: {} }; + // deno-lint-ignore no-unused-vars + const window = Object.assign(Object.create(null), globalThis); + // deno-lint-ignore no-unused-vars + const document = {}; +} + +// helper functions +export function getSolvers(data: string): { + nsig: ((val: string) => string) | null; + sig: ((val: string) => string) | null; +} { + const ast = parse(data, { + attachComment: false, + }); + const body = ast.program.body; + if (body.length !== 2 || body[1].type !== "ExpressionStatement") { + throw "unexpected structure"; + } + const func = body[1]; + if ( + func.expression.type !== "CallExpression" || + func.expression.callee.type !== "FunctionExpression" + ) { + throw "unexpected structure"; + } + const found = { + nsig: [] as ArrowFunctionExpression[], + sig: [] as ArrowFunctionExpression[], + }; + const plainExpressions = func.expression.callee.body.body.filter( + (node, idx) => { + if (idx === 0) { + // Ignore `var window = this;` + return false; + } + if (node.type === "ExpressionStatement") { + if (node.expression.type === "AssignmentExpression") { + const nsig = extractNsig(node.expression); + if (nsig) { + found.nsig.push(nsig); + } + const sig = extractSig(node.expression); + if (sig) { + found.sig.push(sig); + } + return true; + } + return node.expression.type === "StringLiteral"; + } + return true; + }, + ); + func.expression.callee.body.body = plainExpressions; + + for (const [name, options] of Object.entries(found)) { + if (options.length !== 1) { + continue; + } + plainExpressions.push({ + type: "ExpressionStatement", + expression: { + type: "AssignmentExpression", + operator: "=", + left: { + type: "MemberExpression", + computed: false, + object: { + type: "Identifier", + name: "_result", + }, + property: { + type: "Identifier", + name: name, + }, + }, + right: options[0], + }, + }); + } + + ast.program.body.splice(0, 0, ...getFunctionNodes(setup)); + + const { code } = generate(ast, { + comments: false, + compact: false, + concise: false, + }); + + // evil eval!!?! + const resultObj = { nsig: null, sig: null }; + Function("_result", code)(resultObj); + return resultObj; +} diff --git a/src/solvers_test.ts b/src/solvers_test.ts new file mode 100644 index 0000000..71c022e --- /dev/null +++ b/src/solvers_test.ts @@ -0,0 +1,22 @@ +import { assertStrictEquals } from "jsr:@std/assert@1"; +import { getSolvers } from "./solvers.ts"; +import { players, tests } from "../tests/tests.ts"; +import { getCachePath } from "../tests/utils.ts"; + +for (const test of tests) { + for (const variant of test.variants ?? players.keys()) { + const path = getCachePath(test.player, variant); + Deno.test(`${test.player} ${variant}`, async (t) => { + const content = await Deno.readTextFile(path); + const solvers = getSolvers(content); + for (const mode of ["nsig", "sig"] as const) { + for (const step of test[mode] || []) { + await t.step(`${step.input} (${mode})`, () => { + const got = solvers[mode]?.(step.input); + assertStrictEquals(got, step.expected); + }); + } + } + }); + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..8592029 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,4 @@ +export type DeepPartial = T extends object ? { + [P in keyof T]?: DeepPartial; + } + : T; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..d95b883 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,49 @@ +import { parse } from "npm:@babel/parser@7.28.3"; +import { type Node, type Statement } from "npm:@babel/types@7.28.2"; +import { type DeepPartial } from "./types.ts"; + +export function matchesStructure( + obj: Node | Node[], + structure: DeepPartial | readonly DeepPartial[], +): obj is T { + if (Array.isArray(structure)) { + if (!Array.isArray(obj)) { + return false; + } + return ( + structure.length === obj.length && + structure.every((value, index) => matchesStructure(obj[index], value)) + ); + } + if (typeof structure === "object") { + if (!obj) { + return !structure; + } + if ("or" in structure) { + // Allow `{ or: [a, b] }` so we can handle some special cases + return (structure.or! as DeepPartial[]).some((node) => + matchesStructure(obj, node) + ); + } + for (const [key, value] of Object.entries(structure)) { + if (!matchesStructure(obj[key as keyof typeof obj], value)) { + return false; + } + } + return true; + } + return structure === obj; +} + +export function getFunctionNodes(f: (...a: unknown[]) => void): Statement[] { + const func = parse(f.toString()).program.body[0]; + if (func.type === "FunctionDeclaration") { + return func.body.body; + } + console.error("failed to parse function into nodes"); + return []; +} + +export function isOneOf(value: unknown, ...of: readonly T[]): value is T { + return of.includes(value as T); +} diff --git a/tests/download.ts b/tests/download.ts new file mode 100644 index 0000000..ed86204 --- /dev/null +++ b/tests/download.ts @@ -0,0 +1,27 @@ +import { exists } from "jsr:@std/fs/exists"; + +import { tests, players } from "./tests.ts"; +import { getCachePath } from "./utils.ts"; + +for (const test of tests) { + const variants = test.variants ?? players.keys(); + for (const variant of variants) { + const path = getCachePath(test.player, variant); + if (await exists(path)) { + continue; + } + const playerPath = players.get(variant); + const url = `https://www.youtube.com/s/player/${test.player}/${playerPath}`; + console.log("Requesting", url); + const response = await fetch(url); + if (!response.ok) { + console.error(`Failed to request ${variant} player for ${test.player}`); + continue; + } + const file = await Deno.open(path, { + createNew: true, + write: true, + }); + response.body!.pipeTo(file.writable); + } +} diff --git a/tests/players/.gitignore b/tests/players/.gitignore new file mode 100644 index 0000000..120f485 --- /dev/null +++ b/tests/players/.gitignore @@ -0,0 +1,2 @@ +* +!/.gitignore diff --git a/tests/tests.ts b/tests/tests.ts new file mode 100644 index 0000000..dc87f3f --- /dev/null +++ b/tests/tests.ts @@ -0,0 +1,71 @@ +type Step = { + input: string; + expected: string; +}; + +export const tests: { + player: string; + variants?: Variant[]; + nsig?: Step[]; + sig?: Step[]; +}[] = [ + { + player: "3d3ba064", + variants: ["tce"], + nsig: [ + { input: "ZdZIqFPQK-Ty8wId", expected: "qmtUsIz04xxiNW" }, + { input: "4GMrWHyKI5cEvhDO", expected: "N9gmEX7YhKTSmw" }, + ], + sig: [ + { + input: + "gN7a-hudCuAuPH6fByOk1_GNXN0yNMHShjZXS2VOgsEItAJz0tipeavEOmNdYN-wUtcEqD3bCXjc0iyKfAyZxCBGgIARwsSdQfJ2CJtt", + expected: + "ttJC2JfQdSswRAIgGBCxZyAfKyi0cjXCb3gqEctUw-NYdNmOEvaepit0zJAtIEsgOV2SXZjhSHMNy0NXNG_1kNyBf6HPuAuCduh-a7O", + }, + ], + }, + { + player: "5ec65609", + variants: ["tce"], + nsig: [{ input: "0eRGgQWJGfT5rFHFj", expected: "4SvMpDQH-vBJCw" }], + sig: [ + { + input: + "AAJAJfQdSswRQIhAMG5SN7-cAFChdrE7tLA6grH0rTMICA1mmDc0HoXgW3CAiAQQ4=CspfaF_vt82XH5yewvqcuEkvzeTsbRuHssRMyJQ=I", + expected: + "AJfQdSswRQIhAMG5SN7-cAFChdrE7tLA6grI0rTMICA1mmDc0HoXgW3CAiAQQ4HCspfaF_vt82XH5yewvqcuEkvzeTsbRuHssRMyJQ==", + }, + ], + }, + { + player: "6742b2b9", + variants: ["tce"], + nsig: [ + { input: "_HPB-7GFg1VTkn9u", expected: "qUAsPryAO_ByYg" }, + { input: "K1t_fcB6phzuq2SF", expected: "Y7PcOt3VE62mog" }, + ], + sig: [ + { + input: + "MMGZJMUucirzS_SnrSPYsc85CJNnTUi6GgR5NKn-znQEICACojE8MHS6S7uYq4TGjQX_D4aPk99hNU6wbTvorvVVMgIARwsSdQfJAA", + expected: + "AJfQdSswRAIgMVVvrovTbw6UNh99kPa4D_XQjGT4qYu7S6SHM8EjoCACIEQnz-nKN5RgG6iUTnNJC58csYPSrnS_SzricuUMJZGM", + }, + ], + }, +]; + +export const players = new Map([ + ["main", "player_ias.vflset/en_US/base.js"], + ["tcc", "player_ias_tcc.vflset/en_US/base.js"], + ["tce", "player_ias_tce.vflset/en_US/base.js"], + ["es5", "player_es5.vflset/en_US/base.js"], + ["es6", "player_es6.vflset/en_US/base.js"], + ["tv", "tv-player-ias.vflset/tv-player-ias.js"], + ["tv_es6", "tv-player-es6.vflset/tv-player-es6.js"], + ["phone", "player-plasma-ias-phone-en_US.vflset/base.js"], + ["tablet", "player-plasma-ias-tablet-en_US.vflset/base.js"], +] as const); + +export type Variant = (typeof players) extends Map ? T : never; diff --git a/tests/utils.ts b/tests/utils.ts new file mode 100644 index 0000000..e7478d9 --- /dev/null +++ b/tests/utils.ts @@ -0,0 +1,5 @@ +import { type Variant } from "./tests.ts"; + +export function getCachePath(player: string, variant: Variant) { + return `tests/players/${player}_${variant}.js`; +} diff --git a/yt_dlp_jsc_deno/__init__.py b/yt_dlp_jsc_deno/__init__.py new file mode 100644 index 0000000..d5e165e --- /dev/null +++ b/yt_dlp_jsc_deno/__init__.py @@ -0,0 +1,20 @@ +import importlib.resources +import importlib.metadata + +import yt_dlp_jsc_deno + +_name = "dist/jsc-deno.js" + +version = importlib.metadata.version(yt_dlp_jsc_deno.__name__) + + +def exists() -> bool: + return importlib.resources.is_resource(yt_dlp_jsc_deno, _name) + + +def read() -> str: + return importlib.resources.read_text(yt_dlp_jsc_deno, _name) + + +def path(): + return importlib.resources.path(yt_dlp_jsc_deno, _name)