# Plan Status: **ready_for_implement** Goal: implement a containerised YouTube downloader web app per `ROADMAP.md`. ## Scope - Single Docker container running a FastAPI server (Python 3.12-slim + ffmpeg + yt-dlp). - Minimal browser UI: one URL text field + **Download MP3** and **Download Video** buttons. - MP3 download: yt-dlp extracts best audio, ffmpeg re-encodes to MP3 at highest quality (`-q:a 0`), file streamed back to browser. - Video download: yt-dlp fetches `bestvideo+bestaudio`, merges to MP4 via ffmpeg, file streamed back to browser. - Invalid/empty URL returns a JSON error; the UI displays it without crashing. - No persistent storage — temp dir per request, cleaned up after response. ## Acceptance Criteria - `docker build -t yt-dl .` exits 0. - `docker run --rm -p 8080:8080 yt-dl` starts the server and `curl http://localhost:8080/` returns 200. - A valid YouTube URL + MP3 button delivers a `.mp3` file to the browser. - A valid YouTube URL + Video button delivers a `.mp4` file to the browser. - An empty URL or garbage URL returns an error visible in the UI. ## Implementation Phases ### Phase 1 — T-001: FastAPI backend + HTML/JS frontend **Files to create:** - `app.py` — entire server + inline HTML - `requirements.txt` **`app.py` structure:** 1. **HTML constant** (`HTML`) — inline single-page UI: - Text input for URL, placeholder "Paste YouTube URL…" - Two buttons: `Download MP3` / `Download Video` - Status `
` for errors/progress text - JS `downloadFile(mode)`: - Reads URL from input, validates non-empty client-side - `fetch('/download', { method:'POST', body: FormData{url, mode} })` - On success (content-type not application/json): creates object URL → `` click → revoke URL - On error or JSON response: parse JSON and show `.message` in status div 2. **FastAPI app** (`app`): - `GET /` — returns `HTMLResponse(HTML)` - `POST /download` — `url: str = Form(...)`, `mode: Literal["mp3","video"] = Form(...)` - Validate URL non-empty and starts with `http`; if not, raise `HTTPException(400)` - Create `tempfile.TemporaryDirectory()` - Build `ydl_opts`: - MP3: `format='bestaudio/best'`, postprocessor `FFmpegExtractAudio` with `preferredcodec='mp3'`, `preferredquality='0'` - Video: `format='bestvideo+bestaudio/best'`, `merge_output_format='mp4'` - `outtmpl=tmpdir/%(title)s.%(ext)s` - Run `yt_dlp.YoutubeDL(ydl_opts).download([url])` inside `asyncio.get_event_loop().run_in_executor(None, ...)` to avoid blocking the event loop - Find the downloaded file in tmpdir (glob the single file present) - Return `FileResponse(path, filename=..., background=BackgroundTask(cleanup_tmpdir))` — cleanup runs after response is sent - Wrap yt-dlp call in try/except; on any exception raise `HTTPException(500, detail=str(e))` 3. **`requirements.txt`**: ``` fastapi>=0.111.0 uvicorn[standard]>=0.29.0 yt-dlp>=2024.4.9 python-multipart>=0.0.9 ``` **Key decisions:** - Inline HTML keeps the project a two-file app (plus Dockerfile) — no template engine, no static asset serving needed. - `run_in_executor` keeps uvicorn's event loop unblocked while yt-dlp runs (which can take 30–120 s). - `FileResponse` with `BackgroundTask` cleanup is the standard FastAPI pattern for temp file responses. - yt-dlp is called via its Python API (not subprocess) to avoid shell escaping issues with URLs. --- ### Phase 2 — T-002: Dockerfile + README **Files to create/update:** - `Dockerfile` - `README.md` (update with build/run instructions) **`Dockerfile`**: ```dockerfile FROM python:3.12-slim RUN apt-get update \ && apt-get install -y --no-install-recommends ffmpeg \ && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app.py . EXPOSE 8080 CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"] ``` **`README.md`** must document: - Prerequisites: Docker - Build: `docker build -t yt-dl .` - Run: `docker run --rm -p 8080:8080 yt-dl` - Usage: open `http://localhost:8080` in a browser - Note on video size / download time expectations ## Validation ```bash # Build docker build -t yt-dl . # Smoke test — server starts and home page returns 200 docker run --rm -d -p 8080:8080 --name yt-dl-test yt-dl sleep 3 curl -sf http://localhost:8080/ | grep -q "Download MP3" docker stop yt-dl-test ``` No unit-test framework is introduced; the app is small enough that the docker smoke test is the validation gate. The reviewer will perform a live download test.