You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

29 KiB

REVIEW

Append-only review log. Each task section is appended or updated in place; prior task history is preserved.


T-001 — FastAPI backend (app.py) + inline HTML/JS frontend (requirements.txt)

Verdict: PASS_WITH_NOTES

Findings

  1. [minor] app.py:147 — On any failure inside download() (including yt-dlp errors), str(exc) is returned verbatim to the browser as the message field. yt-dlp exceptions can include local filesystem paths (the temp dir) or verbose internal detail. Not a required fix for this local/self-hosted tool, but consider mapping to a generic message (e.g. "The video could not be downloaded.") and logging the raw exception server-side instead, before this is exposed beyond local/dev use.
    • Required fix: No.

No blocker or major findings.

Verification

Steps performed:

  • Re-read .ai/PLAN.md (Phase 1 spec for T-001) and diffed it against app.py / requirements.txt.
  • python -m py_compile app.py — succeeded.
  • Installed fastapi, uvicorn, httpx, python-multipart, yt-dlp into a scratch venv and exercised the app with starlette.testclient.TestClient, mocking yt_dlp.YoutubeDL to avoid live network calls:
    • GET / → 200, body contains "Download MP3" and "Download Video".
    • POST /download with a garbage (non-http) URL → 400 JSON {"message": ...}.
    • POST /download with an empty URL → 400 JSON.
    • POST /download missing the mode field (triggers RequestValidationError handler) → 400 JSON with .message.
    • POST /download with a valid URL, mode mp3, mocked yt-dlp writing a fake file → 200, correct bytes streamed, Content-Disposition: attachment; filename="...", temp dir cleaned up via BackgroundTask.
    • POST /download where mocked yt-dlp raises RuntimeError → 500 JSON {"message": "..."} (no server crash), temp dir cleaned up on the error path.
    • Filename containing non-ASCII characters (e.g. "café mix 🎵.mp3") → correctly RFC 5987-encoded as filename*=utf-8''... by Starlette; no crash or mojibake.

Findings from verification: All acceptance criteria for T-001 hold:

  • GET / returns 200 with HTML containing "Download MP3".
  • POST /download with a valid URL + mode streams a file.
  • Invalid URL returns 400 JSON with .message.

Risks:

  • No live network test against real YouTube was performed (would require external network access and a real video URL); mocked yt-dlp calls confirm the FastAPI plumbing (routing, form validation, error handling, file streaming, temp-dir cleanup) is correct, but do not confirm yt-dlp/ffmpeg behavior against a live video. This risk carries into the Docker-level validation planned for T-002 (docker build + smoke test), where a live download should be attempted at least once.
  • Large/long-running downloads have no timeout or size cap — acceptable for v1 per ROADMAP.md (out of scope), but worth revisiting if this moves beyond local/dev use.

T-002 — Dockerfile + README.md (build/run docs)

Verdict: PASS

Findings

  1. [nit] Dockerfile — the container runs as root (no USER directive). Not required for a local/self-hosted v1 tool per ROADMAP.md, but worth adding a non-root user if this is ever exposed beyond a trusted local network.
    • Required fix: No.

No blocker, major, or minor findings.

Verification

