AI Disclosure: Claude Code was used as assistance for this project.
Download synced (timestamped) .lrc lyric files from Spotify, Deezer, Musixmatch, LRCLIB, and YouTube — organized into Artist\Album\Track.lrc folders compatible with MusicBee, Plex, Kodi, Poweramp, and most LRC-aware players.
Migrating from spotify-lyric-downloader? This is a complete rewrite — same output format, entirely new architecture. No browser automation or extra dependencies required. See Migration Notes below.
- Multi-source waterfall — tries native source first, cascades through fallbacks automatically
- Source priority: Spotify → Deezer → LRCLIB → Musixmatch → YouTube captions
- Auto-detects URL type — paste any Spotify, Deezer, Tidal, or YouTube URL, no flags needed
- Batch downloads — full album, playlist, or artist discography from a single URL
- YouTube playlist support — expands
youtube.com/playlist?list=PL...into all tracks - Smart metadata enrichment — unknown albums filled in via LRCLIB → Spotify fallback
- Fallback transparency — logs which source was used and why a fallback occurred
- Retry missed tracks —
retrycommand re-runs failed tracks from the last session - Token auto-renewal — Musixmatch tokens refresh automatically; Spotify prompts on expiry
- Cross-platform — Windows (CMD/PowerShell), macOS, Linux, Git Bash
- Python 3.10 or newer — required. The tool uses
|union types and other 3.10+ syntax.- Check your version:
python --version(Windows) orpython3 --version(macOS/Linux) - Download from python.org if needed
- Check your version:
pip install spotipy requests python-dotenv tqdm
pip install yt-dlp # optional — enables YouTube caption source
pip install questionary # optional — nicer interactive setup wizard promptsOr install everything at once:
pip install -r requirements.txtmacOS / Linux: use
pip3andpython3instead ofpipandpythonif your system has both Python 2 and Python 3 installed.
python setup_wizard.pyThe wizard walks you through each credential interactively and writes your .env file.
Or copy .env.example to .env and fill it in manually — see Credentials below.
Minimum setup with zero credentials: LRCLIB and Musixmatch work out of the box with no setup at all. Coverage will be limited to those two sources.
Windows:
download.bat
macOS / Linux / Git Bash:
bash download.sh
# or make it executable once:
chmod +x download.sh && ./download.shDirect CLI (any platform):
python downloader.py -track https://open.spotify.com/track/...
python downloader.py -album https://www.deezer.com/us/album/...
python downloader.py -playlist https://youtube.com/playlist?list=PL...
python downloader.py -artist https://open.spotify.com/artist/...
python downloader.py -playing
python downloader.py -retryNot everything needs credentials — here is what each one unlocks:
| Credential | What it enables | Without it |
|---|---|---|
SPOTIFY_CLIENT_ID + SPOTIFY_CLIENT_SECRET |
Spotify URL downloads, metadata enrichment for non-Spotify tracks | Spotify URLs won't resolve; other sources still work |
SPOTIFY_REDIRECT_URI + OAuth setup |
Private playlists you own, -playing |
Only public Spotify playlists work; -playing unavailable |
SPOTIFY_AUTH_TOKEN |
Spotify lyrics (the most accurate synced source) | Lyrics fall back to Deezer → LRCLIB → Musixmatch → YouTube |
DEEZER_ARL |
Deezer URL downloads + Deezer lyrics | Deezer URLs won't resolve; other sources still work |
MUSIXMATCH_TOKEN |
Higher Musixmatch rate limits | Community token used automatically — works fine for normal use |
| (nothing) | LRCLIB + YouTube captions | Always available, no setup required |
Recommended minimum: add DEEZER_ARL — Deezer has the best non-English and newer-release coverage and requires only a browser cookie to set up.
Two separate Spotify auth flows are used — one for metadata (track lists from URLs), one for lyrics:
| Purpose | Credential | Required for |
|---|---|---|
| Track/album/playlist metadata | SPOTIFY_CLIENT_ID + SPOTIFY_CLIENT_SECRET |
Spotify URL downloads, -playing, metadata enrichment |
| Synced lyrics | SPOTIFY_AUTH_TOKEN |
Spotify lyrics specifically |
You can have one without the other — e.g. Spotify API keys for URL resolution but no lyrics token means the lyrics waterfall skips Spotify and tries Deezer next.
Personal playlists and -playing require an additional OAuth user token on top of the API keys. Run the wizard and choose "Set up personal playlist access" — it opens your browser once and caches the token automatically:
python setup_wizard.py
# → choose "Set up personal playlist access"This enables downloading your own private playlists and the -playing command. Spotify's algorithmic playlists (Daily Mix, Discover Weekly, daylist, Radio mixes) are dynamically generated and are not accessible via the Spotify API even with OAuth — this is a Spotify limitation, not a credentials issue.
Getting API keys (free):
- Go to developer.spotify.com → Log in → Create App
- Set any Redirect URI (e.g.
http://192.168.1.x:8888/callback— see-playingnote below) - Copy Client ID and Client Secret into
.env
Getting the Spotify lyrics token (~1 hour validity, prompted automatically when it expires):
- Open open.spotify.com in your browser and play any song
- Open DevTools (F12) → Network tab → filter requests by
color-lyrics - Click the request → Headers tab → find
authorization - Copy the value (everything after
Bearer) and paste into.envasSPOTIFY_AUTH_TOKEN=BQA...
DEEZER_ARL=your_arl_here
The ARL cookie covers both resolving Deezer URLs and fetching Deezer lyrics — one credential does both.
Getting your ARL:
- Open deezer.com and log in (requires a free or paid account)
- Open DevTools (F12) → Application tab → Cookies →
deezer.com - Find the cookie named
arland copy its value - Paste into
.envasDEEZER_ARL=your_value
Validity: ~3 months. When it expires the Deezer plugin will disable itself at startup and the waterfall falls through to LRCLIB.
No setup required — a community token is used automatically.
For higher rate limits (useful for large batch downloads):
- Open musixmatch.com in Firefox
- F12 → Network → reload the page → click the
www.musixmatch.comrequest → Cookies tab - Copy the value of
musixmatchUserToken - Set
MUSIXMATCH_TOKEN=your_tokenin.env
No credentials needed. Always available.
No credentials needed. yt-dlp must be installed (pip install yt-dlp).
Works with auto-generated and manual captions on any public YouTube video.
For age-restricted or private content, set one of these in .env:
YTDLP_BROWSER=chrome # reads cookies directly from your browser (chrome/firefox/edge/brave)
YTDLP_COOKIES_FILE=path/to/cookies.txt # Netscape-format export from a browser extensionFiles are saved to Lyrics/ by default (override with -o path):
Lyrics/
├── Pitbull/
│ └── Planet Pit (Deluxe Version)/
│ └── 02 Give Me Everything ft. Ne-Yo, Afrojack, Nayer.lrc
├── Sabrina Carpenter/
│ └── Short n' Sweet/
│ └── 07 Espresso.lrc
└── OCT/
└── On Company Time/
└── 03 POP! POP!.lrc
The folder name comes from whatever the streaming service reports as the album at the time of download. For a track released as a single before its parent album:
- Downloaded while it's a single → saved under the single name (e.g.
Artist/Song Title/) - Downloaded again after the album releases → saved under the album name (e.g.
Artist/Album Name/) - The old single-directory file is not automatically removed — use
-fto re-download and then manually delete the old directory
Spotify retroactively updates a track's album metadata when the parent album drops, so the same track URL will return the album name once it's out.
Each file includes standard LRC metadata:
[ti:Track Title]
[ar:Artist Name]
[al:Album Name]
[length:03:45]
[#:2] ← track number
[re:Spotify] ← which plugin provided the lyrics
[by:multiplatform-lyric-downloader]
[00:01.23]First line of lyrics
[00:04.56]Second line of lyrics
python downloader.py MODE URL [FLAGS]
Modes:
-track URL Single track
-album URL Full album
-playlist URL Full playlist (Spotify, Deezer, YouTube)
-artist URL Artist discography
-playing Currently playing Spotify track (requires OAuth)
-retry Re-run failed tracks from last session
Info:
-check Show resolver/plugin status and exit
-list-plugins List all lyrics plugins and their status
-list-resolvers List all metadata resolvers and their status
Flags:
-source NAME Force a specific lyrics source:
spotify | deezer | lrclib | musixmatch | youtube
-f Re-download existing files
-v Verbose / debug output
-o PATH Custom output directory (default: Lyrics/)
-delay SECONDS Delay between tracks (default: 0.5s)
For each track, lyrics are fetched in priority order:
- Pass 1 — Synced only: Spotify → Deezer → LRCLIB → Musixmatch → YouTube
- Each source is tried; only timestamped (
[mm:ss.xx]) results are accepted - If the URL is from Deezer, Deezer is moved to the front automatically
- If the URL is from YouTube, YouTube is moved to the front
- Each source is tried; only timestamped (
- Pass 2 — Plain text fallback: if no synced lyrics found anywhere, plain text is accepted from any source
- Metadata enrichment: if album or track number are unknown after lyrics fetch, LRCLIB then Spotify are queried for metadata only
A log line shows which source was used and when fallback occurs:
[Spotify] Matched 'Espresso' → Sabrina Carpenter / Short n' Sweet (track #1)
ℹ Synced lyrics not available via Deezer — using LRCLIB as fallback source
Python setup:
- Download Python 3.10+ from python.org
- During install, check "Add Python to PATH" — required for
pythonto work in CMD/PowerShell - Verify: open CMD and run
python --version
Running the tool:
Use download.bat for the interactive launcher — double-click it or run it from any terminal. It opens a PowerShell session that handles everything including URLs containing & characters (no quoting needed).
If running python downloader.py directly from CMD, URLs containing & must be quoted:
python downloader.py -track "https://youtube.com/watch?v=ID&list=..."
This is not an issue in the launcher or in PowerShell.
Dependencies:
pip install spotipy requests python-dotenv tqdm yt-dlp
Python setup:
macOS does not ship with Python 3.10+. Install it one of two ways:
Option A — Homebrew (recommended):
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install pythonOption B — installer from python.org.
After installing, use python3 and pip3 (not python/pip, which may point to the old system Python 2):
python3 --version # should say 3.10 or newer
pip3 install spotipy requests python-dotenv tqdm yt-dlpRunning the tool:
bash download.sh
# or make executable once:
chmod +x download.sh
./download.shIf macOS Gatekeeper blocks the script with "cannot be opened because it is from an unidentified developer", clear the quarantine flag:
xattr -d com.apple.quarantine download.shThe setup wizard and direct CLI use python3:
python3 setup_wizard.py
python3 downloader.py -track https://...Note: macOS testing on this version is community-reported. Core functionality is confirmed working; edge cases on older macOS versions or non-Homebrew Python setups may vary.
Python setup:
Most distributions ship with Python 3, but you may need to install pip:
Ubuntu / Debian:
sudo apt update
sudo apt install python3 python3-pipFedora / RHEL:
sudo dnf install python3 python3-pipArch:
sudo pacman -S python python-pipThen install dependencies:
pip3 install spotipy requests python-dotenv tqdm yt-dlpRunning the tool:
bash download.sh
# or:
chmod +x download.sh && ./download.shDirect CLI:
python3 downloader.py -track https://...Git Bash ships with Git for Windows and runs download.sh natively.
Requirements:
- Python must be installed on Windows and on your PATH (same as the Windows setup above)
- Git Bash inherits the Windows PATH, so if
python --versionworks in CMD it will work here too
Running the tool:
bash download.shDirect CLI:
python downloader.py -track https://...
# or if python3 is the name on your system:
python3 downloader.py -track https://...If you see
winpty: command not founderrors when running interactive Python scripts, prefix withwinpty:winpty python setup_wizard.py. The launcher handles this automatically.
- Deezer synced lyrics: only available for tracks where Deezer has indexed sync data. New releases often have plain text only for the first few weeks — this is a Deezer data gap, not a bug. The waterfall falls through to LRCLIB or Musixmatch automatically.
- Spotify lyrics token: expires after ~1 hour. The downloader detects this at startup and prompts for a new token before a batch run begins.
- YouTube captions: quality varies — auto-generated captions are not always accurate, and non-English tracks may have no captions at all.
- Spotify algorithmic playlists: Daily Mix, Discover Weekly, daylist, and Radio mixes are dynamically generated by Spotify's servers and are not accessible via the Spotify API — even with full OAuth credentials. This is a hard Spotify limitation; no workaround exists.
-playingand private playlists: require an OAuth user token beyond the API keys. Runpython setup_wizard.py→ "Set up personal playlist access" to authorize once. Spotify no longer acceptslocalhostas a redirect URI — you must use your machine's LAN IP (e.g.http://192.168.1.50:8888/callback), added exactly to your Spotify app's Redirect URIs setting. The wizard auto-detects your LAN IP.- YouTube playlists: mix/radio URLs (
&list=RD...) are not playlists — only the current video downloads. Use a proper playlist URL (youtube.com/playlist?list=PL...). - Rate limits: LRCLIB has no rate limit; Musixmatch community token can hit limits on heavy use (auto-renews once, then gracefully skips); Spotify lyrics ~60 requests/minute. If Spotify lyrics returns a 429, the remaining tracks in that run skip Spotify lyrics and fall back through the waterfall — the cooldown clears automatically for the next run.
- Singles becoming albums: if you download a track while it is only available as a single, it saves under the single name. Once the parent album is released, re-downloading will save to the album directory — the single-directory file must be removed manually.
# ── Spotify API (metadata + OAuth for -playing) ───────────────────────────────
SPOTIFY_CLIENT_ID=
SPOTIFY_CLIENT_SECRET=
# Spotify no longer accepts localhost — use your LAN IP:
SPOTIFY_REDIRECT_URI=http://192.168.x.x:8888/callback
# ── Spotify lyrics Bearer token (~1hr, auto-prompted on expiry) ───────────────
SPOTIFY_AUTH_TOKEN=
# ── Deezer ARL cookie (~3 months, covers URL resolution + lyrics) ─────────────
DEEZER_ARL=
# ── Musixmatch usertoken (optional, community token used by default) ──────────
MUSIXMATCH_TOKEN=
# ── YouTube auth (optional, for age-restricted/private content) ───────────────
YTDLP_BROWSER= # chrome | firefox | edge | brave | opera | safari
YTDLP_COOKIES_FILE= # path to Netscape cookies.txt export
# ── Source priority (written by setup_wizard.py, edit to reorder) ─────────────
RESOLVER_ORDER=Spotify,Deezer,YouTube
PLUGIN_ORDER=Spotify,Deezer,LRCLIB,Musixmatch,YouTubegit pull # or download and extract new zip
cp ../old-version/.env .env # bring credentials forward (Windows: copy)
pip install -r requirements.txt --upgradeThe .cache/ folder holds only derived runtime state (session tokens) and rebuilds itself automatically — no need to copy it between versions.
The original repo only downloaded the currently-playing Spotify track.
This rewrite replaces all of that:
| Original | This version | |
|---|---|---|
| Entry point | converter.py |
download.bat / download.sh / python downloader.py |
| Input | Currently-playing Spotify only | Any Spotify, Deezer, YouTube, YouTube Music URL + -playing |
| Batch support | No | Track, album, playlist, artist discography |
| Lyrics sources | Spotify only | 5-source waterfall: Spotify → Deezer → LRCLIB → Musixmatch → YouTube |
| Token handling | Manual copy each run | Cached, prompted automatically on expiry |
| Output format | Artist/Album/NN Track.lrc |
Identical |
Dependencies from the original repo that are no longer needed:
The original project listed pyautogui, pyperclip, and beautifulsoup4 (bs4) as dependencies. These were unused dead imports in that codebase and are not required here. If you have them installed from the original, you can uninstall them:
pip uninstall pyautogui pyperclip beautifulsoup4Output format is unchanged — Artist/Album/NN Track.lrc — so your existing Lyrics/ folder works as-is with this version.
To migrate your credentials:
- Copy your old
SPOTIFY_CLIENT_IDandSPOTIFY_CLIENT_SECRETinto the new.env - Run
python setup_wizard.pyto fill in the rest interactively - Your Spotify Bearer token from the old setup is still valid — copy
SPOTIFY_AUTH_TOKENacross too
MIT