23 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
- [minor]
app.py:147— On any failure insidedownload()(including yt-dlp errors),str(exc)is returned verbatim to the browser as themessagefield. 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 againstapp.py/requirements.txt. python -m py_compile app.py— succeeded.- Installed
fastapi,uvicorn,httpx,python-multipart,yt-dlpinto a scratch venv and exercised the app withstarlette.testclient.TestClient, mockingyt_dlp.YoutubeDLto avoid live network calls:GET /→ 200, body contains "Download MP3" and "Download Video".POST /downloadwith a garbage (non-http) URL → 400 JSON{"message": ...}.POST /downloadwith an empty URL → 400 JSON.POST /downloadmissing themodefield (triggersRequestValidationErrorhandler) → 400 JSON with.message.POST /downloadwith a valid URL, modemp3, mocked yt-dlp writing a fake file → 200, correct bytes streamed,Content-Disposition: attachment; filename="...", temp dir cleaned up viaBackgroundTask.POST /downloadwhere mocked yt-dlp raisesRuntimeError→ 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 /downloadwith 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
- [nit]
Dockerfile— the container runs as root (noUSERdirective). Not required for a local/self-hosted v1 tool perROADMAP.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 againstDockerfile/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 /downloadwith 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 logsshowed 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 withfile),Content-Dispositionfilename correctly derived from video title.mode=video→ 200,content-type: video/mp4, valid ISO-Media MP4 file returned (verified withfile).- 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-reviewtest 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-dlstarts 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 againstapp.py/README.md; implementation matches the plan (noplaylist: Trueon both modes,sanitize_filenamehelper with the specified regex,extract_info(..., download=True)used to recovertitle/id,filename=display_namepassed toFileResponse, frontendContent-Disposition-basedlink.downloadfix). python3 -m py_compile app.py— succeeded.- Unit-checked
sanitize_filenamedirectly (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
FileResponseencodes such filenames viafilename*=utf-8''...(RFC 5987) when they contain non-ASCII/space/apostrophe characters, and confirmed the frontend regex (/filename\*?=(?:UTF-8''|")?([^";]+)/i) plusdecodeURIComponentcorrectly 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 /downloadwith a playlist-context URL (watch?v=dQw4w9WgXcQ&list=RDdQw4w9WgXcQ),mode=mp3→ 200; container logs showDownloading 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 (fileconfirms ID3 v2.4 MPEG audio) withContent-Disposition: attachment; filename*=utf-8''Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster).mp3— title-based, not a UUID.mode=videoagainst a plain (non-playlist) URL → 200, valid MP4 (fileconfirms ISO Media MP4), filenameMe at the zoo.mp4derived from the video title.POST /downloadwith 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.mdchanges; 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-playlistlog 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.pypasses.
Risks:
- None outstanding.
sanitize_filenamestrips characters outside\w/whitespace/- _ . ( ) , '; this only affects theContent-Dispositionheader 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
- [minor]
app.py:296-315(progress_hook) —job.percentis only updated insideif job.total_bytes:, so when yt-dlp can't report atotal_bytes/total_bytes_estimate(e.g. some DASH/live formats), the progress bar stays at 0% for the whole download even thoughdownloaded_bytesis 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.
- [minor]
app.py:296-367—progress_hooksonly 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 beforestatusflips tofinished. Acceptable per plan (postprocessor_hookswas offered as one of two options, not mandatory), and verified it does not stall indefinitely or misreport.- Required fix: No.
- [nit]
app.py:356-362— the friendlier"The download could not be prepared."message set byprogress_hookon a hook-levelstatus == "error"is generally overwritten by the broaderexcept Exception as exc: ... job.error_message = str(exc)handler inrun_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 againstapp.py/README.md; the three-endpoint split,JobState/_jobs/_jobs_lock,progress_hooks-driven updates,run_in_executorfire-and-forget scheduling (_pending_jobsset to avoid GC warnings), stale-job TTL sweep piggybacked onPOST /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": ...}; pollingGET /progress/{job_id}every ~1s showedstatus: "downloading"withpercent/speed/downloaded_bytes/total_bytesadvancing correctly (0 → 61% → 100%), thenstatus: "finished".GET /download/{job_id}/fileafterfinished→ 200, correct MP3 bytes (fileconfirms valid MPEG audio), title-basedContent-Dispositionfilename (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.", confirmingBackgroundTask(cleanup_job, ...)removed the job and temp dir after delivery. GET /progress/<unknown>andGET /download/<unknown>/file→ both 404 as specified.- Fetched
GET /download/{job_id}/fileon a still-downloadingjob → 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 tostatus: "error"with a descriptiveerrormessage, no server crash (GET /still returned 200 immediately after), anddocker logsshowed 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 /tmpshowed 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-reviewtest images, removed local scratch files.
- Reviewed
README.mdchanges: 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-Dispositionflow 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.pypasses.
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 againstDockerfile,app.py,README.md,ROADMAP.md. The Dockerfile change (installcurl/unzip, run the official Deno install script into/usr/local), theydl_optsadditions (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
shutilis already imported at the top ofapp.py(used elsewhere forrmtree), so no missing-import risk from the newshutil.which("deno")call. - Verified the
js_runtimes/remote_componentsydl_optsshape against the actual installed yt-dlp version inside the built image (yt-dlp2026.08.19), since the plan flagged this as needing confirmation at implementation time:- Inspected
yt_dlp/YoutubeDL.pyinside the container:self.params['js_runtimes']is validated by_clean_js_runtimes, which requires exactlydict[str, dict|None]({"deno": {"path": ...}}) — matchesapp.py's shape precisely, including thatconfig.get('path')is what's read internally. - Inspected
yt_dlp/options.py:dest='js_runtimes'anddest='remote_components'confirm these are the correctydl_optskeys for the installed version. - Constructed a
yt_dlp.YoutubeDL(...)instance inside the container with the exactydl_optsdictapp.pybuilds — no warnings/errors, confirming the options are accepted and not silently ignored.
- Inspected
- Ran the container (
docker run --rm -d -p 8081:8080 ...) and:docker exec ... deno --version→deno 2.9.6— binary present and runnable, onPATH.- Live MP3 download of the previously-failing URL (
https://www.youtube.com/watch?v=7MrdyaSlOfI) via the real job flow (POST /download→ pollGET /progress/{job_id}→GET /download/{job_id}/file) — completed successfully, file delivered with the correct sanitized title-based filename viaContent-Disposition. docker logsduring that download showed[youtube] [jsc:deno] Solving JS challenges using denoand[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 foundwarning 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.mdaddition: 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.mdPriority 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 buildcompletes 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.pypasses.
Risks:
- The fix depends on outbound network access to
github.com(for the EJS solver script) anddeno.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 inREADME.mdas a requirement, so no action needed. js_runtimes/remote_componentsare relatively new, evolving yt-dlp options (confirmed only against the currently pinnedrequirements.txtrange,yt-dlp>=2024.4.9, as resolved to2026.08.19in 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 bypy_compilealone.
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 againstapp.py,README.md,ROADMAP.md. The single-line addition —"extractor_args": {"youtube": {"player_client": ["android", "ios", "tv", "web"]}}in the sharedydl_optsdict, applied uniformly before the per-modeformatupdate — matches the plan exactly.README.mddocuments the client-ordering mitigation and its best-effort nature;ROADMAP.mdPriority 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_clientkey is genuinely consumed by the installed yt-dlp (2026.08.19) inside the built image, not silently ignored:grep player_clientinyt_dlp/extractor/youtube/_video.pyshowsself._configuration_arg('player_client')is read directly by the extractor, and constructing a realyt_dlp.YoutubeDL(...)with the exactydl_optsapp.pybuilds raised no warnings/errors. - Ran the container and drove live downloads through the real job flow (
POST /download→ pollGET /progress/{job_id}→GET /download/{job_id}/file):- Previously-failing video (
LV-NXucnyrc, modemp3) — completed successfully.docker logsshowedWARNING: ... HTTP Error 429: Too Many Requestson the initial webpage fetch (the originally-reported failure trigger), immediately followed byDownloading 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. NoMissing required Visitor DataorThis video is not availableappeared; the job reachedstatus: "finished"and the file was delivered viaContent-Dispositionwith a correctly sanitized title-based filename (Baby's 1st Space Adventure ....mp3), verified as a valid MP3 withfile. - Regression: plain MP3 (
jNQXAC9IVRw) — unaffected, unchanged from prior task behavior. - Regression: plain Video (
jNQXAC9IVRw, modevideo) — completed, delivered a valid MP4 (fileconfirms ISO Media MP4) with filenameMe at the zoo.mp4. - Regression: playlist-link (
jNQXAC9IVRw&list=RDMMjNQXAC9IVRw, modemp3) —docker logsshowedDownloading 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 tostatus: "error"with a clearerrormessage (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.
- Previously-failing video (
Findings from verification: All acceptance criteria for T-006 hold:
- A video URL previously failing with
Missing required Visitor Data/This video is not availablenow downloads successfully via an alternate player client (confirmed live withLV-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.pypasses.
Risks:
- This is an explicitly best-effort mitigation (as documented in
README.mdandROADMAP.md): YouTube's anti-bot posture changes over time, and theandroid/ios/tvclients 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.