This repository has no description
TypeScript 93%
CSS 7%
JavaScript <1%

README.md

Music Migrator #

A local web app for moving your music library between streaming platforms: liked songs, playlists (with track order preserved), followed artists, and saved albums. Supports Spotify and Tidal as both source and destination — including merging one Spotify account's library into a different Spotify account — and Last.fm as a read-only source (loved tracks) plus an assist for disambiguating tricky cross-platform matches.

Features #

  • Spotify ↔ Tidal, either direction, for liked songs / playlists / followed artists / saved albums
  • Spotify → Spotify account merge (union of both libraries, deduplicated)
  • Last.fm as an additional read-only source (loved tracks), and as a matching assist
  • Matching engine: ISRC exact match → cached decision reuse → fuzzy title/artist/duration scoring → Last.fm-assisted rescoring for medium-confidence matches → manual review queue
  • Review queue: ambiguous matches are held for you to confirm, pick an alternate, search manually, or skip — not silently dropped
  • Live progress over SSE, with per-category stats (matched / already present / needs review / failed)
  • Resumable: safely rerunnable (dedups against the destination's live state every run), and a job orphaned by a server restart is automatically relaunched from where it left off
  • Rate-limit aware: automatic retry with backoff on 429s, capped so a long ban fails fast instead of hanging

Setup #

  1. npm install
  2. Copy .env.local.example to .env.local and fill in credentials for whichever platforms you want to use (Spotify is required as a baseline; Tidal and Last.fm are optional). The example file has the exact redirect URIs and scope lists each platform needs.
  3. npm run dev
  4. Open http://localhost:3000/accounts and connect your accounts.
  5. Go to http://localhost:3000/migrate/new to start a migration.

Platform notes #

  • Spotify: register an app at https://developer.spotify.com/dashboard. Uses OAuth Authorization Code + PKCE (no client secret).
  • Tidal: register an app at https://developer.tidal.com. Tidal only grants scopes explicitly approved in the app's dashboard settings — requesting an unapproved scope fails the whole authorize request, not just that scope, so make sure the scopes listed in .env.local.example are all added there first.
  • Last.fm: create an API account at https://www.last.fm/api/account/create. Uses Last.fm's own web-auth flow (predates PKCE/OAuth2), so there's no redirect URI to register in advance. Last.fm is read-only — it can be a migration source, never a destination.

Connected account tokens and job state are stored locally in ./data/app.db (SQLite), with OAuth tokens encrypted at rest using a key in ./data/.master-key. Both are gitignored.

Architecture #

  • lib/adapters/ — one MusicPlatformAdapter implementation per platform (Spotify, Tidal, Last.fm), all normalizing to the canonical types in lib/types/model.ts
  • lib/matching/ — the reconciliation engine (engine.ts for tracks, entity-matching.ts for artists/albums), scoring, and Last.fm disambiguation
  • lib/jobs/ — the migration runner, one scope-runner per library category, SSE progress emitter, and the startup sweep that resumes orphaned jobs (instrumentation.ts)
  • lib/db/ — SQLite persistence: accounts (encrypted tokens), jobs, match cache (so reruns don't re-ask about the same track), review queue
  • app/ — Next.js App Router pages and API routes

Known limitations #

  • Playlist/account-merge dedup is name-based for playlists and canonical-ID-based for tracks/artists/albums — not a perfect identity match in every edge case
  • No automated tests beyond the matching engine (tests/matching/) — everything else has been verified manually against real accounts
  • Tidal's Open API is in beta; endpoint behavior may shift upstream

Scripts #

  • npm run dev — start the dev server
  • npm run build / npm start — production build/run
  • npm test — run vitest unit tests