playlist_to_cd is a small macOS desktop utility that turns Spotify playlist CSV exports into CD-ready output: either an MP3 CD (~700 MB, bitrate-fitted) or an Audio CD (44.1 kHz / 16-bit stereo WAV).
Burning a Spotify playlist to a physical CD requires downloading each track, normalising formats, fitting the collection to disc capacity, applying metadata, and renumbering files. This project automates that pipeline.
You export your Spotify playlist to CSV with Exportify (free, no account needed). The app then:
- Finds each track on YouTube via
yt-dlp, using smart search variants (artist + title, with fallbacks for parenthesised titles, multi-artist tracks, etc.) and tries up to 3 candidates per variant. - Validates before downloading: checks YouTube metadata first. If the duration differs from Spotify’s by more than ±10%, it skips that candidate and tries the next—no wasted downloads.
- Validates after downloading: runs
ffprobeon each file. If the actual duration still doesn’t match, the file is rejected and deleted. Rejected and failed tracks are written to CSVs so you can review. - Fits to disc: for MP3 CD mode, starts at 320 kbps and automatically steps down (256→192→160→128) until the collection fits in ~700 MB.
- Resumes: tracks progress in
state.json; re-run after a stop and it skips already-accepted tracks.
No Spotify API, no paid services, no account friction—just CSV in, CD-ready output out.
There are two entry points:
| Entry point | Interface | Scope |
|---|---|---|
main_original.py |
Tkinter GUI desktop app | Full workflow: CSV parsing, YouTube download via yt-dlp, duration validation, post-processing (MP3 CD or Audio CD) |
main.py |
CLI | Post-processing only: takes an existing directory of MP3 files and runs the MP3 CD or Audio CD pipeline |
The core logic (artist parsing, filename safety, search query building, ffmpeg operations) lives in reusable modules under core/ and modes/.
The supported product path is:
- Export the Spotify playlist to CSV with Exportify.
- Open the Tkinter desktop app.
- Choose the CSV, choose an output folder, select MP3 CD or Audio CD, and run.
That CSV-first flow is the one this project is actively optimized around.
main.pycannot drive the full CSV-to-CD workflow; download/acquisition is only available through the desktop app (main_original.py).core/query.py(search query building) is extracted but only called from the monolith; it is not yet wired intomain.py.- No web app, server mode, or headless full-workflow entry point exists.
- Requires
ffmpeg,ffprobe, andyt-dlpinstalled and available onPATH.
- Export a Spotify playlist to CSV via Exportify.
- Run
python main_original.py. - Select the CSV, choose an output folder and output mode (MP3 CD or Audio CD).
- The app downloads tracks from YouTube, validates durations, and runs the selected post-processing pipeline.
- Output is a directory of numbered, metadata-tagged files ready to burn.
If you already have a directory of MP3 files (e.g. from a previous interrupted run):
# MP3 CD mode (requires accepted duration)
python main.py --mode mp3 --processed-dir /path/to/mp3s --accepted-duration-sec 3600
# Audio CD mode
python main.py --mode audio --processed-dir /path/to/mp3s
Full workflow (GUI):
- Export your Spotify playlist as a CSV using Exportify and save as CSV.
- Run
python main_original.py, choose the CSV and an output folder, pick "MP3 CD" or "Audio CD", then Start. - When finished, burn the output folder to disc (for Audio CD, burn the
audio_cd_ready/subfolder).
Post-processing only (CLI):
# Folder of MP3s → MP3 CD (~700 MB). Example: 1 hour of accepted audio.
python main.py --mode mp3 --processed-dir ./my_tracks --accepted-duration-sec 3600
# Same folder → Audio CD WAVs
python main.py --mode audio --processed-dir ./my_tracksAn experimental branch, feat/spotify-source, explored a direct Spotify-connected route by adding spotipy plus local environment and callback configuration.
That route is not the chosen product direction for this app:
- It adds Spotify app registration, local callback setup, and per-user credential/config management.
- It creates more account/auth friction for a simple local utility.
- It is a worse fit for a packaged desktop app than a plain CSV import flow.
- It still does not remove the downstream dependency on
yt-dlp/YouTube matching for acquisition.
The decision is to keep the app centered on a simple user-owned workflow: export the playlist to CSV, then feed that CSV back into the desktop app. That path is more practical, easier to support, and avoids tying normal use to Spotify-specific auth requirements.
- Python 3.9+
ffmpegandffprobeon PATHyt-dlpon PATH (for the full workflow viamain_original.py)
pip install -r requirements.txt
(requirements.txt contains pytest and ruff for testing and linting.)
ruff check .
ruff check . --fix # auto-fix safe issuespytestTest configuration lives in pyproject.toml. The suite currently includes 83 tests, including coverage for the extracted modules plus a minimal GUI smoke test. Tests mock external boundaries (ffmpeg, ffprobe, subprocess, filesystem) and do not require network access.
To package the GUI as a standalone macOS .app and local .dmg:
-
Prerequisites: PyInstaller (
pip install pyinstaller) and the external binaries inpackaging/bin/. Seepackaging/bin/README.mdfor how to obtainffmpeg,ffprobe, andyt-dlp. -
Build:
./packaging/build_macos.sh
-
Output:
dist/playlist_to_cd.appdist/playlist_to_cd_0.1.0.dmg
The build script uses the project .venv if it exists and has PyInstaller installed.
- Open
dist/playlist_to_cd_0.1.0.dmg. - Copy
playlist_to_cd.apptoApplications(or another local folder). - Launch the app from Finder.
Because this build is unsigned and not notarized, macOS Gatekeeper may block first launch. If that happens, right-click the app and choose Open. If needed, remove quarantine:
xattr -dr com.apple.quarantine "/Applications/playlist_to_cd.app"- Preferred: Build with
ffmpeg,ffprobe, andyt-dlppresent inpackaging/bin/so they are bundled inside the app. - Fallback: If they are not bundled, the target Mac must provide those binaries on
PATH, or the app will show missing-dependency errors at startup.