# ytmdl (C) A terminal UI for downloading audio from YouTube / YouTube Music video, playlist, and album URLs, written in C with ncurses. All the actual extraction/downloading is done by shelling out to [yt-dlp](https://github.com/yt-dlp/yt-dlp) — this program is the queue, the TUI, and the orchestration around it. ## Requirements - gcc (or another C11 compiler) and `make` - `ncursesw` development headers (`libncursesw5-dev` / `ncurses-devel` depending on distro) - `pthread` (part of glibc on Linux) - [`yt-dlp`](https://github.com/yt-dlp/yt-dlp) on your `PATH` — needs to be reasonably recent, since YouTube changes frequently break older releases. An outdated version can fail to resolve *any* playlist and surface confusing errors (e.g. "YouTube Music is not directly supported" or zero entries found) even though the URL itself is fine — it's the extractor being too old to parse YouTube's current page format, not an actual YouTube Music limitation. Update with `yt-dlp -U` or `pip install -U yt-dlp`. `./start.sh` (from the repo root) automatically prefers the copy kept up to date in `../.venv/bin` over an older system package, if present. - [ffmpeg](https://ffmpeg.org/) on your `PATH` (yt-dlp uses it for audio extraction, thumbnail embedding, and metadata tagging) - A [Nerd Font](https://www.nerdfonts.com/) in your terminal — the queue status column and the folder browser use Nerd Font glyphs; without one they'll show as missing-glyph boxes - A terminal with color support for the best look (color-coded status icons/progress bars, focus-highlighted panel borders); the UI still works without it, just flatter ## Build ```bash make ``` Produces a single `ytmdl` binary in this directory. ## Run ```bash ./ytmdl ``` or from the repo root: `./start.sh` (builds if needed, then runs it). Paste a video, playlist, or album URL into the URL field and press Enter. Playlists and albums are expanded into individual tracks, each downloaded and tagged separately, with up to 2 downloads running concurrently. ### Options ``` ./ytmdl [-o output_dir] [-f audio_format] ``` - `-o` — where files are saved (default `~/Music/ytmdl`) - `-f` — output audio format: `mp3`, `m4a`, `flac`, `opus`, `wav`, `vorbis` (default `mp3`) ### Keybindings Global (work no matter what has focus): - `Ctrl+O` — open a folder browser to change the download directory - `Up`/`Down` — move selection, `Enter`/`Right` — open the highlighted folder, `Backspace`/`Left` — go to the parent folder, `s` — select the currently browsed folder, `Esc` — cancel - `Ctrl+F` — toggle "create a folder per playlist/album" (on by default; named after the playlist/album title; single videos are never nested) - `Ctrl+N` — clear finished: remove every successfully completed row from the queue (errored rows stay, so you can still see what failed and retry with `r`) - `Ctrl+Q` — quit - `Tab` — switch focus between the URL field and the queue table URL field focused: - `Enter` — submit the URL Queue table focused: - `Up`/`Down` — move the selected row - `d` / `Delete` — remove the selected row (not while it's actively downloading) - `r` — retry a row that errored ## Architecture - `queue.c/.h` — thread-safe queue of `QueueItem`s (id, url, title, subfolder, status, progress), a fixed-capacity array behind a mutex so worker threads can hold stable ids across UI-driven removals. - `downloader.c/.h` — spawns `yt-dlp` via `fork`/`exec`, parsing structured `--print`-template output (unit-separator-delimited fields) to resolve playlists, and `--newline` progress lines to track download percent/speed/eta. - `app.c/.h` — app state, settings (output dir, audio format, playlist-folder toggle), a small fixed pool of worker threads pulling from a work queue, and the actions the UI calls into (submit URL, clear finished, remove/retry). - `ui.c/.h`, `input_field.c/.h`, `dirpicker.c/.h` — ncurses rendering and input handling. - `icons.h` — Nerd Font glyph codepoints, written as `\uXXXX` escapes. There is no equivalent of Textual's command palette in this version — every action has a direct, always-available keybinding instead.