- Rust 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| patches/vergen-gitcl | ||
| specs | ||
| src | ||
| tests/integration | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
| rust-toolchain.toml | ||
librespot-dl
A fast, keyboard-driven command-line tool for downloading your Spotify library — fully tagged, correctly organized, and ready to play.
librespot-dl streams tracks through librespot, embeds complete metadata and cover art, and writes them straight into a music collection laid out exactly the way you want it. It comes in two flavors: a vim-inspired interactive queue manager for building up a session track by track, and a scriptable non-interactive mode for automation and batch downloads.
Important
librespot-dl requires an active Spotify Premium subscription and authenticates using your own account credentials. Use of this tool is subject to Spotify's Terms of Service. It does not circumvent Spotify's DRM or access controls — it only uses the streaming API surface that librespot itself exposes. You are responsible for how you use downloaded content.
✨ Features
- 🎧 Real audio, real tags — outputs fully-tagged OGG Vorbis, the direct provider stream with zero re-encoding, with per-album cover art embedded automatically.
- 📊 Playback-style progress — watch elapsed/total time and download speed per track update smoothly in real time, right alongside the queue you're managing.
- ⌨️ Vim-style queue control —
j/k/J/K/d/Enternavigate, reorder, and manage the queue right where it's drawn;a(or just pasting a URL) drops you into the query line to add more. - 🤖 Script mode —
librespot-dl -c <dir> <spotify-url>for cron jobs, wrappers, and batch grabs: point it at a track, a whole playlist/album, or pipe in a list of URLs, with clean exit codes and an optional--jsonsummary. - 📁 Configurable layout — control album/track paths with a simple token-based format string, validated up front so a bad template never wastes a download.
- ⏸️ Resumable downloads — pause and resume mid-stream; a checksum-verified partial-download record survives crashes and unclean shutdowns.
- 🐢 Speed-capped, courteous downloading — throttle to a multiple of real-time playback speed instead of hammering the CDN.
- 🛡️ Filename-safe by design — messy track metadata never corrupts a file path or escapes your collection directory; an optional
portable_filenamessetting also keeps output clean on NTFS/exFAT-mounted drives and network shares. - 🖥️ Resize-proof terminal UI — every interactive screen redraws cleanly at any width, correctly sized for wide CJK characters, down to a graceful placeholder on truly tiny terminals.
- 🔒 Collection locking — an advisory lock keeps two instances from stepping on the same collection directory.
📦 Requirements
- Linux (x86_64) — the primary supported platform for v1.
- Spotify Premium account.
- Rust stable toolchain (for building from source).
🚀 Installation
git clone <this-repository>
cd librespot-dl
cargo build --release
The binary is produced at target/release/librespot-dl.
🔑 Authentication
librespot-dl never asks for your password directly. It stores a credentials.json blob at a path resolved in this order:
--credentials <PATH>on the command line, or~/.config/librespot-dl/credentials.jsonby default.
First run (interactive mode only): if that file doesn't exist yet, librespot-dl advertises itself as a Spotify Connect device (via the same zeroconf/mDNS handshake Spotify's own apps use to find devices on your network) and prints setup instructions. Open Spotify on your phone or desktop, pick librespot-dl from the Connect device list, and it captures the resulting credentials and writes them to the resolved path automatically.
This only happens in interactive mode, since it needs a human to complete the pairing step in the Spotify app. In script mode (a URL given on the command line), a missing credentials file is a hard error — run once in interactive mode first to bootstrap it, or point --credentials at a file that already exists.
Once acquired, credentials are reused on every subsequent run. They are only ever sent to Spotify's own authentication endpoints and are never logged or persisted elsewhere.
🕹️ Usage
Interactive mode
librespot-dl -c ~/Music/Collection
Launches the interactive session — one screen, always on, no modal to open:
The hint line always tells you exactly what's live. The queue is the default focus (› marks your cursor); press a, or just paste a URL, to switch into the query line at the bottom.
Queue focus (default):
| Key | Action |
|---|---|
j / k |
Move the cursor down / up |
J / K |
Reorder the selected entry down / up |
d |
Remove the selected pending entry |
Enter |
Promote the selected entry to the front of the queue |
a |
Switch to the query line to add a track |
q / Esc |
Exit gracefully, saving session state |
Ctrl-C / Ctrl-D |
Exit immediately |
Query focus (after a, or as soon as you paste):
Type a Spotify track/playlist/album URL or a free-text search query. ←/→ move the cursor a character at a time, Ctrl-←/Ctrl-→ jump by word, Backspace/Delete edit around the cursor.
| Key | Action |
|---|---|
Enter |
Submit the query and return to the queue |
Esc |
Discard the input and return to the queue |
Ctrl-C / Ctrl-D |
Exit immediately |
Pasting works from either focus — on terminals with bracketed-paste support it's detected instantly and needs no a first.
If the process exits mid-download, relaunching against the same collection directory picks the session back up — in-flight downloads resume from their last verified byte, not from scratch.
Script mode
librespot-dl -c ~/Music/Collection https://open.spotify.com/track/<id>
librespot-dl -c ~/Music/Collection https://open.spotify.com/playlist/<id>
Give it a single track or a whole playlist/album — every track resolves and downloads in one run, using the same -n/--concurrency worker pool as interactive mode. You can also skip the URL argument entirely and pipe in a list instead (one track/playlist/album URL per line, freely mixed):
cat urls.txt | librespot-dl -c ~/Music/Collection
Progress prints to stderr and a session summary goes to stdout (add --json for a machine-readable version) — stdout itself always stays script-safe. As its first line of stderr output, script mode prints the same resolved-collection-directory line interactive mode does ([librespot-dl] collection: ...), so a cron job's logs always record which directory a given run actually used.
Ctrl-C shuts a run down gracefully, same as in interactive mode: anything mid-download is saved as a resumable partial rather than lost, so re-running the same command later picks up where it left off instead of starting over. A track that fails permanently doesn't stop the rest of the batch, but the run's exit code reflects the failure once everything else finishes.
Flags
| Flag | Description |
|---|---|
-c, --collection-dir <DIR> |
Collection root directory. Optional — falls back to collection_dir in config.yml (see Configuration); the tool exits with an error if neither is set |
--credentials <PATH> |
Path to credentials.json |
-n, --concurrency <N> |
Simultaneous download workers (overrides config) |
--speed-cap <MULTIPLIER> |
Download speed as a multiple of playback duration (overrides config) |
--json |
Emit the session summary as JSON |
--debug |
Verbose logging to stderr |
⚙️ Configuration
Global config (config.yml)
librespot-dl reads an optional user-global config file at ~/.config/librespot-dl/config.yml. It currently supports one key — a default collection directory used whenever -c/--collection-dir isn't passed:
# Default collection directory, used when -c/--collection-dir is omitted.
# Leading ~ expands to your home directory; ~otheruser is not supported.
collection_dir: ~/Music/Collection
-c/--collection-dir always wins when passed; config.yml is only read when it's omitted.
Collection config (collection.yml)
Every collection directory owns a collection.yml, written with sensible defaults on first run:
# Album directory path relative to the collection root.
# Available tokens: {artist}, {album_artist}, {album}, {year}
album_format: "{album_artist}/{album}"
# Track filename within the album directory (no extension).
# Available tokens: {title}, {track_number}, {track_number:02}, {disc_number},
# {artist}, {album_artist}, {album}, {year}, {duration_secs}
track_format: "{track_number:02} - {title}"
# Download speed cap as a multiple of playback duration.
# 1.0 = real-time (one second of audio per second of download).
speed_cap: 1.0
# Number of simultaneous download workers.
concurrency: 1
# Rewrite metadata that plain Linux filesystems allow but Windows/NTFS/exFAT don't (things
# like : or ? in a title, or a trailing dot). Only worth enabling if this collection's root
# lives on an auto-mounted NTFS/exFAT drive or network share. Off by default.
portable_filenames: false
Format strings are validated before any download starts — an unrecognized token, or a substituted value that would blow past filesystem path-length limits, is rejected up front rather than silently truncated or discovered mid-download. The same goes for metadata that can't safely become a path component at all (for example a title that's just ..): the track is skipped with a clear error instead of writing outside your collection directory or silently mangling the filename.
🚦 Exit codes
librespot-dl uses exit codes as part of its scripting contract:
| Code | Meaning |
|---|---|
0 |
Success |
1 |
User error — bad arguments, invalid config, bad URL |
2 |
Spotify/authentication error |
3 |
I/O or filesystem error |
🏗️ Development
cargo test # unit tests
cargo test --test integration # integration tests (needs real credentials)
cargo clippy -- --deny warnings # lint
cargo audit # dependency vulnerability scan
See specs/core_architecture.md for the stable internal contracts (download pipeline, provider trait, queue state machine) and CLAUDE.md for the project's development guidelines.
📜 License
GPLv2 — see LICENSE.