Steps performed:

  • Re-read .ai/PLAN.md (Phase 2 spec for T-002) and diffed it against Dockerfile / README.md; matches the planned Dockerfile contents and documented build/run/usage/size-note requirements.
  • docker build -t yt-dl-review . — exited 0.
  • docker run --rm -d -p 8091:8080 yt-dl-review — container started; curl http://localhost:8091/ returned HTTP 200 with both "Download MP3" and "Download Video" present in the body.
  • POST /download with an invalid URL against the running container → 400 JSON {"message": "..."}", confirming the containerized app behaves the same as the local dev checks from T-001; docker logs showed clean request handling with no crash/traceback.
  • Live end-to-end test (real network, real YouTube video https://www.youtube.com/watch?v=jNQXAC9IVRw, "Me at the zoo"):
    • mode=mp3 → 200, content-type: audio/mpeg, valid ID3-tagged MP3 file returned (verified with file), Content-Disposition filename correctly derived from video title.
    • mode=video → 200, content-type: video/mp4, valid ISO-Media MP4 file returned (verified with file).
    • This closes the live-download risk flagged in the T-001 review — yt-dlp + ffmpeg inside the container work correctly end-to-end for both modes.
  • Cleaned up: stopped test container, removed the yt-dl-review test image, removed downloaded test files.

Findings from verification: All acceptance criteria for T-002 hold:

  • docker build -t yt-dl . exits 0.
  • docker run --rm -p 8080:8080 yt-dl starts the server.
  • curl http://localhost:8080/ returns HTML with both download buttons.
  • (Bonus, beyond stated AC) A real MP3 and a real video download both succeed end-to-end through the container.

Risks:

  • None outstanding. The live-download risk noted in the T-001 review has been verified and closed here.

T-003 — noplaylist option, sanitize_filename helper, title-based filenames, frontend Content-Disposition fix

Verdict: PASS

Findings

No blocker, major, minor, or nit findings.

Verification

Steps performed:

  • Re-read .ai/PLAN.md (Phase 1 spec for T-003) and diffed it against app.py / README.md; implementation matches the plan (noplaylist: True on both modes, sanitize_filename helper with the specified regex, extract_info(..., download=True) used to recover title/id, filename=display_name passed to FileResponse, frontend Content-Disposition-based link.download fix).
  • python3 -m py_compile app.py — succeeded.
  • Unit-checked sanitize_filename directly (scratch venv, no mocks):
    • sanitize_filename("Song 🔥 Title ✨ (Live)", "abc123")"Song Title (Live)".
    • sanitize_filename("🔥🔥🔥", "abc123")"abc123" (fallback to id).
    • sanitize_filename("日本語 タイトル", "abc123")"日本語 タイトル" (non-Latin preserved).
    • Verified Starlette's FileResponse encodes such filenames via filename*=utf-8''... (RFC 5987) when they contain non-ASCII/space/apostrophe characters, and confirmed the frontend regex (/filename\*?=(?:UTF-8''|")?([^";]+)/i) plus decodeURIComponent correctly recovers the original title from that header format.
  • Live end-to-end test, built and ran the actual Docker image (docker build -t yt-dl-review ., docker run -p 8081:8080):
    • GET / → 200, contains "Download MP3".
    • POST /download with a playlist-context URL (watch?v=dQw4w9WgXcQ&list=RDdQw4w9WgXcQ), mode=mp3 → 200; container logs show Downloading just the video dQw4w9WgXcQ because of --no-playlist, confirming only the referenced video is fetched, never the rest of the playlist/mix; response is a single valid MP3 (file confirms ID3 v2.4 MPEG audio) with Content-Disposition: attachment; filename*=utf-8''Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster).mp3 — title-based, not a UUID.
    • mode=video against a plain (non-playlist) URL → 200, valid MP4 (file confirms ISO Media MP4), filename Me at the zoo.mp4 derived from the video title.
    • POST /download with an invalid (non-http) URL → unchanged 400 JSON {"message": ...} behavior, confirming the existing validation path was not regressed.
    • Cleaned up: stopped/removed the test container and scratch venv, deleted downloaded test files.
  • Reviewed README.md changes; the new paragraph accurately describes the playlist-single-video behavior and the title-based/emoji-stripped filename rule, matching observed behavior.

