Download entire Spotify and YouTube playlists in bulk — parallel, DJ-ready, straight from the terminal.
Built for DJs, music collectors, and anyone tired of streaming-only libraries. Paste any mix of Spotify and YouTube links, hit enter, and walk away. Your music downloads in parallel as MP3 320k with metadata, renderable cover art, and lyrics embedded — organised into one importable .m3u8 crate per playlist.
You have a 400-song DJ playlist on Spotify. You want the actual files on your hard drive — tagged, organized, and ready for Rekordbox, Serato, Traktor, or VLC.
This tool:
- Takes your Spotify playlist URL(s)
- Matches every track to its YouTube Music equivalent
- Downloads 8 tracks in parallel (tuned for reliability — see Tuning)
- Embeds Spotify metadata (artist, title, album, album art, lyrics)
- Saves each playlist into its own folder + an importable
.m3u8crate underdownloads/ - Retries transient failures automatically and reports exactly which tracks (if any) couldn't be found
One command. DJ-ready MP3 320k by default.
New in v3: paste Spotify and YouTube links together — the tool auto-detects and routes each. YouTube-only songs download via yt-dlp with square cover art; tracks Spotify can't match are auto-sourced from YouTube. Plus parallel playlists (
--jobs), per-session logs +--resume, and alibrary.pyto verify/repair thumbnails and clean junk.From v2: per-playlist
.m3u8crates, MP3 320k default (OPUS loads in no DJ software), auto-retry, accurate reporting,--sync,--update, credentials-path fix,reorganize.py,spotdl_eval.py. SeeANALYSIS.mdfor the full before/after.
# Mix Spotify + YouTube in one command (auto-routed):
python spotidownload.py <spotify_playlist_url> <youtube_video_url> <youtube_playlist_url>
# Download 2 playlists at once (threads auto-split to stay ban-safe):
python spotidownload.py URL1 URL2 --jobs 2
# Continue an interrupted session:
python spotidownload.py --resume
# Fast resume for big playlists (RECOMMENDED for any re-run): fetch each track list
# once, diff against disk, download ONLY what's missing — no full re-scan, no
# Spotify rate-limit spiral. Unfindable tracks auto-fallback into the crate:
python spotidownload.py URL1 URL2 URL3 --diff
# Make your existing OPUS thumbnails show up (opus art doesn't render):
python library.py verify # see the report
python library.py repair --apply # convert to MP3 320k with square art
python library.py clean --apply # remove interrupted-download junk- Auto-routing: Spotify links → spotdl (metadata + matched audio). YouTube/YT-Music links → yt-dlp direct. A
watch?v=…&list=…link grabs just that video; a pureplaylist?list=…link grabs the whole playlist. - Auto-fallback: if a Spotify track has no YouTube-Music match, it's searched for on YouTube and dropped into
downloads/Recovered from YouTube/. Disable with--no-fallback. - Logs: every run writes
logs/session-<id>.json+ per-source.logfiles so you can see exactly what happened and resume.
Going in with 200–400 songs? Read PRACTICES.md first — how to run playlists in parallel without getting 403'd, and what to do when yt-dlp, Deno, Spotify rate-limits, or a kill mid-crate happens. Written from a live three-playlist night that wrote 432 files.
- Quick Start
- Requirements
- Installation
- Spotify Developer Setup
- Usage
- YouTube Premium (Higher Quality)
- Output Formats
- Examples
- Field practices (parallel + failures)
- FAQ & Troubleshooting
- How It Works
- Tips for DJs
- Contributing
- License
If you already have Python, pip, and ffmpeg installed:
pip install spotdl yt-dlp
git clone https://github.com/ckryptickunal/spotidownloader.git
cd spotidownloader
python spotidownload.pyPaste your playlist link when prompted. Done.
| Tool | Version | Purpose |
|---|---|---|
| Python | 3.10+ | Runs the script |
| spotdl | Latest | Spotify metadata + YouTube matching |
| yt-dlp | Latest | Downloads audio from YouTube Music |
| ffmpeg | Any recent | Audio conversion & metadata embedding |
| Deno | via spotdl --download-deno |
Decrypts some YouTube streams (yt-dlp fails without it) |
Optional but recommended:
| Tool | Purpose |
|---|---|
| YouTube cookies | Unlocks 256kbps OPUS with YouTube Premium |
| mediainfo | Verify audio quality after download |
# Install Homebrew if you don't have it
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Install Python and ffmpeg
brew install python@3.11 ffmpeg
# Install the download tools
pip3 install spotdl yt-dlp
# Clone this repo
git clone https://github.com/ckryptickunal/spotidownloader.git
cd spotidownloader# Install system dependencies
sudo apt update
sudo apt install -y python3 python3-pip python3-venv ffmpeg
# Install the download tools
pip3 install spotdl yt-dlp
# Clone this repo
git clone https://github.com/ckryptickunal/spotidownloader.git
cd spotidownloader- Download and install Python 3.11+ — check "Add to PATH"
- Download and install ffmpeg — add to PATH
- Open PowerShell:
pip install spotdl yt-dlp
git clone https://github.com/ckryptickunal/spotidownloader.git
cd spotidownloaderpython3 -m venv .venv
source .venv/bin/activate # macOS/Linux
# .venv\Scripts\Activate.ps1 # Windows PowerShell
pip install spotdl yt-dlpOr use the included setup script:
chmod +x setup.sh
./setup.shYou need a free Spotify Developer App to authenticate API requests. This takes 2 minutes.
- Go to developer.spotify.com/dashboard
- Log in with your Spotify account
- Click Create App
- Fill in the fields:
| Field | Value |
|---|---|
| App Name | SpotDL |
| App Description | Spotify Downloader |
| Redirect URI | http://127.0.0.1:9900/ |
- Under Which API/SDKs are you planning to use?, select Web API only
- Make sure the app is in Development mode
- Open your app in the dashboard
- Click Settings
- Copy your Client ID
- Click View Client Secret and copy your Client Secret
Run the downloader for the first time — it will ask for your Client ID and Secret:
── First-Time Setup ──
Paste your Spotify Client ID: ████████████████████████████████
Paste your Spotify Client Secret: ████████████████████████████████
[OK] Credentials saved to ~/.spotdl/config.json
Credentials are saved locally and never transmitted anywhere except Spotify's API.
Just run the script with no arguments. It will keep asking for playlist URLs until you quit:
python spotidownload.py ╔═══════════════════════════════════════════════════╗
║ Spotify Bulk Playlist Downloader ║
║ Powered by spotdl + yt-dlp ║
╚═══════════════════════════════════════════════════╝
Output : /Users/you/spotify-bulk-downloader/downloads
Format : mp3 @ 320k
Threads : 8 Retries: 1
Layout : one folder + .m3u8 crate per playlist
Paste a Spotify playlist URL (or 'q' to quit): https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M
Downloading: https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M
...
================================================================
Today's Top Hits
New this run : 48
In crate : 50
Failed : 2
Paste a Spotify playlist URL (or 'q' to quit): q
Bye!
Pass the URL as an argument — great for scripting or one-off downloads:
python spotidownload.py https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5Mpython spotidownload.py [URL ...] [OPTIONS]
Positional:
url Spotify and/or YouTube playlist/album/video URL(s)
(omit for interactive mode)
Options:
-t, --threads N Total parallel downloads (default: 8)
-j, --jobs N Concurrent playlist processes, max 3 (default: 1;
threads are split across them)
-o, --output DIR Output directory (default: ./downloads)
-f, --format FORMAT Audio format: mp3, flac, ogg, opus, m4a, wav
(default: mp3)
-b, --bitrate RATE Bitrate: auto, 128k, 256k, 320k (default: 320k)
-c, --cookies FILE Path to YouTube cookies (Netscape format)
-r, --retries N Extra passes for transient failures (default: 1)
--diff Fast resume: download only tracks missing on disk
--resume Continue the last session's unfinished links
--no-fallback Don't YouTube-search unmatched Spotify tracks
--update Upgrade spotdl + yt-dlp
--setup Re-run first-time setup
-h, --help Show help
Full parallel / failure playbook: PRACTICES.md.
Without cookies, you get ~128kbps OPUS (format 251) — still very good quality.
With YouTube Premium cookies, you unlock 256kbps OPUS (format 771) — the best available short of lossless.
-
Install a browser extension that exports cookies in Netscape format:
- Chrome: Get cookies.txt LOCALLY
- Firefox: cookies.txt
-
Go to music.youtube.com and log in with your Premium account
-
Click the extension and export cookies
-
Save the file to:
| OS | Path |
|---|---|
| macOS/Linux | ~/.spotdl/cookies.txt |
| Windows | C:\Users\YourName\.spotdl\cookies.txt |
The downloader auto-detects this file. No extra flags needed.
After downloading, check a file with mediainfo:
mediainfo --Full "Artist - Song.opus" | grep -i "bit rate"Expected output for Premium quality:
Overall bit rate : 256 kb/s
| Format | Extension | DJ software? | Best For |
|---|---|---|---|
| MP3 320k | .mp3 |
✅ All (Rekordbox/Serato/Traktor/CDJs) | Default — universal DJ compatibility |
| M4A (AAC) | .m4a |
✅ Most | Smaller than mp3 at similar quality, Apple ecosystem |
| FLAC | .flac |
✅ Modern software | Lossless container (note: source is lossy, so no real quality gain) |
| OPUS | .opus |
❌ None | Personal listening library only (VLC/mpv) — cannot load in DJ gear |
| OGG | .ogg |
Linux players | |
| WAV | .wav |
✅ All (huge files) | Production, sampling |
- Default (recommended): MP3 320kbps — universal compatibility with CDJs and all DJ software. This is now the default; just run the tool.
- Apple/space-conscious:
--format m4a— smaller files, loads in Rekordbox/Serato/Traktor. - Personal listening only (NOT for DJing):
--format opus— best quality-to-size, but no DJ software can load.opus. - Production/sampling:
--format wavor--format flac.
python spotidownload.py https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5Mpython spotidownload.py URL --format mp3 --bitrate 320k# 8 is the reliability ceiling. Drop to 4 if you see 403/429. Do not jump to 30.
python spotidownload.py URL --threads 8 --output ~/Music/DJ-Setspython spotidownload.py https://open.spotify.com/album/ALBUM_IDpython spotidownload.py https://open.spotify.com/artist/ARTIST_IDpython spotidownload.py saved(Opens a browser once for Spotify user-auth — required for private/liked content.)
One command, shared thread pool — songs from every playlist download interleaved. Do not open one terminal per playlist.
python spotidownload.py \
https://open.spotify.com/playlist/PLAYLIST_1 \
https://open.spotify.com/playlist/PLAYLIST_2 \
https://open.spotify.com/playlist/PLAYLIST_3
# Two processes, threads auto-split so YouTube load stays ~8:
python spotidownload.py URL1 URL2 --jobs 2
# Re-run a big crate — only missing tracks (avoids Spotify rate-limits):
python spotidownload.py URL1 URL2 URL3 --diffOr use interactive mode and paste them one by one.
See PRACTICES.md for the full parallel + recovery playbook.
The full symptom → fix table from live crates is in PRACTICES.md. Short version below.
Some tracks may not have a YouTube Music equivalent. These are logged in downloads/errors.txt. Common causes:
- Regional restrictions on YouTube Music
- The song is a Spotify exclusive
- Very obscure or recently released tracks
- Don't just crank threads. Above ~8, spotdl gets no throughput gain (each thread is a full yt-dlp session) and only raises your odds of a 403/429 rate-limit. The default is 8; drop to 4 if you see errors. Find your machine's real optimum with
spotdl_eval.py. - YouTube may be rate-limiting your IP — wait, or add Premium cookies (
--cookies). - Run
python spotidownload.py --update— a stale yt-dlp is the #1 cause of stalls/errors.
Your cookies may have expired. Re-export them from your browser and replace cookies.txt.
- Double-check your Client ID and Secret at developer.spotify.com/dashboard
- Re-run setup:
python spotidownload.py --setup
Without YouTube Premium cookies, you're limited to ~128kbps. See YouTube Premium section for how to unlock 256kbps.
chmod +x spotidownload.py
python3 spotidownload.py # use python3 explicitlypip install --upgrade spotdl yt-dlpDo this regularly — YouTube frequently changes their API and yt-dlp updates to match.
Yes. Finished files stay on disk. After a Ctrl+C or a kill:
python spotidownload.py --resume # same URL list as last session
python spotidownload.py URL1 URL2 URL3 --diff # any re-run of a big crate (preferred)--diff only downloads what is missing. A full re-scan of hundreds of tracks is what trips Spotify rate-limits.
spotdl --download-deno
python spotidownload.py --update
python spotidownload.py URL1 URL2 --diff| OS | Config Path |
|---|---|
| macOS/Linux | ~/.spotdl/config.json |
| Windows | C:\Users\YourName\.spotdl\config.json |
┌─────────────────┐ ┌──────────────┐ ┌──────────────┐
│ Spotify API │────▶│ spotdl │────▶│ yt-dlp │
│ (metadata) │ │ (matching) │ │ (download) │
└─────────────────┘ └──────────────┘ └──────┬───────┘
│
▼
┌──────────────┐
│ ffmpeg │
│ (convert + │
│ embed tags) │
└──────┬───────┘
│
▼
┌──────────────┐
│ downloads/ │
│ Playlist/ │
│ Artist - │
│ Title.mp3 │
└──────────────┘
- Spotify API provides the playlist track list with full metadata (artist, title, album, artwork, lyrics)
- spotdl matches each Spotify track to its YouTube Music counterpart using audio fingerprinting and metadata comparison
- yt-dlp downloads the audio stream from YouTube Music (8 tracks simultaneously by default)
- ffmpeg converts to your chosen format and embeds all the Spotify metadata
- Files land in
downloads/<Playlist Name>/asArtist - Title.mp3, plus a.m3u8crate
When a YouTube Music match fails, the track is searched on YouTube and recovered into the crate (or downloads/Recovered from YouTube/). Details and failure improvisation: PRACTICES.md.
After downloading, use Mp3Tag (Windows) or PuddleTag (Linux) to sort and batch-edit tags.
| Software | Supported Formats |
|---|---|
| Rekordbox | MP3, WAV, FLAC, M4A, AAC |
| Serato DJ | MP3, OGG, FLAC, WAV, M4A, AAC |
| Traktor | MP3, WAV, FLAC, OGG, AAC |
| VirtualDJ | MP3, WAV, FLAC, OGG, M4A, AAC |
For maximum compatibility across all DJ software, use MP3 320kbps:
python spotidownload.py URL --format mp3 --bitrate 320k- Download in MP3 format
- Copy the
downloads/folder to a USB stick formatted as FAT32 or exFAT - Plug into your CDJ and browse
Two options:
- Quick resume: just re-run the same command. Already-downloaded songs are skipped, so only new tracks get pulled.
- True sync: add
--sync. This saves a.spotdlfile per playlist and, on re-run, adds new tracks and removes ones you deleted from the Spotify playlist — keeping your crate a mirror of the playlist.
python spotidownload.py URL --sync--threads is the main speed/reliability dial. More is not better: each thread is a full YouTube download session from your one IP, so past ~8 you get no extra speed and a rising chance of rate-limiting. Defaults: 8 (drop to 4 if you hit errors).
To find your machine + network's real optimum empirically, use the included benchmark:
# SAFE anytime (metadata only, no audio, won't disturb a running download):
python spotdl_eval.py --mode meta
# Full sweep — run only when idle. Downloads a fixed 12-track sample at
# threads 4/8/16/24 and ranks them by speed × success × quality:
python spotdl_eval.py --mode audio --threads-sweep 4 8 16 24 --reps 3It writes eval_results/summary.csv and prints the winning config.
If you downloaded with v1 (everything in one flat downloads/), sort it into per-playlist crates without re-downloading:
python reorganize.py URL1 URL2 URL3 # preview (safe, moves nothing)
python reorganize.py URL1 URL2 URL3 --apply # execute| Platform | Status |
|---|---|
| macOS (Apple Silicon & Intel) | Fully supported |
| Linux (Ubuntu, Debian, Arch, Fedora) | Fully supported |
| Windows 10/11 | Fully supported |
| WSL/WSL2 | Fully supported |
spotify-bulk-downloader/
├── spotidownload.py # Main CLI — Spotify + YouTube auto-routing downloader
├── mediatools.py # Shared art/extension helpers (square cover, verify, repair)
├── library.py # Verify/repair thumbnails + clean junk in an existing library
├── reorganize.py # Sort an old flat folder into per-playlist crates
├── spotdl_eval.py # Benchmark harness to tune threads/format
├── ANALYSIS.md # Full breakdown of what changed and why
├── PRACTICES.md # Parallel download + failure improvisation (from live crates)
├── setup.sh # One-time setup script (macOS/Linux)
├── README.md # This file
├── .gitignore # Keeps downloads/, logs/, cookies out of git
├── logs/ # Per-session logs + JSON manifest (for --resume)
└── downloads/ # Your downloaded music (created automatically)
├── My House Set/ # one folder per playlist
│ ├── Artist - Song.mp3
│ └── Artist - Song.mp3
├── My House Set.m3u8 # importable crate for that playlist
├── YouTube/ # direct YouTube downloads
├── Recovered from YouTube/ # tracks Spotify couldn't match
└── errors.txt # Failed downloads log
Found a bug? Have a feature idea? Open an issue or submit a PR.
- Fork the repo
- Create your branch:
git checkout -b feature/my-feature - Make your changes
- Submit a pull request
MIT License. See LICENSE for details.
Use this tool responsibly. Support the artists you love by attending their shows, buying merch, and purchasing music when you can.
Built on top of these incredible open-source projects:
- spotdl — Spotify metadata + YouTube matching
- yt-dlp — YouTube audio downloader
- ffmpeg — Audio conversion & metadata embedding
Made with love for the DJ community. Take back your music.