From 6cab4488116f1f542fc0e8792572ea98b96237f0 Mon Sep 17 00:00:00 2001 From: Domagoj Zecevic Date: Fri, 11 Sep 2026 16:29:27 +0200 Subject: [PATCH] feat(downloads): add a 720p video option --- .ai/HANDOFF.md | 39 +++++++++++++++++++++++++++++++++++++++ .ai/REVIEW.md | 40 ++++++++++++++++++++++++++++++++++++++++ .ai/TASKS.md | 2 +- README.md | 2 +- app.py | 12 ++++++++++-- 5 files changed, 91 insertions(+), 4 deletions(-) diff --git a/.ai/HANDOFF.md b/.ai/HANDOFF.md index 545e0bf..ebb5e68 100644 --- a/.ai/HANDOFF.md +++ b/.ai/HANDOFF.md @@ -278,6 +278,45 @@ Append-only role handoff log. Each role adds one entry when its step is complete --- +### T-007 — implement — 2026-09-11T13:43:53Z + +| Field | Value | +|-------|-------| +| Agent | codex | +| Summary | Added a capped-720p video download mode and browser button while preserving the shared download lifecycle. | +| Files Changed | app.py, README.md, .ai/TASKS.md, .ai/HANDOFF.md | +| Validation | `python3 -m py_compile app.py`, `docker build -t yt-dl .`, homepage button check, and a live `video720` MP4 job passed; the delivered video was 640x360, confirming the cap and lower-quality fallback path. | +| Commit | Pending reviewer approval | +| Next Role | review | + +--- + +### T-007 — implement — 2026-09-11T14:28:37Z + +| Field | Value | +|-------|-------| +| Agent | codex | +| Summary | Squashed the reviewed 720p video download option into the task commit. | +| Files Changed | app.py, README.md, .ai/REVIEW.md, .ai/TASKS.md, .ai/HANDOFF.md | +| Validation | Reviewer verification passed; `git diff --check` passed. | +| Commit | Created by this handoff's task commit | +| Next Role | none | + +--- + +### T-007 — review — 2026-09-11T14:27:56Z + +| Field | Value | +|-------|-------| +| Agent | claude | +| Summary | Reviewed and verified the 720p video mode: rebuilt the Docker image, proved the format-selector cap+fallback correct against yt-dlp's real selection engine with synthetic 720p/sub-720p format sets (since real sources reachable in this environment currently top out at 360p due to YouTube's SABR rollout), and drove live video720/MP3/Video/playlist/error-path jobs through the container — all passed. | +| Files Changed | .ai/TASKS.md, .ai/REVIEW.md, .ai/HANDOFF.md | +| Verdict | PASS | +| Blocking Findings | none | +| Next Role | implement | + +--- + ### T-006 — review — 2026-09-11T13:41:31Z | Field | Value | diff --git a/.ai/REVIEW.md b/.ai/REVIEW.md index a68f84c..a49ceed 100644 --- a/.ai/REVIEW.md +++ b/.ai/REVIEW.md @@ -230,3 +230,43 @@ No blocker, major, minor, or nit findings. **Risks:** - This is an explicitly best-effort mitigation (as documented in `README.md` and `ROADMAP.md`): YouTube's anti-bot posture changes over time, and the `android`/`ios`/`tv` clients could themselves become restricted later, requiring the client list to be retuned. No action needed now; this is a known, accepted risk per the plan's scope. - The live test observed `WARNING: ... Some android/ios client https formats have been skipped ... SABR-only streaming experiment` — a yt-dlp/YouTube-side warning unrelated to this change (it did not block the download, format selection fell through to a working format) — noted for awareness, not a regression introduced by T-006. + +--- + +## T-007 — `app.py` (`video720` mode: `Literal` type extension, `bestvideo[height<=720]+bestaudio/best[height<=720]` format branch, third HTML button) + `README.md` update + +**Verdict:** PASS + +### Findings + +No blocker, major, minor, or nit findings. + +### Verification + +**Steps performed:** +- Re-read `.ai/PLAN.md` (Priority 5 / T-007 spec) and diffed it against `app.py` / `README.md`. Implementation matches exactly: `Literal["mp3", "video", "video720"]` extended on both the `/download` route's `mode` `Form(...)` parameter and `JobState.mode`; a `elif mode == "video720":` branch setting `format: "bestvideo[height<=720]+bestaudio/best[height<=720]"` and `merge_output_format: "mp4"`, placed before the existing unrestricted-`video` `else` branch; a third HTML button `Download Video 720p` calling `downloadFile('video720')`, reusing the mode-agnostic `downloadFile` JS unchanged. `README.md` documents the new button and its cap/fallback behavior. +- `python3 -m py_compile app.py` — succeeded. +- `docker build -t yt-dl .` — succeeded (cached, layers unchanged since T-006). +- `GET /` on the running container confirmed the "Download Video 720p" button is present in the HTML. +- **Format-selector correctness, verified directly against the format-selection engine of the installed yt-dlp (2026.08.19) inside the container**, since real YouTube sources reachable from this environment currently only exposed up to 360p (YouTube's ongoing SABR-only-streaming rollout was blocking higher-resolution `https` formats on every test video tried across `android`/`ios`/`tv`/`web` clients — confirmed via `docker logs` warnings and a direct `extract_info` format dump; not something this app or task controls). To verify the cap and fallback logic itself rather than being blocked by an unrelated environmental limitation: + - Constructed synthetic format lists inside the container and ran them through `yt_dlp.YoutubeDL.build_format_selector` with the app's exact selector string, after the required `ydl.sort_formats(info)` pre-sort step (confirmed this step matters: without it the selector silently picks the wrong entry, which the initial harness attempt caught and was corrected). + - Source with formats at 1080p/720p/480p → `bestvideo[height<=720]+bestaudio/best[height<=720]` selected the muxed pair `(v720, audio)` — confirms the cap actually restricts above-720p sources. + - Source with formats only at 480p/360p (no 720p) → the same selector selected `v480` (best available) — confirms graceful fallback to the best sub-720p quality with no error, matching the "no upscaling, no error" requirement. + - `bestvideo` with no filter on the 1080p/720p/480p set correctly selected `v1080`, confirming the harness itself (once correctly pre-sorted) reflects real yt-dlp selection behavior, not an artifact of the test setup. +- Ran the container and drove the real job flow live for `video720`: + - `POST /download` (mode `video720`, `7MrdyaSlOfI`) → completed, `status: "finished"`, delivered a valid MP4 (`file` confirms ISO Media MP4) with a correctly sanitized `Content-Disposition` filename; progress percent/speed/downloaded/total advanced correctly through completion (this source's real available formats topped out at 360p per the SABR limitation above, so this run exercises the fallback path live, complementing the synthetic-selector proof of the capping path). + - Regression: plain `mp3` and plain unrestricted `video` mode against a different video (`jNQXAC9IVRw`) both completed and delivered correct, correctly-named files (`Me at the zoo.mp3` / `.mp4`) — unaffected by the new mode. + - Regression: `video720` against a playlist-context URL (`&list=...`) — `docker logs` showed `Downloading just the video ... because of --no-playlist`, confirming T-003's single-video behavior extends to the new mode. + - Error path: `video720` against a non-existent URL → job reached `status: "error"` with a clear message, no server crash, server remained responsive — confirming existing error handling is unaffected. + - Stopped and removed the test container, deleted downloaded test files. + +**Findings from verification:** All acceptance criteria for T-007 hold: +- The UI shows three buttons: Download MP3, Download Video, Download Video 720p. +- The format selector caps at 720p when higher-resolution sources exist (verified directly against yt-dlp's selection engine) and falls back to the best available quality with no error when the source is below 720p (verified both synthetically and live). +- The 720p mode shows the same live progress bar and auto-delivery behavior as the existing modes (verified live). +- The delivered filename follows the same sanitized-title convention with the correct extension (verified live). +- Existing Download MP3 and Download Video behavior is unchanged (verified live, both regressions passed). +- `python -m py_compile app.py` passes. + +**Risks:** +- Direct live confirmation that a real >720p YouTube source gets *downscaled* to exactly 720p (rather than just "the selector logic caps correctly," which was verified against synthetic data) was not obtainable in this environment: every real video URL tried during this review only exposed up to 360p via the available player clients, due to YouTube's current SABR-only-streaming rollout affecting `https`-protocol adaptive formats broadly (unrelated to this task's code). This is an environmental/platform limitation at review time, not a defect in the implementation — the format-selector logic itself was verified correct against the installed yt-dlp's real selection engine. Re-running the live ">720p → capped to 720p" check opportunistically (e.g. against a video/client combination that regains higher-resolution `https` formats as YouTube's rollout evolves) would close this residual gap, but is not a required fix. diff --git a/.ai/TASKS.md b/.ai/TASKS.md index 0503701..fd65267 100644 --- a/.ai/TASKS.md +++ b/.ai/TASKS.md @@ -26,4 +26,4 @@ Command expectations: | T-004 | `app.py` (job-based `POST /download` + `GET /progress/{job_id}` + `GET /download/{job_id}/file`, in-memory job store, yt-dlp `progress_hooks`, frontend polling + progress bar UI) + `README.md` update | done | Progress bar appears below the buttons on download start and updates with percent/speed/downloaded-total size; file auto-delivers to the browser on completion with the correct sanitized filename; server-side failure shows an error in place of the bar with no crash; `python -m py_compile app.py` passes | Reviewer re-ran `py_compile` and drove the full job lifecycle live through the built Docker image against real YouTube URLs (progress polling, auto-delivered file, 404/409 semantics, server-side error path with no crash) — all passed; see `.ai/REVIEW.md` | none | | T-005 | `Dockerfile` (install Deno JS runtime) + `app.py` (point yt-dlp `ydl_opts` at the installed Deno binary and permit official EJS solver retrieval) + `README.md` update | done | `docker build` exits 0; `docker exec deno --version` succeeds; a video URL previously failing with `This video is not available` now downloads successfully; server logs no longer show `No supported JavaScript runtime could be found` during a download; playlist-single-video/progress-bar/filename behavior from T-003/T-004 is unaffected; `python -m py_compile app.py` passes | Reviewer re-ran `py_compile`, `docker build`, verified the `js_runtimes`/`remote_components` `ydl_opts` shape against the installed yt-dlp source, and drove live MP3 downloads (previously-failing URL + playlist-link regression) through the built Docker image — no missing-runtime warning, EJS solver fetched and used, single-video/progress/filename behavior intact; see `.ai/REVIEW.md` | none | | T-006 | `app.py` (`extractor_args.youtube.player_client` ordering in shared `ydl_opts`) + `README.md` update | done | A video URL previously failing with `Missing required Visitor Data` / `This video is not available` now downloads successfully via an alternate player client; existing playlist-single-video/progress-bar/filename/Deno behavior unaffected; failures still surface a clean UI error with no crash; `python -m py_compile app.py` passes | Reviewer re-ran `py_compile`, `docker build`, confirmed the `player_client` key is read by the installed yt-dlp extractor, and drove live downloads (previously-failing `LV-NXucnyrc`, plain MP3/Video, playlist-link, error-path) through the built Docker image — all passed; see `.ai/REVIEW.md` | none | -| T-007 | `app.py` (`video720` mode: `Literal` type extension, `bestvideo[height<=720]+bestaudio/best[height<=720]` format branch, third HTML button) + `README.md` update | ready_for_implement | UI shows a third "Download Video 720p" button; downloads are capped at 720p with graceful fallback to lower quality when the source is below 720p; progress bar, auto-delivery, and sanitized filename behavior match existing modes; existing MP3/Video buttons unchanged; `python -m py_compile app.py` passes | pending | implement | +| T-007 | `app.py` (`video720` mode: `Literal` type extension, `bestvideo[height<=720]+bestaudio/best[height<=720]` format branch, third HTML button) + `README.md` update | done | UI shows a third "Download Video 720p" button; downloads are capped at 720p with graceful fallback to lower quality when the source is below 720p; progress bar, auto-delivery, and sanitized filename behavior match existing modes; existing MP3/Video buttons unchanged; `python -m py_compile app.py` passes | Reviewer re-ran `py_compile`/`docker build`, verified the format-selector cap+fallback directly against yt-dlp's real selection engine with synthetic 720p/sub-720p format sets, and drove live `video720`/MP3/Video/playlist/error-path jobs through the built Docker image — all passed; see `.ai/REVIEW.md` for the note on real >720p sources being unreachable in this environment due to YouTube's current SABR rollout | none | diff --git a/README.md b/README.md index 7d97569..d7830f1 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ need retuning in the future. docker run --rm -p 8080:8080 yt-dl ``` -Open [http://localhost:8080](http://localhost:8080) in a browser, paste a YouTube URL, then select **Download MP3** or **Download Video**. +Open [http://localhost:8080](http://localhost:8080) in a browser, paste a YouTube URL, then select **Download MP3**, **Download Video**, or **Download Video 720p**. The 720p option caps the video at 720p and falls back to the best available lower quality when the source has no 720p stream; it otherwise uses the same progress, automatic delivery, and sanitized filename behavior as the unrestricted Video option. Downloads run as temporary in-memory jobs, so a progress bar shows percentage, speed, and downloaded size while the server prepares the file. When it finishes, the file is delivered to your browser automatically. Large videos and higher-quality formats can take several minutes, depending on the source video and your network connection. diff --git a/app.py b/app.py index 9f193d7..9e1db2a 100644 --- a/app.py +++ b/app.py @@ -47,6 +47,7 @@ HTML = """
+