# YouTube Downloader Backend

Express + [`youtube-dl-exec`](https://github.com/microlinkhq/youtube-dl-exec) (a wrapper around `yt-dlp`) backend.
Accepts a YouTube video or playlist URL, returns the available video-resolution / mp3-bitrate
options, then downloads the selected one to the server asynchronously and hands back a
download URL once it's ready.

## Prerequisites

- **Node.js 18+**
- **Python 3.9+** available as `python3` on PATH (required by `youtube-dl-exec` to run `yt-dlp`)
- **ffmpeg** installed and on PATH — required to merge separate video/audio streams into
  one mp4, and to transcode audio to mp3 at a specific bitrate.
  - macOS: `brew install ffmpeg`
  - Ubuntu/Debian: `sudo apt install ffmpeg`
  - Windows: install a build and add it to PATH

## Install & run

```bash
npm install
cp .env.example .env    # adjust PORT / PUBLIC_BASE_URL as needed
npm start
```

Server starts on `http://localhost:4000` by default. Downloaded files are written to
`./downloads` and served statically at `/downloads/<filename>`.

## How it works

1. `POST /api/info` — probes the URL. Single video → full option list (resolutions + mp3
   bitrates). Playlist → lightweight list of videos in it (no per-video format probing, so
   it stays fast even for long playlists). Call `/api/info` again with a specific video's
   `webpageUrl` to get that video's own options.
2. `POST /api/download` — you pass a `formatId` from step 1. The server responds
   **immediately** with `202` and a `jobId`; the actual `yt-dlp` download + (if needed)
   ffmpeg merge/transcode happens in the background.
3. `GET /api/status/:jobId` — poll this until `status` is `"completed"`, at which point
   `result.downloadUrl` is a direct link to the file under `/downloads`. (Long-polling can
   be added later by having this route delay its response instead of returning instantly —
   the job store is already structured to support that.)

## API reference

### `POST /api/info`

```json
{ "url": "https://www.youtube.com/watch?v=6xKWiCMKKJg" }
```

Single video response:

```json
{
  "type": "video",
  "video": {
    "id": "6xKWiCMKKJg",
    "title": "...",
    "thumbnail": "...",
    "durationSeconds": 214,
    "uploader": "...",
    "webpageUrl": "https://www.youtube.com/watch?v=6xKWiCMKKJg",
    "videoOptions": [
      { "formatId": "video-best", "label": "Best available (1080p) MP4", "height": 1080, "ext": "mp4", "approxFileSize": null },
      { "formatId": "video-1080p", "label": "1080p MP4", "height": 1080, "ext": "mp4", "approxFileSize": "84.2 MB" },
      { "formatId": "video-720p",  "label": "720p MP4",  "height": 720,  "ext": "mp4", "approxFileSize": "51.0 MB" },
      { "formatId": "video-480p",  "label": "480p MP4",  "height": 480,  "ext": "mp4", "approxFileSize": "29.8 MB" }
    ],
    "audioOptions": [
      { "formatId": "audio-320", "label": "MP3 320 kbps", "bitrateKbps": 320, "ext": "mp3", "approxFileSize": "8.2 MB" },
      { "formatId": "audio-256", "label": "MP3 256 kbps", "bitrateKbps": 256, "ext": "mp3", "approxFileSize": "6.6 MB" },
      { "formatId": "audio-192", "label": "MP3 192 kbps", "bitrateKbps": 192, "ext": "mp3", "approxFileSize": "4.9 MB" },
      { "formatId": "audio-128", "label": "MP3 128 kbps", "bitrateKbps": 128, "ext": "mp3", "approxFileSize": "3.3 MB" },
      { "formatId": "audio-96",  "label": "MP3 96 kbps",  "bitrateKbps": 96,  "ext": "mp3", "approxFileSize": "2.5 MB" }
    ]
  }
}
```

Playlist response:

```json
{
  "type": "playlist",
  "playlistTitle": "My Playlist",
  "videoCount": 42,
  "videos": [
    { "id": "abc123", "title": "...", "thumbnail": "...", "durationSeconds": 180, "webpageUrl": "https://www.youtube.com/watch?v=abc123" }
  ]
}
```

### `POST /api/download`

```json
{ "url": "https://www.youtube.com/watch?v=6xKWiCMKKJg", "formatId": "video-1080p" }
```

Immediate response (`202 Accepted`):

```json
{ "jobId": "b6f1...", "status": "queued", "statusUrl": "/api/status/b6f1..." }
```

### `GET /api/status/:jobId`

While in progress:

```json
{ "jobId": "b6f1...", "status": "processing", "type": "video", "result": null, "error": null }
```

Once finished:

```json
{
  "jobId": "b6f1...",
  "status": "completed",
  "type": "video",
  "result": {
    "fileName": "b6f1....mp4",
    "fileSize": 88276123,
    "downloadUrl": "http://localhost:4000/downloads/b6f1....mp4"
  },
  "error": null
}
```

If it fails: `"status": "failed"` and `error` holds a message.

## Notes / next steps

- **Storage is in-memory** (`src/lib/jobStore.js`). Restarting the server loses job history —
  swap in Redis/a DB if you need persistence across restarts or multiple server instances.
- Finished jobs (and their files) are auto-deleted after `JOB_TTL_MS` (default 1 hour) by a
  periodic sweep in `server.js` — tune via `.env`.
- `noPlaylist: true` is set on single-video downloads so pasting a "watch?v=...&list=..."
  URL never accidentally pulls in the whole playlist.
- To add long-polling later: have `GET /api/status/:jobId` hold the response open (with a
  timeout) and resolve early via an event emitter when `jobStore.updateJob` fires, instead of
  responding immediately as it does now.
- For production, put this behind a reverse proxy, add rate limiting, validate `PUBLIC_BASE_URL`
  matches your real domain, and consider a queue (BullMQ, etc.) instead of firing downloads
  directly off the request so concurrent downloads don't overwhelm the box.
