Open Source · MIT License

Spotify playlists
into Navidrome,
automatically.

Bulk-convert Exportify CSV exports into Navidrome playlists via the Subsonic API. Fuzzy-matches tracks. Retries unmatched songs on weekly runs.

Everything you need, nothing you don't

🎵

Bulk Convert

Process a single CSV or an entire directory of 100+ Exportify playlists in one command.

🔍

Fuzzy Matching

Two-pass Levenshtein search — title + artist weighted scoring. Handles typos, remasters, and alternate versions.

🔄

Incremental Updates

Persistent state file tracks every unmatched track. Re-run weekly as your library grows — previously unmatched songs are retried automatically.

📅

Schedulable

Includes a run.sh wrapper and setup_cron.sh for hands-free weekly runs via WSL cron or Windows Task Scheduler.

📋

Unmatched Report

Exports every track that couldn't be matched to a CSV. Use it to identify what's missing from your local library.

Zero Dependencies

Pure Go standard library — no external runtime deps. Single static binary for Linux, macOS, and Windows.

Simple by design

1

Parse CSVs

Reads Exportify exports. Extracts Track Name, Artist, Album, and Spotify URI.

2

Search Library

Calls Navidrome's search3 API. Two passes: title-only, then artist + title.

3

Score & Match

Levenshtein similarity: title (60%) + artist (40%). Accepts matches above threshold.

4

Create Playlist

Matched song IDs sent to createPlaylist. Large playlists chunked to 200 songs.

5

Save State

Matched URIs + unmatched tracks persisted to JSON. Next run retries the unmatched.

Get up and running

Requires Go 1.22+ and a running Navidrome instance.

Build from source

# Clone and build
git clone https://github.com/justsky/spotsonic
cd spotsonic
go build -o spotsonic .

Install with go install

go install github.com/justsky/spotsonic@latest

Download binary

Pre-built binaries for Linux, macOS, and Windows are available on the Releases page.

CLI Reference

Flag Default Description
-server required Navidrome URL, e.g. http://localhost:4533
-user required Navidrome username
-password required Navidrome password
-input . CSV file or directory of CSV files
-state spotsonic-state.json State file for incremental updates
-threshold 0.80 Fuzzy match threshold 0.0–1.0. Lower = more matches, higher = stricter.
-dry-run false Preview matches without creating or modifying playlists
-report Write unmatched tracks to this CSV file

First run — create all playlists

spotsonic \
  -server   http://localhost:4533 \
  -user     admin \
  -password secret \
  -input    ./spotify_playlists/ \
  -state    spotsonic-state.json \
  -report   unmatched.csv

Subsequent runs — the same command

SpotSonic detects first-run vs update automatically from the state file. Run the exact same command weekly.

spotsonic \
  -server   http://localhost:4533 \
  -user     admin \
  -password secret \
  -input    ./spotify_playlists/ \
  -state    spotsonic-state.json \
  -report   unmatched.csv

How fuzzy matching works

Each candidate from Navidrome is scored by combining title and artist similarity.

Title match
60%
Artist match
40%

Similarity is computed with Levenshtein edit distance after normalizing both strings (lowercase, strip punctuation, collapse whitespace). A two-pass search strategy maximizes recall:

# Pass 1: broad — search by title, score by artist
search3(query="Blinding Lights", songCount=20)

# Pass 2: narrow — if no match, try artist + title combined
search3(query="The Weeknd Blinding Lights", songCount=20)

Tuning: default threshold 0.80 is a good balance. Lower to 0.70 for more aggressive matching (may produce false positives). Raise to 0.90 for strict matching (more tracks left unmatched but no wrong songs).

Grows with your library

The state file is what makes weekly runs work. It never adds duplicates and always picks up where it left off.

📄

CSV tracks

All Spotify URIs from Exportify

🗂️

State file

URI → Navidrome ID map
+ unmatched list

🎵

Navidrome

Playlist updated
no duplicates

What happens each run:

[update] "Chill Mix" — retry 8 unmatched, 3 new tracks
  ✓ (retry) Blinding Lights — The Weeknd (91%)   # now in library
  ✓ (retry) Levitating — Dua Lipa (95%)
  ✗ (retry) Some Obscure Track — Artist (best 38%)  # still missing
  ✓ (new)   New Song — Artist (89%)               # added to Spotify playlist
  added 3 songs to playlist id=a1b2c3d4

If a Navidrome playlist is accidentally deleted, SpotSonic finds it by name via getPlaylists or recreates it from the full matched-URIs history in the state file.

Set it and forget it

Run SpotSonic automatically every week so new songs added to your library are picked up without any manual effort.

Edit scripts/run.sh with your Navidrome credentials, then install the cron job:

# 1. Make scripts executable
chmod +x scripts/run.sh scripts/setup_cron.sh

# 2. Install weekly cron (Monday 09:00)
bash scripts/setup_cron.sh

# 3. Edit crontab to set your password
crontab -e

# 4. Verify it's installed
crontab -l

Or add it manually — this runs every Monday at 09:00:

0 9 * * 1 NAVIDROME_PASSWORD='secret' /path/to/spotsonic/scripts/run.sh

WSL cron may need to be started manually. Add sudo service cron start to ~/.bashrc or configure it via /etc/wsl.conf.

Run SpotSonic through WSL using Windows Task Scheduler (run as Administrator):

$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  Highest

Set NAVIDROME_PASSWORD inside scripts/run.sh before registering the task. Logs are written to spotsonic.log in the project directory.