Skip to content

feat(tools): add vdl downloader + Siphon web front end - #16

Draft
cloudygetty-ai wants to merge 2 commits into
mainfrom
claude/video-downloader-i0sj8c
Draft

cloudygetty-ai wants to merge 2 commits into
mainfrom
claude/video-downloader-i0sj8c

Conversation

@cloudygetty-ai

@cloudygetty-ai cloudygetty-ai commented Aug 14, 2026 •

Copy link
Copy Markdown
Owner

What

Adds tools/video-downloader/ — a video/audio downloader built on yt-dlp (1800+ supported sites), in two forms:

  • vdl — the CLI
  • Siphon — a web front end for Vercel

yt-dlp handles extraction. This adds the operational layer around it: errors classified so retry policy is correct, bounded concurrency, partial-failure isolation, and structured telemetry.

cd tools/video-downloader && pip install -r requirements.txt

python -m vdl URL                        # best quality
python -m vdl URL -q 1080 -c mp4         # cap resolution, force container
python -m vdl URL --audio-only           # extract mp3
python -m vdl URL --section 1:30-4:00    # clip a range
python -m vdl -f urls.txt -j 5           # batch, 5 concurrent
python -m vdl URL --info                 # list formats, download nothing

Architecture

One responsibility per module; every file under the 200-line review threshold except cli.py (215, argument definitions rather than logic).

Module Job
config.py DownloadConfig — frozen, validated once at the boundary
formats.py yt-dlp format selector construction
options.py DownloadConfig → yt-dlp options dict (pure, fully testable)
downloader.py single URL, retry policy, error classification
batch.py thread-pool execution, failure isolation
telemetry.py HEALTH / PRESSURE / EFFICIENCY signals
ffmpeg.py ffmpeg discovery with bundled fallback
errors.py taxonomy, retry classification, stable exit codes
api/resolve.py Vercel function — URL validation + stream resolution
public/index.html Siphon UI

The load-bearing detail

yt-dlp reports a permanent 404 and a timed-out socket with the same Unable to download webpage prefix. A naive classifier retries both. The first build did exactly that — a dead URL burned the full retry budget with exponential backoff before failing.

The classifier now parses HTTP status out of the message and orders matching so unrecoverable cases are caught first: 401/403 → protected, 404/410/451 → not found, 429/5xx → retryable. Measured on a dead URL: 10s → 1.6s.

Exit codes are stable for scripting: 0 all ok, 1 partial failure, 2 bad config, 3 unsupported URL, 4 protected content, 5 network, 6 merge, 7 not found.

Siphon (web)

Paste a URL, get every downloadable stream as a direct link. Obsidian/gold, Cinzel + DM Mono per house style.

Why it resolves instead of proxying. Vercel functions cap at 60s with no persistent disk, so streaming a full video through one fails on anything sizable. The function probes and returns direct stream URLs; the browser fetches from the origin CDN. The server never touches video bytes — no egress bill, no timeout.

validate() is the security boundary. The function feeds caller-supplied URLs to yt-dlp, which fetches whatever it is handed — an SSRF gadget pointed at Vercel's internal network unless fenced. Scheme, length, and every resolved address are checked before extraction; private, loopback, link-local, reserved and multicast space are refused, including the 169.254.169.254 metadata endpoint. If DNS returns several addresses and any one is internal, the request is rejected.

Not deployed yet. The Vercel API returned 403 forbidden — "You don't have permission to create a project." on both personal and team scope, so the project could not be created from here. Deploy with vercel --cwd tools/video-downloader once the account can create projects. I did not deploy into any of the 24 existing projects, since each is a different live app.

Two bugs found while verifying against real media

  • _kind() read vcodec=None as "no video track", but yt-dlp uses the literal string "none" for absent and None for unprobed. A plain .avi came back labelled audio-only. Unknown now reads as present.
  • _label() hardcoded "audio only" whenever height was missing, mislabelling the same generic-extractor case. It now derives from the stream kind.

Testing

114 tests, no network required — yt_dlp.YoutubeDL is replaced with a scripted stub, so retry policy, classification, and concurrency are verified offline. The SSRF guard has its own 20-case suite.

114 passed in 0.29s

Also verified end to end against CC-licensed Blender demo media: 8.1 MB download at 4.5 MB/s with ffmpeg metadata pass, audio extraction → 1.3 MB mp3, batch of 2 with one intentional failure (1 ok / 1 failed, exit 1, good URL unaffected), --json-logs telemetry, and --simulate.

