Skip to content

Releases: fdff87554/youtube-downloader

v0.4.1 — resolve each mp4 download once, halving its memory

Choose a tag to compare

@fdff87554 fdff87554 released this 25 Sep 14:17
ed10556

Resource fix on top of v0.4.0. No API or behaviour change.

Each MP4 download resolves the video once (#121)

v0.4.0 fetches video and audio with separate yt-dlp processes, and each one resolved the video on its own. Every resolution runs deno to solve YouTube's player challenge, which peaks at about 270 MiB. After deploying v0.4.0, production measured:

  • one 1080p download: 680 MiB peak
  • two concurrent downloads: reached a 1G container limit

Now yt-dlp -J resolves the video once, and both track downloads read that JSON from stdin (--load-info-json -), so deno never runs during the downloads. Measured with the production limits (1G, 1 CPU):

v0.4.0 v0.4.1
one 1080p download, peak memory 666 MiB 365 MiB
two concurrent downloads, peak memory 994 MiB (at the limit) 681 MiB
download time 8.6 s 8.6 s

YouTube also sees one resolution per download again instead of two. This resolves the "resolves the video twice" limitation listed in the v0.4.0 notes.

If the resolver fails during setup, it is now torn down along with the processes it started, instead of being left running.

Deploying

A container memory limit of 1G is recommended, as in the bundled compose.yaml. If you run the image behind a reverse proxy, set TRUSTED_PROXIES: since v0.4.0, per-client limits otherwise apply to the proxy's single address, which caps the whole site at 3 concurrent downloads.

v0.4.0 — real MP4 downloads: H.264/AAC, fragmented MP4, complete audio

Choose a tag to compare

@fdff87554 fdff87554 released this 25 Sep 13:31
d73606c

Playback and hardening release on top of v0.3.0. MP4 downloads are now real MP4s with H.264 video and complete audio. They open in QuickTime and suit hardware players such as car head units. The API surface, subprocess handling and edge configuration also got a round of fixes.

Changed behaviour

  • fmt=mp4 prefers H.264 and AAC over AV1 and VP9 (#113).
    • YouTube rarely offers H.264 above 1080p, so quality=best usually tops out at 1080p, where it used to reach 4K AV1.
    • The fixed qualities are ceilings. When a video's H.264 stops below the requested height, that lower H.264 is chosen: for example, 720p for quality=1080 on a video with 1080p AV1 but only 720p H.264.
    • The picker now reads Best (most compatible) and Up to 1080p / 720p / 480p.

MP4 downloads (#112, #108, #115)

Before Now
AV1 was selected at every quality whenever a video had it, and many hardware decoders cannot play AV1 (#112) H.264/AAC first, with other codecs only when no H.264 fits the ceiling (#113)
The body was MPEG-TS served as video/mp4, and AV1 inside it had no codec identification (#108) Fragmented MP4, the only MP4 layout that can be streamed without storing the file (#117, which lands #114)
Merged downloads over about 10 MiB were throttled to about 110 s, and their audio stopped early (#115) Video and audio are fetched separately with yt-dlp's chunked downloader and merged by our own ffmpeg (#117)

Measured on a 213 s video at 480p: 109 s with audio ending at 175 s → 4.5 s with complete audio. A 10-minute video downloads in 7.6 s with complete audio.

A download that fails before its first 64 KiB, such as a progressive MP4 with its index at the end (#119), now returns a JSON error instead of a 200 with an empty video.

Fixes and hardening

  • Downloads:
    • Failures surface as errors instead of empty 200s (#84).
    • A client disconnect kills the whole pipeline (#107), and subprocess teardown is hardened (#98).
    • Concurrent streams per client are capped (#93).
    • Playlist extraction is bounded (#94).
  • Input handling:
    • Only YouTube's own yt-dlp extractors run (#87).
    • Uppercase hosts are accepted (#95).
    • Unsupported YouTube URLs are reported as client errors (#103).
  • Edge and privacy:
    • Security headers apply on every location, and public docs are closed (#89).
    • Error envelopes survive nginx (#90).
    • Video URLs stay out of the access log (#91).
    • Rate limits key on the real client behind a proxy (#86).
    • Runtime writes are confined to tmpfs (#92).
  • Frontend: metadata is built through the DOM rather than innerHTML (#88), and the empty state is fixed (#100).
  • Runtime:
    • yt-dlp extraction runs off the event loop (#85).
    • The yt-dlp challenge solver scripts are installed (#110).
    • The entrypoint teardown and signal exits are fixed (#99, #109).
  • Supply chain and CI:
    • Actions are pinned to SHAs with read-only defaults (#96).
    • Backend lockfiles are hash-pinned (#97).
    • The app version has a single source (#101).
    • Test coverage is extended (#83, #102).
    • CI now runs the real-ffmpeg remux tests.

Known limitations

  • A progressive MP4 with its moov at the end cannot be remuxed from a pipe, and fails with an error (#119). No YouTube sample of that layout has been seen yet.
  • Each MP4 download resolves the video twice, once per track. Whether that raises the risk of YouTube's bot detection is unverified.
  • The mp3 stage still lacks the -xerror guard (#116).
  • Two pushes to main in quick succession can leave :main on the older image (#118).

v0.3.0 — Python 3.14, security backlog cleared, Maple Mono UI

Choose a tag to compare

@fdff87554 fdff87554 released this 21 Sep 06:42
9c2dc2b

Maintenance and hardening release on top of v0.2.3. The backlog of 13 stale Dependabot PRs is cleared, every known vulnerability in both lockfiles is gone, the runtime moves to Python 3.14, and the release pipeline no longer publishes images that failed CI.

No API changes. /api/info and /api/download behave exactly as before — verified against a real video on every step of the way.

⚠️ Breaking for local development

requires-python is now >=3.14 and mise.toml pins python = "3.14". Contributors on 3.12 will need to re-run mise install and rebuild .venv. Container users are unaffected: the image ships its own interpreter.

If mise install fails with Python installation is missing a 'lib' directory, your mise is too old to pick the right precompiled CPython flavor for 3.14 — run mise self-update (fixed as of 2026.9.12).

Security

pip-audit --strict went from 15 vulnerabilities across 3 packages to 0; npm audit from 5 (3 high, 2 moderate) to 0. None of the vulnerable code paths were reachable from this service's API, but they gated CI and all sat on the request path.

Package Fixed Advisories
yt-dlp 2026.3.17 → 2026.8.19 GHSA-69qj-pvh9-c5wg, CVE-2026-50019, CVE-2026-50023, CVE-2026-50574, CVE-2026-55404
starlette 1.2.1 → 1.6.0 CVE-2026-54283 (high), CVE-2026-54282
anyio 4.13.0 → 4.15.1 CVE-2026-63374 (critical), CVE-2026-64847
postcss 8.5.15 → 8.5.28 CVE-2026-73646 (high), CVE-2026-69153
nanoid 3.3.12 → 3.3.19 CVE-2026-67214, CVE-2026-67213
ws (via happy-dom) 8.20.0 → 8.21.3 GHSA-96hv-2xvq-fx4p (high)
node (build stage) 26.3.0 → 26.9.0-alpine covers the 26.3.1 and 26.5.1 security releases

Hardening: yt-dlp subprocesses now pass --ignore-config (#81). A yt-dlp.conf picked up from ~/.config/yt-dlp, ~/.yt-dlp, /etc/yt-dlp or beside the binary could otherwise inject --exec, an external downloader, a cookie file, or -P/-o — the last of which would write media to disk and break the project's no-disk-I/O guarantee.

Repository settings: Dependabot alerts and security updates are now enabled. Every advisory above is transitive, so none of them would have opened a PR on their own.

Runtime

  • Python 3.12.13 → 3.14.7 (#74), with mise.toml, requires-python, ruff's target-version, both Dockerfile stages, the README and both lockfiles moved together. Regenerating the lockfiles under 3.14 changed no package versions.
  • Deno bin-2.8.2 → 2.9.7 (#51) — the JS runtime yt-dlp uses for YouTube player challenges.
  • uvicorn 0.49.0 → 0.53.0 (#73, #75), fastapi 0.136.3 → 0.141.1 (#72).

fastapi 0.137 turned router.routes into a tree, which changes how slowapi's middleware resolves handlers. The outcome is unchanged — rate limiting is performed by the @limiter.limit decorators either way — and test_returns_429_after_info_limit_exceeded covers it.

Interface

  • The UI is now set in Maple Mono (#78), by name only. No webfont is shipped: the CN build is 20.4 MB per weight with no upstream woff2, and a CDN would leak viewers' IPs, which is the opposite of what this service is for. Anyone with the font installed sees it; everyone else gets their platform's monospace face. CJK video titles fall back cleanly to the system face.

Build and release pipeline

  • GHCR publishes only after CI passes (#77). The publish job used to trigger on push to main independently of every check — during this release cycle alone it would have published ~13 times, several while the audit was red. It is now a needs-gated job inside ci.yaml, and CI also runs on v* tags so a tagged release is validated before it ships. This release is the first to go through that path.
  • npm audit and pip-audit are separate jobs (#77). As sequential steps, a frontend finding aborted the run before pip-audit ever executed, so a red X said nothing about the backend.
  • Dependabot is grouped and labelled (#76). The five labels the config had always requested did not exist, so every PR got a complaint comment instead. npm dev-dependencies group by minor+patch, tailwindcss and @tailwindcss/* are pinned to the same group (updating one alone leaves a nested duplicate in the lockfile), docker patches group, actions group. pip stays ungrouped on purpose — its lockfiles must be regenerated by hand, one package at a time.

Toolchain

vitest 4.1.6 → 5.0.1, TypeScript 6.0.3 → 7.0.2, vite 8.0.12 → 8.3.0, happy-dom 20.9.0 → 20.14.5, tailwindcss 4.3.0 → 4.3.3, actions/cache v5 → v6, actions/checkout v6 → v7. All dev-only; the emitted bundle is byte-identical across the TypeScript 7 migration.

Verification

Every change in this release was checked against a real video through the full Docker stack, not just unit tests — the backend suite mocks yt-dlp, so version bumps to it cannot be validated any other way. /api/info returns 6 formats, /api/download returns valid mp4 and mp3 streams, and container logs are unchanged from the v0.2.3 baseline.

Backend 56 tests, frontend 20 tests, pre-commit run --all-files clean.

Pull the image

docker pull ghcr.io/fdff87554/youtube-downloader:0.3.0
# or the floating minor tag:
docker pull ghcr.io/fdff87554/youtube-downloader:0.3

Digest: sha256:aa46bcd6fb62c67d2192e6c1bfda08a7875f88b584435071d25d410541daa8af

Run command and configuration are unchanged from v0.2.0 — see that release for the full setup notes.

v0.2.3 — code review fixes & dependency maintenance

Choose a tag to compare

@fdff87554 fdff87554 released this 21 Sep 06:35

Maintenance release on top of v0.2.2. Bundles a small batch of frontend fixes (security, a11y, robustness) plus the regular dev-dep refresh. No new features, no breaking changes.

Frontend fixes (user-visible behavior)

  • Set noopener,noreferrer on the download window.open so the new tab can no longer reach back through window.opener or leak referer. (#31)
  • Time out /api/info requests at 30 s via AbortController, aligned with backend SOCKET_TIMEOUT, so a stalled API can no longer hang the UI. (#31)
  • Translate AbortError to a user-facing timeout message; broadened the check from instanceof DOMException to instanceof Error for portability across runners and polyfills. (#31)
  • Accessibility: form controls now have proper label[for] associations, the URL input announces validity via aria-invalid / aria-describedby, and both error regions use role="status" (polite live region) instead of the role / aria-live anti-pattern. (#31)

Tests

  • Backend: cover subprocess pipe orchestration with a fake-binary integration test so future refactors of the streaming layer have a regression net. (#31)

Documentation

  • Clarify the stateless thin-client architecture in internal docs — server-side is structurally required, not "serverless". (#31)
  • Ignore a local-only sentinel file. (#31)

Image changes (frontend bundle rebuilt + build infra)

  • node build base 22.22.2-alpine → 26.1.0-alpine (#29)
  • vite 8.0.10 → 8.0.12 (#26)
  • vitest 4.1.5 → 4.1.6 (#28)
  • tailwindcss 4.2.4 → 4.3.0 (#27)
  • @tailwindcss/vite 4.2.4 → 4.3.0 (#25)

Bundle output is byte-equivalent in size to v0.2.2; behavior unchanged.

Pull the image

docker pull ghcr.io/fdff87554/youtube-downloader:0.2.3
# or, for the floating minor tag:
docker pull ghcr.io/fdff87554/youtube-downloader:0.2

Run command and configuration are unchanged from v0.2.0 — see that release for the full setup notes.

v0.2.2 — troubleshooting docs

Choose a tag to compare

@fdff87554 fdff87554 released this 30 Apr 03:34

Documentation-only release on top of v0.2.1. No application behavior changes — backend, frontend bundle, and Dockerfile stages are all identical to v0.2.1.

Documentation

  • README Troubleshooting now maps RuntimeError: ALLOWED_ORIGINS must be set explicitly in production to the corresponding configuration fix, so deployers hitting the v0.2.0 startup failure can resolve it without reading source. (#23)

Pull the image

docker pull ghcr.io/fdff87554/youtube-downloader:0.2.2
# or, for the floating minor tag:
docker pull ghcr.io/fdff87554/youtube-downloader:0.2

Run command and configuration are unchanged from v0.2.1 — see v0.2.0 for full setup notes.

v0.2.1 — dependency and CI maintenance

Choose a tag to compare

@fdff87554 fdff87554 released this 29 Apr 08:11

Maintenance release on top of v0.2.0. No application behavior changes — application source code is identical to v0.2.0; this release only refreshes build-time dependencies and CI tooling.

Image changes (frontend bundle rebuilt)

  • typescript 5.9.3 → 6.0.3 (build-time only; emitted output unchanged)
  • tailwindcss 4.2.2 → 4.2.4
  • @tailwindcss/vite 4.2.2 → 4.2.4

CI / dev only (no image impact)

  • uvicorn requirement cap widened to <0.47 (lockfile still pinned at the same patch)
  • actions/cache 4 → 5
  • jdx/mise-action 2 → 4 (Node.js 24 runtime)
  • docker/metadata-action 5 → 6
  • actions/checkout 4 → 6
  • docker/build-push-action 6 → 7
  • docker/login-action 3 → 4
  • docker/setup-buildx-action 4 → 4

The actions sweep silences the upstream Node.js 20 deprecation warning GitHub started emitting on every workflow run.

Pull the image

```bash
docker pull ghcr.io/fdff87554/youtube-downloader:0.2.1

or, for the floating minor tag:

docker pull ghcr.io/fdff87554/youtube-downloader:0.2
```

Run command and configuration are unchanged from v0.2.0 — see that release for the full notes.

v0.2.0 — Pre-release hardening

Choose a tag to compare

@fdff87554 fdff87554 released this 29 Apr 07:55

Highlights

Pre-release hardening based on code review feedback (#11). 29 atomic commits across reproducibility, security, functional correctness, operability, and developer experience.

  • Reproducibility: uv-generated lockfiles for the backend; yt-dlp pinned to 2026.3.17; pinned base image patch versions.
  • Security: per-IP rate limiting (slowapi + nginx limit_req), nginx security headers (CSP / X-Frame-Options / etc.), non-root container, mandatory ALLOWED_ORIGINS in production, masked yt-dlp errors when DEBUG=false, frontend npm audit clean.
  • Functional correctness: Deno bundled into the runtime image so yt-dlp can solve YouTube JS challenges (full format coverage); quality fallback now respects the requested height ceiling.
  • Operability: Docker HEALTHCHECK; entrypoint signal trap so docker stop is graceful; subprocess stderr forwarded to logger and return-code monitored.
  • CVE patches: starlette CVE-2025-62727 (via fastapi>=0.121,<0.124), pytest CVE-2025-71176, vite / postcss audit fixes.
  • Dev experience: full README, MIT LICENSE, LEGAL.md, CI security audits (npm audit + pip-audit), CI dependency caching, vitest with frontend tests.

Breaking changes

  • ALLOWED_ORIGINS must be set when DEBUG=false; compose.yaml no longer defaults to *.
  • GET /api/formats removed; the same data is on GET /api/info (formats field).
  • Uvicorn now boots via --factory app.main:create_app instead of app.main:app.

Pull the image

docker pull ghcr.io/fdff87554/youtube-downloader:0.2.0
# or, for the floating minor tag:
docker pull ghcr.io/fdff87554/youtube-downloader:0.2

Run with the new mandatory configuration:

docker run -d \
  -p 8080:8080 \
  -e ALLOWED_ORIGINS="https://your-frontend.example.com" \
  --name youtube-downloader \
  ghcr.io/fdff87554/youtube-downloader:0.2.0

For local exploration, use DEBUG=true instead of ALLOWED_ORIGINS. See the README for the full configuration reference and LEGAL.md for the usage notice.

Verify

curl http://localhost:8080/api/health
# -> {"status":"ok"}