A terminal user interface (TUI) application that converts Spotify playlists and albums to local MP3 files. It accepts a Spotify URL or a CSV export, searches for each track on YouTube, downloads the audio, and embeds metadata.
This project is a complete rewrite of Spotify2MP3 built with C# / .NET 10 and Terminal.Gui.
- .NET 10 SDK
- yt-dlp — must be on
PATH - FFmpeg — required by yt-dlp for MP3 extraction
# Clone the repository
git clone git@github.com:pacyfist/Spotify2MP3.NET.git
cd Spotify2MP3.NET
# Run
dotnet run --project Spotify2MP3.NET/
# Or build first
dotnet build
cd Spotify2MP3.NET/bin/Debug/net10.0/
# Linux / macOS
./Spotify2MP3.NET
# Windows
./Spotify2MP3.NET.exe| Flag | Description |
|---|---|
--source <path or URL> |
CSV file path, Spotify playlist URL, or Spotify album URL. Pre-fills the source field in the TUI; in headless mode, this is the input. |
--folder <path> |
Output folder. Pre-fills the output field in the TUI; in headless mode, this is the destination root. |
--headless |
Run without the TUI: logs to stdout, exit code reflects success. Requires both --source and --folder. |
--deep-search <true|false> |
Toggle Deep Search for the session. Default true. |
--variants <csv> |
Comma-separated search variants (e.g. remix,acoustic). Empty string clears the list. Overrides the saved config for the session. |
--exclude <csv> |
Comma-separated words to exclude from YouTube titles (e.g. instrumental,karaoke). Empty string disables filtering. Default: instrumental. |
--duration-min <seconds> |
Reject videos shorter than this many seconds. Overrides the saved config for the session. |
--duration-max <seconds> |
Reject videos longer than this many seconds. Overrides the saved config for the session. |
--m3u <true|false> |
Generate playlist.m3u alongside the MP3s. Overrides the saved config for the session. |
--safe-mode <true|false> |
Pace downloads to avoid YouTube throttling (see Safe Mode below). Overrides the saved config for the session. |
--cover-art <true|false> |
Embed Spotify album art instead of the YouTube thumbnail. Overrides the saved config for the session. |
CLI overrides apply to the current run only; they don't write to config.json. Open the Settings dialog and click Save to persist them.
# Open the TUI with the source field pre-filled and the output field set to /music
dotnet run --project Spotify2MP3.NET/ -- --source ~/playlists/mix.csv --folder /music
# Run headlessly — no UI, logs to stdout, exit code reflects success
dotnet run --project Spotify2MP3.NET/ -- --headless \
--source https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M \
--folder /music
# Headless run with extra overrides: faster (no deep search), with cover art and an M3U
dotnet run --project Spotify2MP3.NET/ -- --headless \
--source ~/playlists/mix.csv --folder /music \
--deep-search false --cover-art true --m3u true \
--duration-min 60 --duration-max 480Headless exit codes: 0 all tracks downloaded, 1 partial failure (some tracks not found), 2 fatal error (bad input, IO failure, etc.), 130 cancelled with Ctrl+C.
- Provide a source — paste a Spotify URL into the first field, or click Browse to pick a CSV export
- Pick an output folder with the second Browse button
- Toggle Deep Search for more accurate YouTube matching (slower, but better results)
- Adjust Settings if needed (variants, duration filters, M3U, cover art, safe mode)
- Click Convert Playlist and watch progress in the log window
Downloaded MP3s are saved to a subfolder named after the playlist or album, with embedded ID3 tags (title, artist, album, track number).
Paste any of the following directly into the source field:
- Playlist URL:
https://open.spotify.com/playlist/{id} - Album URL:
https://open.spotify.com/album/{id} - Spotify URI:
spotify:playlist:{id}orspotify:album:{id} - Bare 22-character ID (treated as a playlist ID)
The app fetches the track list from Spotify's public embed page (open.spotify.com/embed/...), which requires no developer account or login. Album tracks automatically pick up the album name for ID3 tagging.
Note: This uses an undocumented Spotify endpoint and is intended for personal use only. The embed page typically returns up to ~100 tracks, so very large playlists may be truncated — fall back to the CSV path for those.
If you'd rather use a CSV export (e.g. via Exportify), the app expects these columns:
Track Name,Artist Name(s),Album Name,Duration (ms)Duration (ms) is optional but improves Deep Search match quality. Header aliases like Track name, Artist name, and Album are also accepted.
When the Deep Search checkbox is on (default), the app does more than take the first YouTube result:
- It inspects the top result and accepts it only if the title, the uploader, and the duration all match what Spotify reported (within ±10 s).
- If that fails, it scores the top three results — preferring titles that start with the track name, uploader names containing the artist, durations close to the Spotify length, and (when relevant) titles that include the requested variant.
#shortsare excluded. - If both passes fail, it falls back to a plain
ytsearch1:download.
Turn Deep Search off for fastest runs; leave it on for the best chance of getting the right version.
Open the Settings dialog (Alt+S or click the button) to configure:
| Setting | Default | Description |
|---|---|---|
| Variants | (empty) | Comma-separated search variants (e.g. remix,acoustic) |
| Exclude | instrumental |
Comma-separated words to reject in YouTube titles (e.g. instrumental,karaoke) |
| Min Duration | 30s | Reject videos shorter than this |
| Max Duration | 600s | Reject videos longer than this |
| Generate M3U | On | Write a playlist.m3u file alongside the MP3s |
| Safe Mode | Off | Pace downloads to avoid YouTube throttling (see below) |
| Use Spotify cover Art | Off | Embed real album cover from Spotify instead of YT thumb |
Settings persist in a config.json file next to the executable (AppContext.BaseDirectory, e.g. /path/to/app/config.json). The file is written only when you click Save in the Settings dialog. On launch the app reads it if present and silently falls back to defaults if missing or malformed. CLI overrides (--m3u, --variants, etc.) mutate the in-memory config for the session and are not written back.
If you set Variants to remix,acoustic, every track is downloaded twice — once searching for Title Artist remix, once for Title Artist acoustic — into separate files. Leave it empty to download each track only once. If a track title already contains instrumental, an instrumental variant is automatically prepended (regardless of the Variants setting).
By default, yt-dlp embeds the YouTube video's thumbnail. Turn on Use Spotify cover Art to replace it with the real album cover fetched from Spotify's public endpoint:
- Album inputs use the album-level cover for every track.
- Playlist inputs resolve each track's album cover individually via
open.spotify.com/embed/track/{id}. - If Spotify doesn't return an image, the YT thumbnail embedded by yt-dlp is kept as a fallback.
Safe Mode adjusts download pacing based on playlist size to avoid YouTube rate-limiting:
| Tier | Playlist Size | Delay Between Tracks | Rate Limit |
|---|---|---|---|
| Normal | < 250 tracks | 3s | 5 MB/s |
| Large | 250–499 | 8s | 2 MB/s |
| Aggressive | 500+ | 15s | 1 MB/s |
Tracks that already exist on disk are skipped without delay. The selected tier is logged at the start of each run.
The app checks each output filename before downloading. If Artist - Track.mp3 already exists in the output folder, it's logged as [OK] Already exists and skipped — making re-runs cheap and safe.
While a run is active, the bottom panel of the main window shows a live log (errors highlighted), the status label shows the current track, and the progress bar fills as tracks complete. Every line is also written to conversion.log in the output folder.
When a run finishes, a summary dialog shows the total tracks processed, how many were downloaded, and lists any that failed (track name and artist). Failed tracks are also reported in the log.
output_folder/
playlist_or_album_name/
Artist - Track.mp3
Artist - Track 2.mp3
playlist.m3u # if "Generate M3U" is on
conversion.log
Filenames are sanitized by stripping non-alphanumeric characters from the artist and title.
Tab/Shift+Tab— navigate between controlsEnter/Space— activate buttons and checkboxesAlt+B/Alt+O— focus the CSV/URL Browse / output folder Browse buttonsAlt+D— toggle Deep SearchAlt+S— open SettingsAlt+C— Convert PlaylistAlt+T— Stop (cancel the active conversion)- In the Settings dialog:
Alt+M(M3U),Alt+F(safe mode),Alt+A(cover art),Alt+Ssave,Alt+Ccancel Esc— quit the application
dotnet testThis project is licensed under the GNU Affero General Public License v3.0.

