# 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.