Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
2aa321a
Create deno.yml
tcely Nov 29, 2025
ec51e35
deno fmt: worker.ts
tcely Nov 28, 2025
06a5172
deno fmt: server.ts
tcely Nov 28, 2025
2b2f20f
deno fmt: src/instrumentedCache.ts
tcely Nov 28, 2025
a4fdb1e
deno fmt: src/metrics.ts
tcely Nov 28, 2025
a9f3b15
deno fmt: src/middleware.ts
tcely Nov 28, 2025
e81aacb
deno fmt: src/playerCache.ts
tcely Nov 28, 2025
7013cc6
deno fmt: src/preprocessedCache.ts
tcely Nov 28, 2025
60a4dd8
deno fmt: src/solver.ts
tcely Nov 28, 2025
bf199f4
deno fmt: src/solverCache.ts
tcely Nov 28, 2025
f3980ec
deno fmt: src/stsCache.ts
tcely Nov 28, 2025
59b41e7
deno fmt: src/types.ts
tcely Nov 28, 2025
e6633d9
deno fmt: src/utils.ts
tcely Nov 28, 2025
52bbc1e
deno fmt: src/validation.ts
tcely Nov 28, 2025
2b2d80a
deno fmt: src/workerPool.ts
tcely Nov 28, 2025
b520d94
deno fmt: src/handlers/decryptSignature.ts
tcely Nov 28, 2025
caf3f83
deno fmt: src/handlers/getSts.ts
tcely Nov 28, 2025
6873384
deno fmt: src/handlers/resolveUrl.ts
tcely Nov 28, 2025
959e82b
deno lint: src/utils.ts
tcely Nov 28, 2025
d26aeac
deno lint: src/types.ts
tcely Nov 28, 2025
8cb4e9d
Always use a string for port
tcely Nov 28, 2025
4afb394
Ignore the `no-explicit-any` lint results
tcely Nov 29, 2025
a156b15
Address `no-prototype-builtins` lint error
tcely Nov 29, 2025
1a878d9
Add `deno check` to this workflow
tcely Nov 29, 2025
f2e1627
Use a submodule for `ejs`
tcely Nov 29, 2025
1b05f3e
Create deno.json
tcely Nov 26, 2025
b29cd42
Create deno.lock
tcely Nov 26, 2025
0772eee
Disable frozen flag for now
tcely Nov 29, 2025
f14d3c2
Exclude `no-import-prefix` lint rule for now
tcely Nov 29, 2025
948aaed
Handle HOME returning undefined
tcely Nov 29, 2025
ee3994e
Ignore the `require-await` lint rule
tcely Nov 29, 2025
daf7a5e
Use configuration for `deno fmt`
tcely Nov 29, 2025
7412105
Use configuration for `deno lint`
tcely Nov 29, 2025
c543b2a
Merge branch 'master' into tcely-deno-workflow
tcely Nov 29, 2025
6a19926
Rename from `test` to `check` in deno.yml
tcely Dec 1, 2025
b4f30be
Merge branch 'tcely-a' into tcely-b
tcely Dec 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/deno.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

# This workflow will install Deno then run:
# - `deno fmt`
# - `deno lint`
# - `deno check`
# For more information see: https://github.com/denoland/setup-deno

name: Deno

on:
push:
branches: ["master"]
pull_request:
branches: ["master"]

permissions:
contents: read

jobs:
check:
runs-on: ubuntu-latest

steps:
- name: Setup repo
uses: actions/checkout@v6
with:
submodules: recursive

- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: latest
cache: true

- name: Verify formatting
run: deno fmt --check

- name: Run linter
run: deno lint

- name: Check types
run: deno check --frozen=false *.ts

# - name: Run tests
# run: deno test -A
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "ejs"]
path = ejs
url = https://github.com/yt-dlp/ejs.git
10 changes: 10 additions & 0 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,21 @@
"compilerOptions": {
"lib": [ "deno.worker" ]
},
"fmt": {
"include": ["src/", "*.ts"],
"indentWidth": 4
},
"imports": {
"@std/crypto": "jsr:@std/crypto@^0.224.0",
"@std/fs": "jsr:@std/fs@^0.224.0",
"@std/http": "jsr:@std/http@0.224.5",
"@std/path": "jsr:@std/path@^0.224.0",
"ejs/": "https://esm.sh/gh/yt-dlp/ejs@0.3.0&standalone/"
},
"lint": {
"include": ["src/", "*.ts"],
"rules": {
"exclude": [ "no-import-prefix" ]
}
}
}
1 change: 1 addition & 0 deletions ejs
Submodule ejs added at 2655b1
33 changes: 22 additions & 11 deletions server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async function baseHandler(req: Request): Promise<Response> {
{
status: 404,
headers: { "Content-Type": "text/plain" },
}
},
);
}
}
Expand All @@ -48,7 +48,7 @@ async function baseHandler(req: Request): Promise<Response> {
}
}

if (pathname === '/metrics') {
if (pathname === "/metrics") {
return new Response(registry.metrics(), {
headers: { "Content-Type": "text/plain" },
});
Expand All @@ -57,28 +57,39 @@ async function baseHandler(req: Request): Promise<Response> {
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<Response>;

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

Expand All @@ -88,8 +99,8 @@ async function baseHandler(req: Request): Promise<Response> {

const handler = baseHandler;

const port = Deno.env.get("PORT") || 8001;
const host = Deno.env.get("HOST") || '0.0.0.0';
const port = Deno.env.get("PORT") || "8001";
const host = Deno.env.get("HOST") || "0.0.0.0";

await initializeCache();
initializeWorkers();
Expand Down
31 changes: 23 additions & 8 deletions src/handlers/decryptSignature.ts
Original file line number Diff line number Diff line change
@@ -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<Response> {
const { encrypted_signature, n_param, player_url } = ctx.body as SignatureRequest;
export async function handleDecryptSignature(
ctx: RequestContext,
): Promise<Response> {
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);
}
Expand All @@ -25,5 +37,8 @@ export async function handleDecryptSignature(ctx: RequestContext): Promise<Respo
decrypted_n_sig,
};

return new Response(JSON.stringify(response), { status: 200, headers: { "Content-Type": "application/json" } });
}
return new Response(JSON.stringify(response), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
23 changes: 16 additions & 7 deletions src/handlers/getSts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ export async function handleGetSts(ctx: RequestContext): Promise<Response> {
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",
},
});
}

Expand All @@ -26,12 +29,18 @@ export async function handleGetSts(ctx: RequestContext): Promise<Response> {
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" },
},
);
}
}
}
52 changes: 43 additions & 9 deletions src/handlers/resolveUrl.ts
Original file line number Diff line number Diff line change
@@ -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<Response> {
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");
}
Expand All @@ -29,15 +52,26 @@ export async function handleResolveUrl(ctx: RequestContext): Promise<Response> {

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" } });
}
return new Response(JSON.stringify(response), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
2 changes: 1 addition & 1 deletion src/instrumentedCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ export class InstrumentedLRU<T> extends LRU<T> {
super.remove(key);
cacheSize.labels({ cache_name: this.cacheName }).set(this.size);
}
}
}
11 changes: 9 additions & 2 deletions src/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
});

Expand Down Expand Up @@ -58,4 +65,4 @@ export const playerScriptFetches = Counter.with({
help: "Total number of player script fetches.",
labels: ["player_url", "status"],
registry: [registry],
});
});
Loading