agentsclimarketplace

Tidal

Skill aalzubidy/spotify-to-tidal/tidal

Migrate your entire Spotify library to Tidal — playlists, liked songs, albums & followed artists — using an AI coding assistant (Claude Code, Cursor, Cline). ISRC-accurate matching, resumable, append-only/idempotent. No web service.From the repository description

Install
npx -y skills add aalzubidy/spotify-to-tidal --skill tidal

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

6.6 KB, ~1.8k tokens by cl100k_base, as published. Nobody here has run it

Tidal Import Phase

Imports a Spotify export into Tidal using the official TIDAL v2 API (openapi.tidal.com/v2) with Authorization-Code + PKCE auth.

Matching is exact: every track is resolved by its ISRC via GET /tracks?filter[isrc]=... (batched), not by fuzzy text search. Albums match by UPC barcode, artists by name. All matches are written to a resumable cache (tidal-match-cache.json) so a killed or rate-limited run picks up where it left off instead of starting over.

⚠️ All writes go to Tidal only. Never write, modify, or delete anything on Spotify.

Why the official API

Tidal's internal api.tidal.com/v1 API has no ISRC filter, which used to force fragile "search album → fuzzy match → guess track" logic (slow, error-prone, and aggressively rate-limited). The official v2 API supports exact ISRC/barcode lookup in batches, so ~6.5k unique tracks resolve in a few hundred requests. The internal v1 path and device-code flow have been retired.

Prerequisites

  • Python 3.8+ and requests (pip install requests)
  • A TIDAL developer app (https://developer.tidal.com) with:
    • TIDAL_CLIENT_ID / TIDAL_CLIENT_SECRET in the working-dir spotify-to-tidal.env
    • a redirect URI of http://localhost:3030/callback registered on the app
    • scopes granting collection + playlists read/write
  • A completed spotify-export.json from the Spotify export phase

Step 1 — Authenticate (one-time, browser)

WORK_DIR="/path/to/spotify-tidal-working-directory"
python3 .agents/skills/spotify-to-tidal/tidal/helpers/tidal_openapi.py "$WORK_DIR" auth

Opens a TIDAL login URL, captures the redirect on localhost:3030, and saves tidal-openapi-session.json (access + refresh tokens, user id, country). Tokens auto-refresh; no need to re-auth unless the refresh token is revoked.

Sanity-check a single lookup any time:

python3 .agents/skills/spotify-to-tidal/tidal/helpers/tidal_openapi.py "$WORK_DIR" probe USRC11700001

Step 2 — Analyze (match) — read-only

Resolves every ISRC to a Tidal track and writes tidal-match-cache.json.

python3 .agents/skills/spotify-to-tidal/tidal/helpers/analyze-import.py \
  "$WORK_DIR/spotify-export.json" \
  "$WORK_DIR" \
  "$WORK_DIR/tidal-analysis.json"
  • ISRC-exact matching first (batched, --batch N, default 20 per request).
  • Capped text-search fallback for ISRC misses (--fallback-cap N, or --no-fallback).
  • Albums by UPC barcode (needs UPC in the export) then fuzzy fallback; artists by name.
  • --limit N matches only the first N ISRCs — use for a quick test slice.
  • Resumable: re-running skips everything already in tidal-match-cache.json.

tidal-analysis.json is a human-readable summary (match rates, per-playlist). Present it to the user before importing.

Step 3 — Import (write)

Reads the match cache and writes to Tidal. Append-only and idempotent — re-running skips items already in the collection (HTTP 409 treated as success).

python3 .agents/skills/spotify-to-tidal/tidal/helpers/import-all.py \
  "$WORK_DIR/spotify-export.json" \
  "$WORK_DIR" \
  "$WORK_DIR/tidal-import-result.json"

Selectively skip categories with --no-liked, --no-albums, --no-artists, --no-playlists, --no-liked-playlist.

What it does (all batched 50/request):

  1. Like tracksPOST /userCollections/{userId}/relationships/tracks
  2. Save albumsPOST /userCollections/{userId}/relationships/albums
  3. Follow artistsPOST /userCollections/{userId}/relationships/artists
  4. Create playlistsPOST /playlists
  5. Add playlist tracks in Spotify orderPOST /playlists/{id}/relationships/items (sequential batches append in order, preserving the original sequence)
  6. Create "Liked Songs" playlist → liked tracks in Spotify order as a browsable UNLISTED playlist (in addition to the liked-track favorites). Skip with --no-liked-playlist.
Spotify dataTidal actionMatch method
Liked tracksFavorite tracksISRC exact (+ capped search fallback)
Saved albumsSaved albumsUPC barcode / fuzzy; plus albums of matched tracks
Followed artistsFollowed artistsName search; plus artists of matched tracks
PlaylistsCreated, tracks in orderISRC exact
Liked Songs playlistCreated UNLISTED, liked tracks in orderISRC exact

Step 4 — Clean library (optional, destructive)

Empties categories before a fresh import. Dry-run by default.

# Dry run
python3 .agents/skills/spotify-to-tidal/tidal/helpers/clean-library.py \
  "$WORK_DIR" --categories tracks,albums,artists,playlists

# Actually delete (typed confirmation required)
python3 .agents/skills/spotify-to-tidal/tidal/helpers/clean-library.py \
  "$WORK_DIR" --categories tracks,albums,artists,playlists --confirm

Step 5 — Verify

Reads back the actual Tidal account state and cross-references the match cache.

python3 .agents/skills/spotify-to-tidal/tidal/helpers/verify-import.py \
  "$WORK_DIR/spotify-export.json" \
  "$WORK_DIR" \
  "$WORK_DIR/tidal-import-result.json" \
  "$WORK_DIR/tidal-verification-result.json"

Reports liked-track presence, per-playlist track presence, and playlists found.

Step 6 — Report

PYTHONDONTWRITEBYTECODE=1 python3 reports/generate.py --type import \
  --input "$WORK_DIR/tidal-import-result.json" \
  --output "$WORK_DIR/tidal-report.html"

Files (working directory)

FilePurpose
spotify-to-tidal.envTIDAL_CLIENT_ID/SECRET (+ Spotify creds)
tidal-openapi-session.jsonOfficial-API PKCE session (auto-refresh)
tidal-match-cache.jsonResumable ISRC/album/artist → Tidal ID cache
tidal-analysis.jsonHuman-readable match summary
tidal-import-result.jsonImport results
tidal-verification-result.jsonRead-back verification

Architecture

All scripts share tidal/helpers/tidal_openapi.py (TidalOpenAPI):

  • API base: openapi.tidal.com/v2 (official JSON:API)
  • Auth: Authorization Code + PKCE; tokens in tidal-openapi-session.json
  • Rate limiting: steady pacing (TIDAL_OPENAPI_MIN_INTERVAL, default ~8 req/s) plus exact Retry-After honoring on 429 — no burst-then-backoff
  • Matching: tracks_by_isrc, albums_by_barcode, search_tracks/artists/albums
  • Writes: add_favorites, create_playlist, add_playlist_items
  • Reads: get_collection_ids, get_user_playlists, get_playlist_item_ids

Tuning

  • --batch N on analyze: ISRCs per ISRC request (default 20).
  • TIDAL_OPENAPI_MIN_INTERVAL=0.2 in the .env: slow down if you still hit 429s.
  • --fallback-cap N / --no-fallback: control text-search fallback volume.

What ships with it: 5 files

60.3 KB alongside SKILL.md, 5 of them executable

helpers/

Keep looking

Skills are one crate of 326,835. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.