{"id":50056000,"url":"https://github.com/just5ky/spotsonic","last_synced_at":"2026-05-21T13:14:45.303Z","repository":{"id":356223930,"uuid":"1230990702","full_name":"just5ky/SpotSonic","owner":"just5ky","description":"Convert Exportify CSV playlists into Navidrome playlists","archived":false,"fork":false,"pushed_at":"2026-05-07T05:05:36.000Z","size":31,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-07T07:10:43.022Z","etag":null,"topics":["navidrome","playlist","spotify"],"latest_commit_sha":null,"homepage":"https://just5ky.github.io/SpotSonic/","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/just5ky.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-06T14:14:51.000Z","updated_at":"2026-05-07T05:05:40.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/just5ky/SpotSonic","commit_stats":null,"previous_names":["just5ky/spotsonic"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/just5ky/SpotSonic","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/just5ky%2FSpotSonic","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/just5ky%2FSpotSonic/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/just5ky%2FSpotSonic/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/just5ky%2FSpotSonic/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/just5ky","download_url":"https://codeload.github.com/just5ky/SpotSonic/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/just5ky%2FSpotSonic/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33301848,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-21T12:23:38.849Z","status":"ssl_error","status_checked_at":"2026-05-21T12:22:11.673Z","response_time":62,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["navidrome","playlist","spotify"],"created_at":"2026-05-21T13:14:42.755Z","updated_at":"2026-05-21T13:14:45.297Z","avatar_url":"https://github.com/just5ky.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SpotSonic\n\n[![CI](https://github.com/just5ky/spotsonic/actions/workflows/ci.yml/badge.svg)](https://github.com/just5ky/spotsonic/actions/workflows/ci.yml)\n[![Release](https://github.com/just5ky/spotsonic/actions/workflows/release.yml/badge.svg)](https://github.com/just5ky/spotsonic/releases)\n[![Go Report Card](https://goreportcard.com/badge/github.com/just5ky/spotsonic)](https://goreportcard.com/report/github.com/just5ky/spotsonic)\n[![Docs](https://img.shields.io/badge/docs-just5ky.github.io%2Fspotsonic-green)](https://just5ky.github.io/spotsonic)\n\nConvert [Exportify](https://github.com/watsonbox/exportify) CSV playlists into Navidrome playlists via the Subsonic API, with incremental weekly updates.\n\n## How it works\n\n**First run** — creates playlists:\n1. Reads Exportify CSV files\n2. Searches your Navidrome library via the Subsonic `search3` API\n3. Fuzzy-matches by title (60%) + artist (40%)\n4. Creates playlists in Navidrome; saves state to `spotsonic-state.json`\n\n**Subsequent runs** — updates playlists:\n1. Loads state from previous run\n2. Retries previously unmatched tracks (songs added to your library since last run)\n3. Detects new tracks added to CSV (re-exported Exportify playlist)\n4. Appends newly matched songs to existing Navidrome playlists\n5. If a playlist was deleted in Navidrome, recreates it automatically\n\n## Requirements\n\n- Go 1.22+\n- A running [Navidrome](https://www.navidrome.org/) instance\n- Exportify CSV exports from [Exportify](https://github.com/watsonbox/exportify)\n\n## Installation\n\n```bash\ngit clone https://github.com/justsky/spotsonic\ncd spotsonic\ngo build -o spotsonic .\n```\n\n## Usage\n\n```\nspotsonic [flags]\n\nFlags:\n  -server     string   Navidrome URL (e.g. http://localhost:4533)          [required]\n  -user       string   Navidrome username                                  [required]\n  -password   string   Navidrome password                                  [required]\n  -input      string   Input CSV file or directory (default \".\")\n  -state      string   State file path (default \"spotsonic-state.json\")\n  -threshold  float    Fuzzy match threshold 0.0–1.0 (default 0.80)\n  -dry-run             Preview matches without creating or modifying playlists\n  -report     string   Write currently unmatched tracks to this CSV file\n  -version             Print version and exit\n```\n\n### Examples\n\n**First run — create all playlists:**\n```bash\nspotsonic \\\n  -server http://localhost:4533 \\\n  -user admin \\\n  -password secret \\\n  -input ./spotify_playlists/ \\\n  -state spotsonic-state.json \\\n  -report unmatched.csv\n```\n\n**Subsequent runs — retry unmatched, add newly found songs:**\n```bash\nspotsonic \\\n  -server http://localhost:4533 \\\n  -user admin \\\n  -password secret \\\n  -input ./spotify_playlists/ \\\n  -state spotsonic-state.json \\\n  -report unmatched.csv\n```\n\nThe command is identical — SpotSonic automatically detects whether a playlist is new (creates it) or already exists (updates it) based on the state file.\n\n**Dry-run preview:**\n```bash\nspotsonic \\\n  -server http://localhost:4533 \\\n  -user admin \\\n  -password secret \\\n  -input my_playlist.csv \\\n  -dry-run\n```\n\n## Weekly Scheduling\n\n### WSL / Linux (cron)\n\n1. Edit `scripts/run.sh` and fill in your Navidrome credentials (or set them as environment variables).\n\n2. Install the weekly cron job:\n   ```bash\n   bash scripts/setup_cron.sh\n   ```\n   This installs a job that runs every Monday at 09:00.\n\n3. Edit the crontab to set your password:\n   ```bash\n   crontab -e\n   ```\n\n4. Enable the cron service in WSL if needed:\n   ```bash\n   sudo service cron start\n   # To start automatically on WSL launch, add the above to ~/.bashrc or /etc/wsl.conf\n   ```\n\n### Windows Task Scheduler\n\nRun SpotSonic through WSL on a weekly schedule:\n\n```powershell\n$action = New-ScheduledTaskAction `\n  -Execute \"wsl.exe\" `\n  -Argument \"-e bash /mnt/d/Github/SpotSonic/scripts/run.sh\"\n\n$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 9am\n\nRegister-ScheduledTask `\n  -TaskName \"SpotSonic Weekly\" `\n  -Action $action `\n  -Trigger $trigger `\n  -RunLevel Highest\n```\n\nSet `NAVIDROME_PASSWORD` in `scripts/run.sh` or pass it via the task's environment.\n\n## State File\n\n`spotsonic-state.json` is the key to incremental updates. It tracks:\n- Navidrome playlist ID per CSV (so songs are appended to the right playlist)\n- Spotify URI → Navidrome song ID for every matched track (prevents duplicates)\n- Full details of every unmatched track (retried on next run)\n\nKeep the state file alongside your CSV files. If lost, the next run recreates all playlists from scratch.\n\nThe state file is gitignored by default. Do not commit it.\n\n## CSV Format\n\nSpotSonic expects the default [Exportify](https://github.com/watsonbox/exportify) export with at least:\n\n| Column | Used for |\n|--------|----------|\n| `Track URI` | Unique identifier (deduplication across runs) |\n| `Track Name` | Search query + match scoring |\n| `Album Name` | Displayed in logs |\n| `Artist Name(s)` | Match scoring (primary artist) |\n\nAll other Exportify columns (audio features, popularity, etc.) are ignored.\n\n## Matching\n\nTracks are matched in two passes:\n\n1. Search by **title** → score candidates by title (60%) + artist (40%)\n2. If no match: search by **\"artist title\"** combined → rescore\n\nA match is accepted when the combined score meets `-threshold` (default 0.80 = 80%).\n\n**Adjust the threshold** to tune precision vs. recall:\n- Higher (e.g. `0.90`) → fewer false positives, more unmatched\n- Lower (e.g. `0.70`) → more matches, possible false positives\n\n## Playlist Names\n\nDerived from CSV filenames — underscores become spaces:\n- `My_Playlist.csv` → `My Playlist`\n- `505_vibes_but_better.csv` → `505 vibes but better`\n\n## Unmatched Report\n\nWith `-report unmatched.csv`, all currently unmatched tracks are written each run:\n\n```\nPlaylist,Track Name,Artist,Album\nMy Playlist,Some Song,Some Artist,Some Album\n```\n\nTracks listed here will be retried automatically on the next run. Use this report to identify songs genuinely missing from your local library.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjust5ky%2Fspotsonic","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjust5ky%2Fspotsonic","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjust5ky%2Fspotsonic/lists"}