Notes for review

  • Self-contained Python package under tools/. No dependency on the React Native game, no changes to existing files, nothing added to package.json, and the root vercel.json (the game's landing page) is untouched.
  • requirements.txt narrowed to yt-dlp so the lambda doesn't ship a 25 MB ffmpeg binary it never invokes; ffmpeg stays an optional CLI extra.
  • CI is red on a pre-existing main failure, not this diff — see the comment below. type-check and npm test (107 passing) are clean; lint fails identically on pristine origin/main.
  • Downloads what a site serves to an ordinary client. No DRM circumvention, no paywall bypass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01US2uXf9esJvHouvwvvUxCB

Wraps yt-dlp with the operational layer it does not provide: classified
errors driving retry policy, bounded concurrency, partial-failure isolation,
and structured run telemetry.

- config.py   frozen DownloadConfig, validated once at the boundary
- formats.py  format selectors that degrade progressively so single-stream
              sites still resolve instead of erroring on a hard constraint
- errors.py   taxonomy + retry classification + stable exit codes
- batch.py    thread-pool execution; one failed URL never stops the run
- telemetry.py HEALTH / PRESSURE / EFFICIENCY, JSON to stderr
- ffmpeg.py   system ffmpeg, falling back to the imageio-ffmpeg binary so
              merging and audio extraction work with no system packages

Retry policy is the load-bearing detail: yt-dlp reports a permanent 404 and a
timed-out socket with the same "Unable to download webpage" prefix, so the
classifier parses HTTP status out of the message and retries only what can
succeed. Measured on a dead URL: 10s of pointless backoff down to 1.6s.

Verified end to end against CC-licensed Blender demo media — download, mp3
extraction, container remux, batch with an intentional failure, and telemetry
output. 79 tests, no network required (yt_dlp.YoutubeDL is stubbed).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01US2uXf9esJvHouvwvvUxCB

Copy link
Copy Markdown
Owner Author

CI is red on a pre-existing main failure, not this diff

npm run lint fails with 79 errors across 8 files, all of them prettier/prettier formatting violations in existing game source:

 24  src/types/game.ts
 15  src/core/characters/characters.ts
 13  src/core/characters/portraits.ts
 10  src/core/gameEngine/GameEngine.ts
  7  src/services/state/gameStore.ts
  5  src/components/HUD.tsx
  4  src/screens/LobbyScreen.tsx
  1  src/core/meteor/MeteorManager.ts

Zero are under tools/. Confirmed by checking out pristine origin/main — which has no tools/ directory at all — into a separate worktree and running the same command: identical 87 problems (79 errors, 8 warnings).

This is not new. Across the last 24 CI runs on main there are 14 failures, 10 cancelled, and zero successes — the workflow has never been green, including at the current default-branch HEAD 641d49c.

The other two steps are clean at this commit:

Step Result
npm run type-check passes
npm test 107 passed, 8 suites
npm run lint 79 errors — pre-existing on main

Why I'm not fixing it here

73 of the 79 are auto-fixable with npx eslint . --ext .ts,.tsx --fix (pure whitespace/formatting). But that rewrites 8 battle-royale source files that have nothing to do with a downloader, turning a self-contained additive PR into a repo-wide reformat and burying the reviewable change. Better as its own PR.

Happy to open that separately if wanted — it should be a one-command mechanical commit.


Generated by Claude Code

Siphon — the vdl engine behind a static UI plus one serverless function.

Resolution, not proxying: Vercel functions cap at 60s with no persistent
disk, so streaming full videos through one fails on anything sizable. The
function probes the URL and returns direct stream URLs; the browser fetches
from the origin CDN. The server never touches video bytes.

- api/resolve.py     URL validation + stream resolution, shared error taxonomy
- public/index.html  obsidian/gold UI, Cinzel + DM Mono per house style
- vercel.json        60s/1024MB function budget, nosniff/DENY/no-referrer

validate() is the security boundary. The function feeds caller-supplied URLs
to yt-dlp, which fetches whatever it is handed — an SSRF gadget pointed at
Vercel's internal network unless fenced. Scheme, length, and every resolved
address are checked before extraction; private, loopback, link-local,
reserved and multicast space are refused, including 169.254.169.254. If DNS
returns several addresses and any one is internal, the request is rejected.

Two bugs found while verifying against real media:

- _kind() read vcodec=None as "no video track", but yt-dlp uses the literal
  string "none" for absent and None for unprobed. A plain .avi came back
  labelled audio-only. Unknown now reads as present.
- _label() hardcoded "audio only" whenever height was missing, mislabelling
  the same generic-extractor case. It now derives from the stream kind.

requirements.txt narrowed to yt-dlp so the lambda does not ship a 25MB
ffmpeg binary it never invokes; ffmpeg stays an optional CLI extra.

114 tests (35 new), all offline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01US2uXf9esJvHouvwvvUxCB
@cloudygetty-ai cloudygetty-ai changed the title feat(tools): add vdl — hardened CLI video/audio downloader feat(tools): add vdl downloader + Siphon web front end Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants