16 Commits
0.1.0 ... 0.3.1

Author SHA1 Message Date
bashonly
4b4ac2b896 Include test data directory in sdist (#27) 2025-11-06 22:09:05 +00:00
sepro
2655b1f55f Fix es6 and tv_es6 n func extraction (#26) 2025-10-28 21:54:48 +01:00
Simon Sawicki
877164a326 Add build time lockfile for deno (#25) 2025-10-28 00:41:18 +01:00
bashonly
25b77b7310 Fix encoding for Windows (#22) 2025-10-24 13:33:13 +13:00
bashonly
57fe708cf4 Do not run CI on tag pushes (#21) 2025-10-23 22:18:55 +00:00
bashonly
508dddae12 Finalize for release (#20) 2025-10-23 21:29:29 +00:00
bashonly
32e6af5fb2 Cache player JS test data on GitHub Actions (#19) 2025-10-23 21:18:04 +00:00
Simon Sawicki
e0560ee403 Use importlib.resources.files() api (#15) 2025-10-23 21:32:43 +02:00
sepro
b57ce18965 Add formatting/linting CI (#18) 2025-10-23 21:24:31 +02:00
bashonly
52a4f9d19a Add Python module test (#17) 2025-10-23 17:59:59 +00:00
bashonly
5d7bf090bb CI, build and documentation fixes (#14) 2025-10-22 23:01:24 +00:00
sepro
6a73fa37ba Fix sig in player 638ec5c6 (#10) 2025-10-22 20:48:45 +02:00
bashonly
bf12d399b2 Add CI testing (#11)
Co-authored-by: sepro <sepro@sepr0.com>
2025-10-22 04:53:57 +00:00
bashonly
c9bbdcb445 Improve build and test instructions in README 2025-10-21 22:46:22 -05:00
sepro
4d0d39eca4 Fix build hook for Node and Bun 2025-10-21 22:46:22 -05:00
N/Ame
398f81bbed Use a local window variable (#9) 2025-10-22 01:14:14 +02:00
22 changed files with 1961 additions and 88 deletions

377
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,377 @@
name: CI
on:
push:
branches:
- '**'
paths-ignore:
- 'README.md'
- 'LICENSE'
pull_request:
paths-ignore:
- 'README.md'
- 'LICENSE'
permissions:
contents: read
concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
ruff-format:
name: Ruff format check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: astral-sh/ruff-action@v3
with:
args: "check --output-format github"
ruff-lint:
name: Ruff linting check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: astral-sh/ruff-action@v3
with:
args: "format --check --diff"
prettier:
name: Prettier check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install Deno v2.x (latest)
uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Install Deno requirements
run: |
deno install
- name: Run Prettier check
run: |
deno task fmt:check
eslint:
name: ESLint check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install Deno v2.x (latest)
uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Install Deno requirements
run: |
deno install
- name: Run ESLint check
run: |
deno task lint
python_tests:
name: Python tests
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
runner: [ubuntu-latest, windows-latest]
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', pypy-3.11]
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install Deno v2.x (latest)
uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Build project
run: |
# `pip install -e` omits the force-included JS, so use `build` instead
python -m pip install -U build
python -m build
- name: Unpack wheel (Linux)
if: matrix.runner == 'ubuntu-latest'
run: |
unzip -u dist/yt_dlp_ejs-*.whl "yt_dlp_ejs/*"
- name: Unpack wheel (Windows)
if: matrix.runner == 'windows-latest'
shell: pwsh
run: |
Expand-Archive -Path dist/yt_dlp_ejs-*.whl -DestinationPath ./ -Force
- name: Run Python tests
timeout-minutes: 5
run: |
python -Werror -m unittest
prepare:
name: Prepare JS runtime tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install Deno v2.x (latest)
uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Install Deno requirements
run: |
deno install
- name: Build control bundle
run: |
deno task bundle
- name: Generate bundle hashes
run: |
pushd dist
sha256sum -- yt.solver.*.js | tee SHA2-256SUMS
popd
- name: Upload bundle hashes
uses: actions/upload-artifact@v4
with:
name: bundle-hashes
path: |
dist/SHA2-256SUMS
compression-level: 0
- name: Cache player JS files
uses: actions/cache@v4
env:
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
with:
path: |
src/yt/solver/test/players
key: test-player-js-${{ hashFiles('src/yt/solver/test/tests.ts') }}
- name: Download player JS files
run: |
deno run \
--no-prompt \
--allow-read=src/yt/solver/test/players/ \
--allow-write=src/yt/solver/test/players/ \
--allow-net=www.youtube.com \
--allow-sys=uid \
src/yt/solver/test/download.ts
- name: Upload player JS artifact
uses: actions/upload-artifact@v4
with:
name: player-js
path: |
src/yt/solver/test/players/*
compression-level: 0
deno_build:
name: Test Deno build
needs: [prepare]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install Deno
uses: denoland/setup-deno@v2
with:
deno-version: "2.0.0" # minimum supported version
- uses: actions/setup-python@v6
with:
python-version: "3.10" # minimum supported version
- name: Install Python requirements
run: |
python -m pip install -U build
- name: Test Deno build
run: |
python -m build
- name: Verify artifact contents
run: |
tar -tvzf dist/yt_dlp_ejs-*.tar.gz
unzip -l dist/yt_dlp_ejs-*.whl | tee .wheel_contents
grep -q 'yt_dlp_ejs/yt/solver/core\.min\.js' .wheel_contents
grep -q 'yt_dlp_ejs/yt/solver/lib\.min\.js' .wheel_contents
- name: Install Deno requirements
run: |
deno install
- name: Bundle with Deno
run: |
deno task bundle
- name: Download bundle hashes
uses: actions/download-artifact@v5
with:
path: dist
name: bundle-hashes
- name: Verify bundle hashes
run: |
cd dist
sha256sum -c SHA2-256SUMS
deno_tests:
name: Run Deno tests
needs: [prepare]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install Deno
uses: denoland/setup-deno@v2
with:
deno-version: "2.0.0" # minimum supported version
- name: Install Deno requirements
run: |
deno install
- name: Download player JS artifact
uses: actions/download-artifact@v5
with:
path: src/yt/solver/test/players
name: player-js
- name: Run Deno tests
run: |
deno test \
--no-prompt \
--allow-read=src/yt/solver/test/players/
bun_build:
name: Test Bun build
needs: [prepare]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.0.31" # minimum supported version
- uses: actions/setup-python@v6
with:
python-version: "3.10" # minimum supported version
- name: Install Python requirements
run: |
python -m pip install -U build
- name: Test Bun build
run: |
python -m build
- name: Verify artifact contents
run: |
tar -tvzf dist/yt_dlp_ejs-*.tar.gz
unzip -l dist/yt_dlp_ejs-*.whl | tee .wheel_contents
grep -q 'yt_dlp_ejs/yt/solver/core\.min\.js' .wheel_contents
grep -q 'yt_dlp_ejs/yt/solver/lib\.min\.js' .wheel_contents
- name: Install Bun requirements
run: |
bun install
- name: Bundle with Bun
run: |
bun --bun run bundle
- name: Download bundle hashes
uses: actions/download-artifact@v5
with:
path: dist
name: bundle-hashes
- name: Verify bundle hashes
run: |
cd dist
sha256sum -c SHA2-256SUMS
bun_tests:
name: Run Bun tests
needs: [prepare]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.2.11" # XXX: We support 1.0.31, but test suite requires 1.2.11+
- name: Install Bun requirements
run: |
bun install
- name: Download player JS artifact
uses: actions/download-artifact@v5
with:
path: src/yt/solver/test/players
name: player-js
- name: Run Bun tests
run: |
bun test
node_build:
name: Test Node build
needs: [prepare]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "20.0" # minimum supported version
- uses: actions/setup-python@v6
with:
python-version: "3.10" # minimum supported version
- name: Install Python requirements
run: |
python -m pip install -U build
- name: Test Node build
run: |
python -m build
- name: Verify artifact contents
run: |
tar -tvzf dist/yt_dlp_ejs-*.tar.gz
unzip -l dist/yt_dlp_ejs-*.whl | tee .wheel_contents
grep -q 'yt_dlp_ejs/yt/solver/core\.min\.js' .wheel_contents
grep -q 'yt_dlp_ejs/yt/solver/lib\.min\.js' .wheel_contents
- name: Install Node requirements
run: |
npm install
- name: Bundle with Node
run: |
npm run bundle
- name: Download bundle hashes
uses: actions/download-artifact@v5
with:
path: dist
name: bundle-hashes
- name: Verify bundle hashes
run: |
cd dist
sha256sum -c SHA2-256SUMS
node_tests:
name: Run Node tests
needs: [prepare]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: "22.18" # XXX: We support 20.0, but test suite requires 22.18+
- name: Install Node requirements
run: |
npm install
- name: Download player JS artifact
uses: actions/download-artifact@v5
with:
path: src/yt/solver/test/players
name: player-js
- name: Run Node tests
run: |
node --test
all_passed:
needs:
- ruff-format
- ruff-lint
- prettier
- eslint
- python_tests
- deno_build
- deno_tests
- bun_build
- bun_tests
- node_build
- node_tests
runs-on: ubuntu-latest
steps:
- name: All checks passed
run: |
echo "All checks passed!"

View File

@@ -37,7 +37,7 @@ jobs:
compression-level: 0
- name: Build JavaScript artifacts
run: |
deno install
deno install --frozen
deno task bundle
- name: Upload JavaScript artifacts
uses: actions/upload-artifact@v4
@@ -88,7 +88,7 @@ jobs:
run: |
gh release create "${TAG}" \
--title "yt-dlp-ejs ${TAG}" \
--notes-from-tag \
--generate-notes \
--verify-tag \
dist/yt.solver.*.js \
dist/yt_dlp_ejs-*.whl \

2
.gitignore vendored
View File

@@ -3,6 +3,6 @@
/yt_dlp_ejs/_version.py
/node_modules
/bun.lock
/deno.lock
/package-lock.json
/.idea
/.venv

View File

@@ -1,24 +1,21 @@
# yt-dlp-ejs
> [!CAUTION]
> This is currently in development
External JavaScript for yt-dlp supporting many runtimes
## Manual Installation
In the yt-dlp repository, install the python package, either directly or from url:
Install ejs into the same environment as yt-dlp:
```console
pip install git+https://github.com/yt-dlp/ejs@main
pip install -U yt-dlp-ejs
```
## Development
While this project does pin its dependencies,
it does not use lockfiles or enforce a particular package manager.
it only provides a lockfile for building with `deno`.
You may install dependencies using any compatible package manager.
If you notice differences between different runtimes builds
If you notice differences between different runtimes' builds
please open an issue [here](<https://github.com/yt-dlp/ejs/issues/new>).
### Build
@@ -26,13 +23,52 @@ please open an issue [here](<https://github.com/yt-dlp/ejs/issues/new>).
To build the Python package you need a PEP518 compatible builder.
The build hook will automatically invoke `deno`, `bun` or `node` as required.
Alternatively, to only build the JavaScript files you can run the `bundle` script manually.
Alternatively, to only build the JavaScript files you can run the `bundle` script manually:
```bash
# Deno:
deno install --frozen
deno task bundle
# Bun:
bun install
bun --bun run bundle
# Node:
npm install
npm run bundle
```
### Tests
First, to download the player files, run `src/yt/solver/test/download.ts`.
First, make sure the project's dependencies are installed and download the player JS files:
After running that once, use any of `deno test`, `bun test` or `node --test`.
```bash
# Deno:
deno install --frozen
deno run src/yt/solver/test/download.ts
# Bun:
bun install
bun --bun run src/yt/solver/test/download.ts
# Node 22.6+:
npm install
node --experimental-strip-types src/yt/solver/test/download.ts
```
Then the tests can be run:
```bash
# Deno
deno test
# Bun
bun test
# Node
node --test
```
## Licensing

1262
deno.lock generated Normal file

File diff suppressed because it is too large Load Diff

14
eslint.config.js Normal file
View File

@@ -0,0 +1,14 @@
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
plugins: { js },
extends: ["js/recommended"],
languageOptions: { globals: globals.node },
},
tseslint.configs.recommended,
]);

View File

@@ -2,7 +2,6 @@ import os
import shutil
import subprocess
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
@@ -11,28 +10,33 @@ class CustomBuildHook(BuildHookInterface):
if shutil.which("deno"):
print("Building with deno...", flush=True)
os.environ["DENO_NO_UPDATE_CHECK"] = "1"
subprocess.run(["deno", "install"], check=True)
subprocess.run(["deno", "install", "--frozen"], check=True)
subprocess.run(["deno", "task", "bundle"], check=True)
elif shutil.which("bun"):
print("Building with bun...", flush=True)
subprocess.run(["bun", "install", "--frozen-lockfile"], check=True)
subprocess.run(["bun", "run", "bundle"], check=True)
subprocess.run(["bun", "install"], check=True)
subprocess.run(["bun", "--bun", "run", "bundle"], check=True)
elif shutil.which("npm"):
print("Building with npm...", flush=True)
# npm is a batch file (`npm.cmd`) on windows, which requires `shell=True`
requires_shell = os.name == "nt"
subprocess.run(["npm", "ci"], check=True, shell=requires_shell)
subprocess.run(["npm", "install"], check=True, shell=requires_shell)
subprocess.run(["npm", "run", "bundle"], check=True, shell=requires_shell)
else:
raise RuntimeError(
"One of 'deno', 'bun', or 'npm' could not be found. "
"Please install one of them to proceed with the build.")
"Please install one of them to proceed with the build."
)
build_data["force_include"]["dist/yt.solver.core.min.js"] = "yt_dlp_ejs/yt/solver/core.min.js"
build_data["force_include"]["dist/yt.solver.lib.min.js"] = "yt_dlp_ejs/yt/solver/lib.min.js"
build_data["force_include"].update(
{
"dist/yt.solver.core.min.js": "yt_dlp_ejs/yt/solver/core.min.js",
"dist/yt.solver.lib.min.js": "yt_dlp_ejs/yt/solver/lib.min.js",
}
)
def clean(self, versions):
shutil.rmtree('node_modules', ignore_errors=True)
shutil.rmtree("node_modules", ignore_errors=True)

View File

@@ -3,21 +3,27 @@
"type": "module",
"scripts": {
"bundle": "rollup -c",
"fmt": "prettier --write \"src/**.ts\" \"package.json\" \"rollup.config.js\" \"run.ts\""
"fmt": "prettier --write \"src/**.ts\" \"package.json\" \"rollup.config.js\" \"run.ts\" \"eslint.config.js\"",
"fmt:check": "prettier --check \"src/**.ts\" \"package.json\" \"rollup.config.js\" \"run.ts\" \"eslint.config.js\"",
"lint": "eslint src"
},
"dependencies": {
"astring": "1.9.0",
"meriyah": "6.1.4"
},
"devDependencies": {
"@eslint/js": "9.38.0",
"@rollup/plugin-node-resolve": "16.0.3",
"@rollup/plugin-sucrase": "5.0.2",
"@rollup/plugin-terser": "0.4.4",
"rollup-plugin-license": "3.6.0",
"@types/bun": "1.3.0",
"@types/deno": "2.5.0",
"@types/node": "24.8.1",
"eslint": "9.38.0",
"globals": "16.4.0",
"prettier": "3.6.2",
"rollup": "4.52.5",
"prettier": "3.6.2"
"rollup-plugin-license": "3.6.0",
"typescript-eslint": "8.46.2"
}
}

View File

@@ -30,6 +30,11 @@ classifiers = [
]
dependencies = []
[dependency-groups]
dev = [
"ruff>=0.14.1",
]
[project.urls]
Documentation = "https://github.com/yt-dlp/ejs#readme"
Issues = "https://github.com/yt-dlp/ejs/issues"
@@ -42,9 +47,43 @@ source = "vcs"
exclude = [
"/.github/**",
"/src/yt/solver/test/players/*",
"!/src/yt/solver/test/players/.gitignore",
]
[tool.hatch.build.targets.wheel]
packages = ["yt_dlp_ejs"]
[tool.hatch.build.hooks.vcs]
version-file = "yt_dlp_ejs/_version.py"
[tool.hatch.build.targets.wheel.hooks.custom]
[tool.ruff.lint]
select = [
"C4",
"E",
"F",
"I",
"PLC",
"PLE",
"PLW",
"PYI",
"RET",
"RUF",
"SIM",
"TD",
"TID",
"W",
]
ignore = [
"TD003",
"E402",
"E501",
"PLR09",
]
[tool.ruff.lint.isort]
force-single-line = true
[tool.ruff.lint.flake8-tidy-imports]
ban-relative-imports = "all"

View File

@@ -31,7 +31,9 @@ function printHash() {
for (const [fileName, assetInfo] of Object.entries(bundle)) {
if (assetInfo.code) {
try {
const digest = createHash("sha3-512").update(assetInfo.code).digest("hex");
const digest = createHash("sha3-512")
.update(assetInfo.code)
.digest("hex");
console.log(`SHA3-512 for ${assetInfo.fileName}: ${digest}`);
} catch (err) {
console.error(`Error hashing ${fileName}:`, err.message);

View File

@@ -1,7 +1,16 @@
export type DeepPartial<T> = T extends object
? Or<{
[P in keyof T]?: DeepPartial<T[P]>;
}>
: Or<T>;
type DP<T> = T extends (infer U)[]
? DeepPartial<U>[]
: T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;
type Or<T> = T | { or: T[] };
type ValueOf<T> = T extends (infer U)[]
? U
: T extends object
? T[keyof T]
: never;
export type DeepPartial<T> =
| DP<T>
| { or: DP<T>[] }
| { anykey: DP<ValueOf<T>>[] };

View File

@@ -22,6 +22,13 @@ export function matchesStructure<T extends ESTree.Node>(
// Handle `{ or: [a, b] }`
return structure.or.some((node) => matchesStructure(obj, node));
}
if ("anykey" in structure && Array.isArray(structure.anykey)) {
// Handle `{ anykey: [a, b] }`
const haystack = Array.isArray(obj) ? obj : Object.values(obj);
return structure.anykey.every((value) =>
haystack.some((el) => matchesStructure(el, value)),
);
}
for (const [key, value] of Object.entries(structure)) {
if (!matchesStructure(obj[key as keyof typeof obj], value)) {
return false;

View File

@@ -2,26 +2,50 @@ import { type ESTree } from "meriyah";
import { matchesStructure } from "../../utils.ts";
import { type DeepPartial } from "../../types.ts";
const identifier: DeepPartial<ESTree.VariableDeclaration> = {
type: "VariableDeclaration",
kind: "var",
declarations: [
const identifier: DeepPartial<ESTree.Node> = {
or: [
{
type: "VariableDeclarator",
id: {
type: "Identifier",
},
init: {
type: "ArrayExpression",
elements: [
type: "VariableDeclaration",
kind: "var",
declarations: {
anykey: [
{
type: "Identifier",
type: "VariableDeclarator",
id: {
type: "Identifier",
},
init: {
type: "ArrayExpression",
elements: [
{
type: "Identifier",
},
],
},
},
],
},
},
{
type: "ExpressionStatement",
expression: {
type: "AssignmentExpression",
left: {
type: "Identifier",
},
operator: "=",
right: {
type: "ArrayExpression",
elements: [
{
type: "Identifier",
},
],
},
},
},
],
};
} as const;
const catchBlockBody = [
{
@@ -92,23 +116,37 @@ export function extract(
return null;
}
if (node.type !== "VariableDeclaration") {
return null;
if (node.type === "VariableDeclaration") {
for (const declaration of node.declarations) {
if (
declaration.type !== "VariableDeclarator" ||
!declaration.init ||
declaration.init.type !== "ArrayExpression" ||
declaration.init.elements.length !== 1
) {
continue;
}
const [firstElement] = declaration.init.elements;
if (firstElement && firstElement.type === "Identifier") {
return makeSolverFuncFromName(firstElement.name);
}
}
} else if (node.type === "ExpressionStatement") {
const expr = node.expression;
if (
expr.type === "AssignmentExpression" &&
expr.left.type === "Identifier" &&
expr.operator === "=" &&
expr.right.type === "ArrayExpression" &&
expr.right.elements.length === 1
) {
const [firstElement] = expr.right.elements;
if (firstElement && firstElement.type === "Identifier") {
return makeSolverFuncFromName(firstElement.name);
}
}
}
const declaration = node.declarations[0];
if (
declaration.type !== "VariableDeclarator" ||
!declaration.init ||
declaration.init.type !== "ArrayExpression" ||
declaration.init.elements.length !== 1
) {
return null;
}
const [firstElement] = declaration.init.elements;
if (!firstElement || firstElement.type !== "Identifier") {
return null;
}
return makeSolverFuncFromName(firstElement.name);
return null;
}
function makeSolverFuncFromName(name: string): ESTree.ArrowFunctionExpression {

View File

@@ -4,11 +4,9 @@ export const setupNodes = parse(`
if (typeof globalThis.XMLHttpRequest === "undefined") {
globalThis.XMLHttpRequest = { prototype: {} };
}
if (typeof globalThis.window === "undefined") {
globalThis.window = Object.create(null);
}
const window = Object.create(null);
if (typeof URL === "undefined") {
globalThis.window.location = {
window.location = {
hash: "",
host: "www.youtube.com",
hostname: "www.youtube.com",
@@ -22,7 +20,7 @@ if (typeof URL === "undefined") {
username: "",
};
} else {
globalThis.window.location = new URL("https://www.youtube.com/watch?v=yt-dlp-wins");
window.location = new URL("https://www.youtube.com/watch?v=yt-dlp-wins");
}
if (typeof globalThis.document === "undefined") {
globalThis.document = Object.create(null);

View File

@@ -62,7 +62,7 @@ const logicalExpression: DeepPartial<ESTree.ExpressionStatement> = {
},
};
const identifier = {
const identifier: DeepPartial<ESTree.Node> = {
or: [
{
type: "ExpressionStatement",
@@ -82,25 +82,50 @@ const identifier = {
type: "FunctionDeclaration",
params: [{}, {}, {}],
},
{
type: "VariableDeclaration",
declarations: {
anykey: [
{
type: "VariableDeclarator",
init: {
type: "FunctionExpression",
params: [{}, {}, {}],
},
},
],
},
},
],
} as const;
export function extract(
node: ESTree.Node,
): ESTree.ArrowFunctionExpression | null {
if (
!matchesStructure(node, identifier as unknown as DeepPartial<ESTree.Node>)
) {
if (!matchesStructure(node, identifier)) {
return null;
}
const block =
node.type === "ExpressionStatement" &&
let block: ESTree.BlockStatement | undefined | null;
if (node.type === "ExpressionStatement" &&
node.expression.type === "AssignmentExpression" &&
node.expression.right.type === "FunctionExpression"
? node.expression.right.body
: node.type === "FunctionDeclaration"
? node.body
: null;
node.expression.right.type === "FunctionExpression") {
block = node.expression.right.body;
} else if (node.type === "VariableDeclaration") {
for (const decl of node.declarations) {
if (
decl.type === "VariableDeclarator" &&
decl.init?.type === "FunctionExpression" &&
decl.init?.params.length === 3
) {
block = decl.init.body;
break;
}
}
} else if (node.type === "FunctionDeclaration") {
block = node.body;
} else {
return null;
}
const relevantExpression = block?.body.at(-2);
if (!matchesStructure(relevantExpression!, logicalExpression)) {
return null;

View File

@@ -23,7 +23,9 @@ export async function getIO(): Promise<IO> {
}
async function _getIO(): Promise<IO> {
if (globalThis.process?.release?.name === "node") {
// Old Deno requires casting to any as globalThis lacks an index signature
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((globalThis as any).process?.release?.name === "node") {
// Assume node compatibility
const { access, readFile } = await import("node:fs/promises");
const { deepStrictEqual } = await import("node:assert");

View File

@@ -251,6 +251,38 @@ export const tests: {
},
],
},
{
player: "638ec5c6",
n: [
// Synthetic test
{ input: "ZdZIqFPQK-Ty8wId", expected: "1qov8-KM-yH" },
],
sig: [
// Synthetic test
{
input:
"gN7a-hudCuAuPH6fByOk1_GNXN0yNMHShjZXS2VOgsEItAJz0tipeavEOmNdYN-wUtcEqD3bCXjc0iyKfAyZxCBGgIARwsSdQfJ2CJtt",
expected:
"MhudCuAuP-6fByOk1_GNXN7gNHHShjyXS2VOgsEItAJz0tipeav0OmNdYN-wUtcEqD3bCXjc0iyKfAyZxCBGgIARwsSdQfJ2CJtt",
},
],
},
{
player: "87644c66",
n: [
// Synthetic test
{ input: "ZdZIqFPQK-Ty8wId", expected: "iF5NxEm1BYk" },
],
sig: [
// Synthetic test
{
input:
"gN7a-hudCuAuPH6fByOk1_GNXN0yNMHShjZXS2VOgsEItAJz0tipeavEOmNdYN-wUtcEqD3bCXjc0iyKfAyZxCBGgIARwsSdQfJ2CJtt",
expected:
"atJC2JfQdSswRAtgGBCxZyAfKyi0cjXCb3DqEctUw-NYdNmOEvIepit0zJAtIEsgOV2SXZjhSHMNy0NXNG_1kOyBf6HPuAuCduh-a7Ng",
},
],
},
];
export const players = new Map([

0
test/__init__.py Normal file
View File

24
test/test_modules.py Normal file
View File

@@ -0,0 +1,24 @@
import unittest
from pathlib import Path
import yt_dlp_ejs.yt.solver
BASE_PATH = Path(__file__).parent.parent
CORE_PATH = BASE_PATH / "yt_dlp_ejs/yt/solver/core.min.js"
LIB_PATH = BASE_PATH / "yt_dlp_ejs/yt/solver/lib.min.js"
class TestModules(unittest.TestCase):
def test_yt_solver(self):
self.assertEqual(
yt_dlp_ejs.yt.solver.core(),
CORE_PATH.read_text(encoding="utf-8"),
)
self.assertEqual(
yt_dlp_ejs.yt.solver.lib(),
LIB_PATH.read_text(encoding="utf-8"),
)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,7 +1,3 @@
from yt_dlp_ejs._version import version
from yt_dlp_ejs import yt
__all__ = [
"version",
"yt"
]
__all__ = ["version"]

View File

@@ -1,3 +0,0 @@
from . import solver
__all__ = ["solver"]

View File

@@ -2,15 +2,20 @@ import importlib.resources
import yt_dlp_ejs.yt.solver
def core() -> str:
"""
Read the contents of the JavaScript core solver bundle as string.
"""
return importlib.resources.read_text(yt_dlp_ejs.yt.solver, "core.min.js")
return (importlib.resources.files(yt_dlp_ejs.yt.solver) / "core.min.js").read_text(
encoding="utf-8"
)
def lib() -> str:
"""
Read the contents of the JavaScript library solver bundle as string.
"""
return importlib.resources.read_text(yt_dlp_ejs.yt.solver, "lib.min.js")
return (importlib.resources.files(yt_dlp_ejs.yt.solver) / "lib.min.js").read_text(
encoding="utf-8"
)