Convert Exportify CSV playlists into Navidrome playlists via the Subsonic API, with incremental weekly updates.
First run — creates playlists:
- Reads Exportify CSV files
- Searches your Navidrome library via the Subsonic
search3API - Fuzzy-matches by title (60%) + artist (40%)
- Creates playlists in Navidrome; saves state to
spotsonic-state.json
Subsequent runs — updates playlists:
- Loads state from previous run
- Retries previously unmatched tracks (songs added to your library since last run)
- Detects new tracks added to CSV (re-exported Exportify playlist)
- Appends newly matched songs to existing Navidrome playlists
- If a playlist was deleted in Navidrome, recreates it automatically
git clone https://github.com/justsky/spotsonic
cd spotsonic
go build -o spotsonic .spotsonic [flags]
Flags:
-server string Navidrome URL (e.g. http://localhost:4533) [required]
-user string Navidrome username [required]
-password string Navidrome password [required]
-input string Input CSV file or directory (default ".")
-state string State file path (default "spotsonic-state.json")
-threshold float Fuzzy match threshold 0.0–1.0 (default 0.80)
-dry-run Preview matches without creating or modifying playlists
-report string Write currently unmatched tracks to this CSV file
-version Print version and exit
First run — create all playlists:
spotsonic \
-server http://localhost:4533 \
-user admin \
-password secret \
-input ./spotify_playlists/ \
-state spotsonic-state.json \
-report unmatched.csvSubsequent runs — retry unmatched, add newly found songs:
spotsonic \
-server http://localhost:4533 \
-user admin \
-password secret \
-input ./spotify_playlists/ \
-state spotsonic-state.json \
-report unmatched.csvThe command is identical — SpotSonic automatically detects whether a playlist is new (creates it) or already exists (updates it) based on the state file.
Dry-run preview:
spotsonic \
-server http://localhost:4533 \
-user admin \
-password secret \
-input my_playlist.csv \
-dry-run-
Edit
scripts/run.shand fill in your Navidrome credentials (or set them as environment variables). -
Install the weekly cron job:
bash scripts/setup_cron.sh
This installs a job that runs every Monday at 09:00.
-
Edit the crontab to set your password:
crontab -e
-
Enable the cron service in WSL if needed:
sudo service cron start # To start automatically on WSL launch, add the above to ~/.bashrc or /etc/wsl.conf
Run SpotSonic through WSL on a weekly schedule:
$action = New-ScheduledTaskAction `
-Execute "wsl.exe" `
-Argument "-e bash /mnt/d/Github/SpotSonic/scripts/run.sh"
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 9am
Register-ScheduledTask `
-TaskName "SpotSonic Weekly" `
-Action $action `
-Trigger $trigger `
-RunLevel HighestSet NAVIDROME_PASSWORD in scripts/run.sh or pass it via the task's environment.
spotsonic-state.json is the key to incremental updates. It tracks:
- Navidrome playlist ID per CSV (so songs are appended to the right playlist)
- Spotify URI → Navidrome song ID for every matched track (prevents duplicates)
- Full details of every unmatched track (retried on next run)
Keep the state file alongside your CSV files. If lost, the next run recreates all playlists from scratch.
The state file is gitignored by default. Do not commit it.
SpotSonic expects the default Exportify export with at least:
| Column | Used for |
|---|---|
Track URI |
Unique identifier (deduplication across runs) |
Track Name |
Search query + match scoring |
Album Name |
Displayed in logs |
Artist Name(s) |
Match scoring (primary artist) |
All other Exportify columns (audio features, popularity, etc.) are ignored.
Tracks are matched in two passes:
- Search by title → score candidates by title (60%) + artist (40%)
- If no match: search by "artist title" combined → rescore
A match is accepted when the combined score meets -threshold (default 0.80 = 80%).
Adjust the threshold to tune precision vs. recall:
- Higher (e.g.
0.90) → fewer false positives, more unmatched - Lower (e.g.
0.70) → more matches, possible false positives
Derived from CSV filenames — underscores become spaces:
My_Playlist.csv→My Playlist505_vibes_but_better.csv→505 vibes but better
With -report unmatched.csv, all currently unmatched tracks are written each run:
Playlist,Track Name,Artist,Album
My Playlist,Some Song,Some Artist,Some Album
Tracks listed here will be retried automatically on the next run. Use this report to identify songs genuinely missing from your local library.
MIT