Initial POC

This commit is contained in:
Simon Sawicki
2025-08-24 08:01:07 +02:00
commit a54e90cd77
18 changed files with 923 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/dist
*.py[cd]
/yt_dlp_jsc_deno/dist
/yt_dlp_jsc_deno/__about__.py

9
README.md Normal file
View File

@@ -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.

12
bundle.ts Normal file
View File

@@ -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();

10
deno.jsonc Normal file
View File

@@ -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"
}
}

263
deno.lock generated Normal file
View File

@@ -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
}
}
}

43
pyproject.toml Normal file
View File

@@ -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",
]

44
src/main.ts Normal file
View File

@@ -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));
}

113
src/nsig.ts Normal file
View File

@@ -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<Node> = {
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",
},
],
},
};
}

122
src/sig.ts Normal file
View File

@@ -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<Node> = {
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",
},
],
},
};
}

103
src/solvers.ts Executable file
View File

@@ -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;
}

22
src/solvers_test.ts Normal file
View File

@@ -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);
});
}
}
});
}
}

4
src/types.ts Normal file
View File

@@ -0,0 +1,4 @@
export type DeepPartial<T> = T extends object ? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;

49
src/utils.ts Normal file
View File

@@ -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<T extends Node>(
obj: Node | Node[],
structure: DeepPartial<T> | readonly DeepPartial<T>[],
): 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<Node>[]).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<T>(value: unknown, ...of: readonly T[]): value is T {
return of.includes(value as T);
}

27
tests/download.ts Normal file
View File

@@ -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);
}
}

2
tests/players/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!/.gitignore

71
tests/tests.ts Normal file
View File

@@ -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<infer T, unknown> ? T : never;

5
tests/utils.ts Normal file
View File

@@ -0,0 +1,5 @@
import { type Variant } from "./tests.ts";
export function getCachePath(player: string, variant: Variant) {
return `tests/players/${player}_${variant}.js`;
}

View File

@@ -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)