Repository navigation
Releases: fdff87554/youtube-downloader
Release list
v0.4.1 — resolve each mp4 download once, halving its memory
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
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=mp4prefers H.264 and AAC over AV1 and VP9 (#113).- YouTube rarely offers H.264 above 1080p, so
quality=bestusually 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=1080on a video with 1080p AV1 but only 720p H.264. - The picker now reads Best (most compatible) and Up to 1080p / 720p / 480p.
- YouTube rarely offers H.264 above 1080p, so
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:
- Input handling:
- Edge and privacy:
- Frontend: metadata is built through the DOM rather than
innerHTML(#88), and the empty state is fixed (#100). - Runtime:
- Supply chain and CI:
Known limitations
- A progressive MP4 with its
moovat 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
-xerrorguard (#116). - Two pushes to
mainin quick succession can leave:mainon the older image (#118).
v0.3.0 — Python 3.14, security backlog cleared, Maple Mono UI
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'starget-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 insideci.yaml, and CI also runs onv*tags so a tagged release is validated before it ships. This release is the first to go through that path. npm auditandpip-auditare separate jobs (#77). As sequential steps, a frontend finding aborted the run beforepip-auditever 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,
tailwindcssand@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.3Digest: 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
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,noreferreron the downloadwindow.openso the new tab can no longer reach back throughwindow.openeror leak referer. (#31) - Time out
/api/inforequests at 30 s viaAbortController, aligned with backendSOCKET_TIMEOUT, so a stalled API can no longer hang the UI. (#31) - Translate
AbortErrorto a user-facing timeout message; broadened the check frominstanceof DOMExceptiontoinstanceof Errorfor portability across runners and polyfills. (#31) - Accessibility: form controls now have proper
label[for]associations, the URL input announces validity viaaria-invalid/aria-describedby, and both error regions userole="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)
nodebuild base 22.22.2-alpine → 26.1.0-alpine (#29)vite8.0.10 → 8.0.12 (#26)vitest4.1.5 → 4.1.6 (#28)tailwindcss4.2.4 → 4.3.0 (#27)@tailwindcss/vite4.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.2Run command and configuration are unchanged from v0.2.0 — see that release for the full setup notes.
v0.2.2 — troubleshooting docs
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 productionto 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.2Run command and configuration are unchanged from v0.2.1 — see v0.2.0 for full setup notes.
v0.2.1 — dependency and CI maintenance
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)
typescript5.9.3 → 6.0.3 (build-time only; emitted output unchanged)tailwindcss4.2.2 → 4.2.4@tailwindcss/vite4.2.2 → 4.2.4
CI / dev only (no image impact)
uvicornrequirement cap widened to<0.47(lockfile still pinned at the same patch)actions/cache4 → 5jdx/mise-action2 → 4 (Node.js 24 runtime)docker/metadata-action5 → 6actions/checkout4 → 6docker/build-push-action6 → 7docker/login-action3 → 4docker/setup-buildx-action4 → 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
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-dlppinned to2026.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, mandatoryALLOWED_ORIGINSin production, masked yt-dlp errors whenDEBUG=false, frontendnpm auditclean. - 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 sodocker stopis graceful; subprocess stderr forwarded to logger and return-code monitored. - CVE patches:
starletteCVE-2025-62727 (viafastapi>=0.121,<0.124),pytestCVE-2025-71176,vite/postcssaudit 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_ORIGINSmust be set whenDEBUG=false;compose.yamlno longer defaults to*.GET /api/formatsremoved; the same data is onGET /api/info(formatsfield).- Uvicorn now boots via
--factory app.main:create_appinstead ofapp.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.2Run 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.0For 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"}