Findings from verification: All acceptance criteria for T-003 hold:

  • Pasting a playlist/list= URL downloads only the referenced video (confirmed via yt-dlp's own --no-playlist log line and a single file being produced).
  • Saved filename matches the video title with emoji/icons stripped (normal chars, including non-Latin, kept) instead of a UUID, with the correct extension.
  • python -m py_compile app.py passes.

Risks:

  • None outstanding. sanitize_filename strips characters outside \w/whitespace/- _ . ( ) , '; this only affects the Content-Disposition header value used for the browser's save-as name, not the on-disk path used to read the file, so there is no path-traversal or injection concern from unsanitized titles.

T-004 — 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

Verdict: PASS_WITH_NOTES

Findings

  1. [minor] app.py:296-315 (progress_hook) — job.percent is only updated inside if job.total_bytes:, so when yt-dlp can't report a total_bytes/total_bytes_estimate (e.g. some DASH/live formats), the progress bar stays at 0% for the whole download even though downloaded_bytes is advancing and shown as text. Not required: the plan explicitly allows "downloaded size only if total is unknown" as the fallback UX, and this doesn't affect the common case (verified live: percent tracked correctly when total is known).
    • Required fix: No.
  2. [minor] app.py:296-367progress_hooks only reports raw download percent; ffmpeg post-processing (mp3 extraction, video mux) after the download hits 100% is not reflected, so the bar can sit at 100% for a few extra seconds while the file is finalized before status flips to finished. Acceptable per plan (postprocessor_hooks was offered as one of two options, not mandatory), and verified it does not stall indefinitely or misreport.
    • Required fix: No.
  3. [nit] app.py:356-362 — the friendlier "The download could not be prepared." message set by progress_hook on a hook-level status == "error" is generally overwritten by the broader except Exception as exc: ... job.error_message = str(exc) handler in run_job, since yt-dlp raises after invoking the hook. In practice this means the browser sees yt-dlp's raw exception text (confirmed in testing: "ERROR: [generic] not-a-video: Unable to download webpage: HTTP Error 404...") rather than the friendlier hook message. Same class of note as the T-001 review (raw exception text surfaced to the browser); not a regression introduced by this task and not required to fix here.
    • Required fix: No.

No blocker or major findings.

Verification

Steps performed:

  • Re-read .ai/PLAN.md (Phase 2 spec for T-004) and diffed it against app.py / README.md; the three-endpoint split, JobState/_jobs/_jobs_lock, progress_hooks-driven updates, run_in_executor fire-and-forget scheduling (_pending_jobs set to avoid GC warnings), stale-job TTL sweep piggybacked on POST /download, and the frontend polling/progress-bar/button-disable/error-handling all match the plan.
  • python3 -m py_compile app.py — succeeded.
  • Built the actual Docker image (docker build -t yt-dl-review2 .) and ran it, then drove the real HTTP flow end-to-end against real YouTube URLs:
    • POST /download (playlist-context MP3 URL) → {"job_id": ...}; polling GET /progress/{job_id} every ~1s showed status: "downloading" with percent/speed/downloaded_bytes/total_bytes advancing correctly (0 → 61% → 100%), then status: "finished".
    • GET /download/{job_id}/file after finished → 200, correct MP3 bytes (file confirms valid MPEG audio), title-based Content-Disposition filename (reusing T-003's sanitization) — auto-delivered with no extra click needed on the client side.
    • Job cleanup confirmed: GET /progress/{job_id} after the file was served → 404 "Download job not found.", confirming BackgroundTask(cleanup_job, ...) removed the job and temp dir after delivery.
    • GET /progress/<unknown> and GET /download/<unknown>/file → both 404 as specified.
    • Fetched GET /download/{job_id}/file on a still-downloading job → 409 "The download is not ready yet.", matching the plan's pre-completion semantics.
    • Error path: posted a non-YouTube, non-existent URL (http://example.com/not-a-video) → job transitioned to status: "error" with a descriptive error message, no server crash (GET / still returned 200 immediately after), and docker logs showed no traceback — matches "server-side failure shows an error in place of the bar with no crash."
    • Confirmed no temp-dir leak on the error path (docker exec ... ls /tmp showed the errored job's temp dir already removed); a separate job's temp dir that was intentionally never fetched was still present, consistent with the documented "removed after delivery, or after a timeout for abandoned jobs" behavior (not fetching a finished job's file is expected to leave it until TTL/next sweep).
    • Cleaned up: stopped the test container, removed the yt-dl-review2/yt-dl-review test images, removed local scratch files.
  • Reviewed README.md changes: accurately documents the job/progress/file endpoint flow, automatic delivery, and non-persistent in-memory job state.

Findings from verification: All acceptance criteria for T-004 hold:

  • Progress bar appears on download start and updates with percent/speed/downloaded-total size (verified live with real percentages/speeds).
  • File auto-delivers to the browser on completion with the correct sanitized filename (server-side fetch + Content-Disposition flow verified; same header-parsing logic as T-003, already confirmed to round-trip correctly in the browser).
  • Server-side failure shows an error in place of the bar with no crash (verified: job marked error, server remained responsive).
  • python -m py_compile app.py passes.

Risks:

  • Progress percent can remain at 0% for formats where yt-dlp cannot report a total size (noted above, minor, not required).
  • The in-memory job store means an app restart mid-download loses all job state/progress for any in-flight browser sessions; this matches the explicitly stated "no persistent storage" design constraint and is now documented in README.md.

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

Verdict: PASS

Findings

No blocker, major, minor, or nit findings.

Verification

Steps performed:

  • Re-read .ai/PLAN.md (Phase 1 spec for T-005) and diffed it against Dockerfile, app.py, README.md, ROADMAP.md. The Dockerfile change (install curl/unzip, run the official Deno install script into /usr/local), the ydl_opts additions (js_runtimes, remote_components), and the README note all match the plan.
  • python3 -m py_compile app.py — succeeded.
  • docker build -t yt-dl . — succeeded (cached from the implementer's build, layers unchanged).
  • Confirmed shutil is already imported at the top of app.py (used elsewhere for rmtree), so no missing-import risk from the new shutil.which("deno") call.
  • Verified the js_runtimes/remote_components ydl_opts shape against the actual installed yt-dlp version inside the built image (yt-dlp 2026.08.19), since the plan flagged this as needing confirmation at implementation time:
    • Inspected yt_dlp/YoutubeDL.py inside the container: self.params['js_runtimes'] is validated by _clean_js_runtimes, which requires exactly dict[str, dict|None] ({"deno": {"path": ...}}) — matches app.py's shape precisely, including that config.get('path') is what's read internally.
    • Inspected yt_dlp/options.py: dest='js_runtimes' and dest='remote_components' confirm these are the correct ydl_opts keys for the installed version.
    • Constructed a yt_dlp.YoutubeDL(...) instance inside the container with the exact ydl_opts dict app.py builds — no warnings/errors, confirming the options are accepted and not silently ignored.
  • Ran the container (docker run --rm -d -p 8081:8080 ...) and:
    • docker exec ... deno --versiondeno 2.9.6 — binary present and runnable, on PATH.
    • Live MP3 download of the previously-failing URL (https://www.youtube.com/watch?v=7MrdyaSlOfI) via the real job flow (POST /download → poll GET /progress/{job_id}GET /download/{job_id}/file) — completed successfully, file delivered with the correct sanitized title-based filename via Content-Disposition.
    • docker logs during that download showed [youtube] [jsc:deno] Solving JS challenges using deno and [youtube] [jsc:deno] Downloading challenge solver lib script from https://github.com/yt-dlp/ejs/releases/... — confirms Deno and the GitHub-hosted EJS solver are actively used, not just installed-but-unused.
    • Full container log for both live downloads showed no No supported JavaScript runtime could be found warning anywhere.
    • Regression check: re-ran a playlist-context URL (watch?v=7MrdyaSlOfI&list=RDMM7MrdyaSlOfI) through the same job flow — logs show only the single video's extraction/download (no playlist enumeration), progress percent/speed/downloaded/total advanced correctly through completion, and the delivered filename matched the sanitized video title — confirms T-003/T-004 behavior is unaffected.
    • Stopped and removed the test container.
  • Reviewed the README.md addition: accurately explains the Deno runtime dependency, the GitHub-hosted EJS remote component, why they're needed (JS-based signature/PO-token deciphering), and the GitHub reachability requirement.
  • Reviewed the ROADMAP.md Priority 3 entry: accurately reflects the bug, root cause, and fix, consistent with what was implemented and verified.

Findings from verification: All acceptance criteria for T-005 hold:

  • docker build completes without errors after adding Deno.
  • The previously-failing video URL now downloads successfully end-to-end.
  • Server logs no longer show the missing-JS-runtime warning during a normal download.
  • Priority 1/2 behavior (single-video-from-playlist, progress bar, filename sanitization) is unaffected.
  • python -m py_compile app.py passes.

Risks:

  • The fix depends on outbound network access to github.com (for the EJS solver script) and deno.land (Dockerfile install-time only); if GitHub is unreachable from the container at runtime, the EJS-dependent extraction path could fail again — this is already called out in README.md as a requirement, so no action needed.
  • js_runtimes/remote_components are relatively new, evolving yt-dlp options (confirmed only against the currently pinned requirements.txt range, yt-dlp>=2024.4.9, as resolved to 2026.08.19 in the built image); a future yt-dlp release changing this option's shape would need to be caught by re-running this same build+live-download validation, not by py_compile alone.

T-006 — app.py (extractor_args.youtube.player_client ordering in shared ydl_opts) + README.md update

Verdict: PASS

Findings

No blocker, major, minor, or nit findings.

Verification

Steps performed:

  • Re-read .ai/PLAN.md (Priority 4 / T-006 spec) and diffed it against app.py, README.md, ROADMAP.md. The single-line addition — "extractor_args": {"youtube": {"player_client": ["android", "ios", "tv", "web"]}} in the shared ydl_opts dict, applied uniformly before the per-mode format update — matches the plan exactly. README.md documents the client-ordering mitigation and its best-effort nature; ROADMAP.md Priority 4 accurately describes the bug, fix, and acceptance criteria.
  • python3 -m py_compile app.py — succeeded.
  • docker build -t yt-dl . — succeeded (cached, layers unchanged since T-005).
  • Verified the extractor_args/player_client key is genuinely consumed by the installed yt-dlp (2026.08.19) inside the built image, not silently ignored: grep player_client in yt_dlp/extractor/youtube/_video.py shows self._configuration_arg('player_client') is read directly by the extractor, and constructing a real yt_dlp.YoutubeDL(...) with the exact ydl_opts app.py builds raised no warnings/errors.
  • Ran the container and drove live downloads through the real job flow (POST /download → poll GET /progress/{job_id}GET /download/{job_id}/file):
    • Previously-failing video (LV-NXucnyrc, mode mp3) — completed successfully. docker logs showed WARNING: ... HTTP Error 429: Too Many Requests on the initial webpage fetch (the originally-reported failure trigger), immediately followed by Downloading android player API JSON, Downloading ios player API JSON, Downloading tv client config/tv player API JSON — confirming the alternate clients were tried and used. No Missing required Visitor Data or This video is not available appeared; the job reached status: "finished" and the file was delivered via Content-Disposition with a correctly sanitized title-based filename (Baby's 1st Space Adventure ....mp3), verified as a valid MP3 with file.
    • Regression: plain MP3 (jNQXAC9IVRw) — unaffected, unchanged from prior task behavior.
    • Regression: plain Video (jNQXAC9IVRw, mode video) — completed, delivered a valid MP4 (file confirms ISO Media MP4) with filename Me at the zoo.mp4.
    • Regression: playlist-link (jNQXAC9IVRw&list=RDMMjNQXAC9IVRw, mode mp3) — docker logs showed Downloading just the video jNQXAC9IVRw because of --no-playlist, confirming T-003's single-video behavior is unaffected by the client-ordering change.
    • Error path: posted a non-existent URL (http://example.com/not-a-video) — job transitioned to status: "error" with a clear error message (HTTP Error 404: Not Found), no server crash (GET / still returned 200 immediately after), confirming the existing error-handling path is unaffected.
    • Progress bar percent/speed/downloaded/total advanced correctly for all live jobs.
    • Stopped and removed the test container, deleted downloaded test files.

Findings from verification: All acceptance criteria for T-006 hold:

  • A video URL previously failing with Missing required Visitor Data / This video is not available now downloads successfully via an alternate player client (confirmed live with LV-NXucnyrc).
  • Server logs show the alternate player clients (android/ios/tv) being used, and no PO-Token-blocking failure occurred.
  • Existing playlist-single-video/progress-bar/filename/Deno behavior (T-003/T-004/T-005) is unaffected — all regression checks passed.
  • Failures still surface a clean UI error with no crash (verified with the error-path check).
  • python -m py_compile app.py passes.

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.