YouTube Music to Spotify Playlist Sync
Keep a YouTube Music playlist in sync with Spotify: the tool reads the playlist through the YT Music API, finds each track on Spotify with title + artist / fuzzy matching (plus an exact ISRC pass for CSV exports that carry ISRCs), pushes only what changed, and enriches every track with Spotify and Last.fm metadata. It also exports and imports YT Music playlists and Liked Songs (JSON + CSV) without Spotify. Fully resumable, with a guided setup wizard, an interactive menu and CLI mode.
Why?
YouTube Music and Spotify don't talk to each other. If you curate playlists on one platform you either listen on two platforms or manually rebuild them. This tool automates the bridge: it reads your YT Music playlist, finds the matching tracks on Spotify, and keeps the two in sync. Only changes since the last run are processed — no full re-scan, no duplicates.
Features
| Feature | Description |
|---|---|
| Guided setup wizard | Asks for the YT Music login, both playlists, the Spotify app keys and the optional Last.fm key, checks each one live and saves it to .env — no manual file editing |
| Playlist pickers | Choose the YT Music source from your library and the Spotify target from your playlists (or let it create one); pasted links work too |
| Live YT Music API | Fetches your playlist directly via ytmusicapi — no manual export required |
| 3-pass smart matching | Normalised title + artist → fuzzy fallback, both behind hard title, artist and duration checks; an exact ISRC match runs first for CSV exports with an isrc column (the YT Music API has no ISRCs) |
| Diff-based sync | JSON snapshots track playlist state; only added/removed tracks are touched each run |
| Spotify playlist management | Adds new matches and removes deleted tracks automatically |
| Liked Songs sync | Mirrors YT Music likes into Spotify Liked Songs |
| YT Music backup | Exports a playlist, Liked Songs or the whole library to JSON + CSV, and imports a file or another playlist into a new or existing YT Music playlist — no Spotify needed |
| JSON + CSV output | Every enriched CSV gets a matching .json copy, ready for jq and dashboards |
| Metadata enrichment | Captures ISRC, explicit flag, album release date, album type, track number, and Spotify popularity |
| Audio features enrichment | Schema for danceability, energy, valence, tempo, key, and 7 more (populated only if your Spotify app has audio-features access; see Known Limitations) |
| Last.fm artist tags | Pulls play counts, listeners, and dense genre tags via artist.getInfo (typical ~93 % coverage on niche libraries) |
| Local genre + mood classification | Derives primary_genre (17 buckets) and mood (14 labels) from the tag pool — no API calls |
| Run anywhere | Interactive menu for manual use and subcommands for cron/Jenkins — same codebase |
| Resumable | Match progress is cached every 25 tracks; after a rate limit or Ctrl+C, re-running continues where it stopped, and matches found before the stop still reach Spotify (checked against the playlist, so not twice) |
Quota-friendly --limit |
Cap new tracks per run to stay within Spotify's daily API quota |
| Dry-run mode | Every Spotify-changing command can run without changing anything |
| CSV fallback | Works from a CSV export if you prefer not to use the live API |
| Unmatched tracking | Saves failed matches to data/unmatched.csv for manual review or later retry |
Install
Via PyPI (recommended):
pip install ytmusic-to-spotify-sync
pipx install ytmusic-to-spotify-sync # isolated install, puts ytmusic-sync on PATH
pip install -U ytmusic-to-spotify-sync # upgrade
This installs the ytmusic-sync command.
Or clone for development:
git clone https://github.com/davidmalko87/ytmusic-to-spotify-sync.git
cd ytmusic-to-spotify-sync
pip install -r requirements.txt # or: pip install -e ".[dev]" for tests + ruff
With a clone, run python playlist_sync.py wherever this README says ytmusic-sync.
Requirements: Python 3.10+.
Where your settings live
.env, browser.json (YT Music login), the Spotify token cache, data/ and the log file are kept
together in one settings folder:
| How you run it | Settings folder |
|---|---|
Git clone — python playlist_sync.py |
the repo folder |
PyPI install on Windows — ytmusic-sync |
%APPDATA%\ytmusic-to-spotify-sync |
| PyPI install on macOS | ~/Library/Application Support/ytmusic-to-spotify-sync |
| PyPI install on Linux | ~/.config/ytmusic-to-spotify-sync (or $XDG_CONFIG_HOME/...) |
Either, with PLAYLIST_SYNC_HOME set |
that folder (handy for cron/Jenkins) |
The folder is created on first use and the same one is used from any terminal. The menu header and
ytmusic-sync status show the folder in use. Upgrading from 0.7.x or older via pip? Those versions
kept their settings inside site-packages; the setup wizard finds them and offers to copy them over.
Quick Start
1. Configure — setup wizard
ytmusic-sync setup
The wizard walks through five steps and saves every answer to .env for you:
- YT Music login — paste the request headers of one YT Music page from DevTools (see below). The login is checked with YT Music before it is saved; it stays valid for about two years.
- YT Music playlist — pick it from your library by number, search by name, or paste its link.
- Spotify app keys — create an app at the
Spotify Developer Dashboard with the redirect URI
http://127.0.0.1:8888/callbackand the Web API checkbox, then paste its Client ID and secret (the secret is hidden while you type). A browser window opens once to grant access. Spotify only runs developer apps while the account that created the app has Premium. - Spotify playlist — pick one of yours, paste a link, or let the wizard create a private playlist named after the YT Music one.
- Last.fm key (optional) — free from last.fm/api/account/create; checked with Last.fm before it is saved.
Finished steps are skipped on later runs unless you choose to change them; Ctrl+C stops the
wizard with everything entered so far kept. Running ytmusic-sync with no arguments offers the
wizard automatically on first start.
How to copy the YT Music request headers
- Open music.youtube.com and make sure you are logged in
- Press F12 and switch to the Network tab
- Type
browseinto the filter box, then click Home or Library - Find a request named
browse?...(method POST, status 200) - Copy its request headers — any one of these works:
- Chrome / Edge (easiest): right-click it → Copy → Copy as cURL (bash)
- Firefox: right-click it → Copy Value → Copy Request Headers
- Any browser: Headers tab → Request Headers → select every line from the first
header to the last →
Ctrl+C
- Paste into the terminal and press Enter on an empty line
If the paste can't be used (single line, missing cookie or authorization, selection started
mid-cookie), the wizard explains what went wrong and asks again. The login is saved in browser.json in the
settings folder — treat it like a password.
2. Run — interactive menu
ytmusic-sync # or, from a clone: python playlist_sync.py
==================================================================
YT Music -> Spotify Sync v0.8.0
==================================================================
Settings folder: C:\Users\you\ytmusic-to-spotify-sync
SETUP
[ OK ] YT Music login saved
[ OK ] YT Music playlist Road Trip Mix
[ OK ] Spotify app keys Client ID ******3f9c
[ OK ] Spotify playlist Road Trip Mix
[ OK ] Last.fm key ******71d2
SYNC
1 Sync playlist now YT Music -> Spotify
2 Preview changes what changed on YT Music
3 Sync Liked Songs YT likes -> Spotify Liked Songs
4 Status match rate, unmatched, snapshots
FIX & MAINTAIN
5 Retry unmatched tracks search Spotify again
6 Re-fill Spotify playlist after recreating or emptying it
EXTRAS
7 Last.fm tags & play counts needs a free Last.fm key
8 Genre & mood labels from tags, no API calls
9 Save a JSON copy of the synced data, where you choose
10 Use a CSV export import or sync from a CSV file
11 Save a snapshot record the YT playlist as it is now
YT MUSIC BACKUP (no Spotify needed)
12 Export from YT Music playlist / Liked Songs -> JSON + CSV
13 Import into YT Music file or playlist -> YT playlist
SETTINGS
S Setup wizard walk through every step
C Change one setting playlists, keys, login
Q Quit
The SETUP block shows what is configured and Next suggests the next step. Any action that needs missing setup offers to run just those wizard steps first, and actions that change Spotify ask whether to run for real or as a dry run. C changes a single setting (login, either playlist, Spotify keys, Last.fm key, CSV export file, redirect URI).
3. Run — CLI (automation)
ytmusic-sync setup # guided setup wizard (interactive)
ytmusic-sync setup-ytmusic # only the YT Music login step
ytmusic-sync snapshot # save the current YT Music playlist state
ytmusic-sync diff # show changes since the last snapshot
ytmusic-sync sync # full sync (YT Music API -> Spotify)
ytmusic-sync sync --dry-run # same work, nothing changed on Spotify
ytmusic-sync sync --limit 50 # match at most 50 new tracks this run
ytmusic-sync sync --retry-unmatched # also retry tracks that previously failed
ytmusic-sync sync --from-csv # sync from SOURCE_CSV instead of the API
ytmusic-sync sync --from-csv my_export.csv # sync from a specific CSV export
ytmusic-sync import-csv --csv my_export.csv # one-time bootstrap from a CSV export
ytmusic-sync import-csv --csv my_export.csv --force # replace existing sync data (archived first)
ytmusic-sync retry-unmatched # standalone retry of previously failed matches
ytmusic-sync lastfm # Last.fm enrichment on the existing data
ytmusic-sync lastfm --retry-missing # also retry rows flagged attempted that still have no data
ytmusic-sync classify # re-derive primary_genre and mood from tags
ytmusic-sync classify --force # re-classify rows that already have values
ytmusic-sync sync-likes # mirror YT Music likes to Spotify Liked Songs
ytmusic-sync sync-likes --dry-run # preview likes-sync changes
ytmusic-sync sync-likes --retry-unmatched # also search again for likes that did not match
ytmusic-sync sync-likes --force # unsave even after a sudden mass drop (see below)
ytmusic-sync export -o my_data.json # copy of the enriched data as JSON, anywhere
ytmusic-sync export-yt --playlist liked # YT Music Liked Songs -> data/exports/*.json + .csv
ytmusic-sync export-yt --playlist <ID-or-link> # one YT Music playlist -> JSON + CSV
ytmusic-sync export-yt --all # Liked Songs + every library playlist, one dated folder
ytmusic-sync import-yt backup.json # export file -> new private YT Music playlist
ytmusic-sync import-yt backup.json --to <ID-or-link> # ... or into one of your playlists
ytmusic-sync import-yt <ID-or-link> --new-name "Copy" --privacy unlisted # copy a playlist
ytmusic-sync import-yt liked --dry-run # preview copying Liked Songs into a new playlist
ytmusic-sync repush # re-push all matches (idempotent, only adds missing)
ytmusic-sync repush --replace # empty the playlist first, then add (cleans duplicates)
ytmusic-sync repush --dry-run # preview without pushing
ytmusic-sync status # settings folder, match rate, snapshots
CSV exports (import-csv, sync --from-csv) need a title and an artist column; album,
duration (seconds or m:ss), isrc, trackId and platform are used when present. Files
Excel saved with ; as the separator are read as such. Save the file as "CSV UTF-8": another
encoding, a missing column or a bad cell stops the command with the file and row named, before
anything is changed.
All commands accept --verbose / -v (before the command) for debug-level log output, e.g.
ytmusic-sync -v sync. For cron/Jenkins, set PLAYLIST_SYNC_HOME so the job always uses the same
settings folder:
PLAYLIST_SYNC_HOME=/srv/ytmusic-sync ytmusic-sync sync --limit 200
Run setup once interactively first — it also caches the Spotify login, which needs a browser the
first time. After that the other commands run unattended: when something is not configured they
print what is missing and exit with code 1 instead of asking.
How it works
YT Music Playlist ──(API or CSV)──> Current State
│
Diff Engine ←── Previous Snapshot
╱ ╲
Added Tracks Removed Tracks
│ │
Matcher Spotify Remove
╱ ╲
Matched Unmatched
│ │
Spotify Add unmatched.csv
│
Enricher (audio features + metadata)
│
playlist_enriched.csv
Track matching strategy
The matcher runs up to three passes in order of reliability:
- ISRC match — only for tracks that carry an ISRC, i.e. from a CSV export with an
isrccolumn (sync --from-csv, orimport-csvbefore the first sync). The YT Music API returns no ISRCs, so playlists and Liked Songs read from it start at pass 2. Searches Spotify byisrc:; highest accuracy, confidence 0.95–1.0 - Title + Artist — normalised search (
feat.stripped, HTML decoded); a duration within ± 5 s scores a bonus - Relaxed — title-only search with fuzzy artist matching; catches live versions and alternate releases
Before scoring, passes 2 and 3 drop every candidate that fails a hard check. This rejects most same-title songs by other artists, other songs by the same artist and much longer versions:
- the title must be at least 60 % similar (version suffixes such as
- 2015 Remasteror(Live)are ignored for this). A match that passes only once a suffix is ignored gets a confidence below 0.75 when that suffix names another version (- Extended Remix,(Part 2),(Live)); remaster, mono, edition and radio-edit notes do not count. The best candidate is chosen first, so this flag never lets another version win; - when both durations are known, they may differ by 15 s at most;
- one of the track's artists must be at least 60 % similar to one of the Spotify artists. An
artist written in another alphabet (
Zemfira/Земфира) still passes when the title is near-identical and the durations are within 5 s, with a confidence below 0.75. Two different names in the same alphabet are rejected, even for a same-length cover.
The checks compare text and length, not recordings: a sequel with a near-identical title by the
same artist (The Unforgiven II / The Unforgiven, 9 s apart) can still pass when the right
track is missing from Spotify's results.
Fuzzy matches (passes 2 and 3) below confidence 0.75 are still added to Spotify and also listed
in data/review.csv, lowest first, with both links, so you can check them by hand. The file is
rebuilt on every sync and removed when nothing needs a look. If it (or unmatched.csv,
likes_unmatched.csv) is still open in a spreadsheet then, the sync only warns that the open copy
is out of date.
Unmatched tracks are written to data/unmatched.csv and can be retried later with retry-unmatched.
sync exits with code 1 when the Spotify playlist cannot be opened or is not yours (checked
before any search), or when matched tracks could not be added or removed tracks could not be
taken off Spotify, including a long rate limit or a network error while it adds or removes them.
Nothing is lost: the next sync retries them. A rate limit or Ctrl+C during matching still
exits 0 with progress saved, and so does a rate limit in the metadata backfill after the push
(the next sync fetches the rest).
Removals are only applied against the source the sync data came from. After switching
YTMUSIC_PLAYLIST_ID (by hand, or in the menu without archiving), the first sync of the new
source removes nothing from Spotify, even if snapshot ran in between; removals start with its
second sync. data/enriched_source.json records which source playlist_enriched.csv belongs to.
Data from 0.7 has no such record yet: its first 0.8 sync removes only tracks the snapshot of the
current source also lists, as 0.7 did.
Matches found by a run that was stopped before they reached Spotify are kept in
data/match_cache.json and added by the next sync. If that file cannot be read, it is renamed to
match_cache.json.bad with a warning; run repush once so nothing it held is missing.
Output: enriched CSV
The sync produces data/playlist_enriched.csv with 50 columns:
| Column | Source |
|---|---|
title, artist, album |
YT Music |
platform, trackId, url, duration |
YT Music (platform is ytmusic, or the CSV export's own value) |
addedDate, addedBy |
Copied from a CSV export that has them; empty for the YT Music API |
spotify_uri, spotify_url, spotify_duration_ms |
Spotify match |
isrc, isrc_enriched, explicit, album_release_date |
Spotify metadata |
popularity |
Spotify track popularity (0–100) — only with extended-access apps |
artist_genres |
Primary artist genre tags from Spotify /artists — only with extended-access apps |
album_type |
Album type (album / single / compilation) |
track_number |
Track position within the album |
danceability, energy, valence |
Spotify audio features — only with extended-access apps |
tempo, key, mode, loudness |
Spotify audio features |
speechiness, acousticness |
Spotify audio features |
instrumentalness, liveness, time_signature |
Spotify audio features |
audio_features_fetched |
Skip-flag — audio features endpoint already attempted |
lastfm_playcount, lastfm_listeners, lastfm_tags |
Last.fm track.getInfo |
artist_tags |
Last.fm artist.getInfo — much denser than track tags |
tag_source |
Which source filled artist_tags (lastfm_artist, …) |
lastfm_attempted, lastfm_track_attempted |
Skip-flags — Last.fm already answered (found or not found); failed requests stay unset and are retried next run. lastfm --retry-missing re-checks flagged rows that still have no data (CSVs enriched before 0.8.0) |
spotify_metadata_attempted, spotify_genres_attempted |
Skip-flags — Spotify endpoints already attempted |
skip_reason |
Why the matcher pre-filtered this track (e.g. no_album for YT Music tracks lacking album metadata) — set means no Spotify search was attempted |
match_attempted |
true once the matcher has run on this track. Tracks with match_attempted=true AND no spotify_uri are skipped on subsequent syncs unless --retry-unmatched is passed (or retry-unmatched is run standalone) |
primary_genre |
Single broad genre bucket (electronic, rock, soundtrack, …) — derived locally from tags |
mood |
Multi-label mood (chill, epic, cinematic, …) — derived locally from tags |
match_method, match_confidence |
Matching diagnostics |
first_synced, last_synced |
Sync timestamps |
Genre & mood classification
primary_genre and mood are derived locally from the tag pool (artist_tags + lastfm_tags) — no API calls. This runs automatically at the end of every sync and lastfm command, and can be triggered standalone with ytmusic-sync classify (or --force to re-bucket rows that already have values).
The classifier uses word-boundary tokenisation, so compound tags like deep house map to the electronic bucket via house, and post-rock correctly maps to rock (not soundtrack via ost).
A tag that is itself a bucket keyword counts only for that bucket, so post-punk is rock and never punk. Bare hardcore decides nothing (it tags hardcore techno as often as hardcore punk), and a punk tag loses to a higher-ranked tag of an unrelated genre (electronic, french, punk is electronic). For YouTube uploads without an album, whose "artist" is usually the uploading channel, artist tags are ignored unless that artist also has album tracks in the library or is named in the title.
Genre buckets, in priority order: soundtrack, classical, jazz, hip hop, metal, punk, country, blues, reggae, folk, electronic, ambient, rock, pop, rnb, indie, world.
Mood labels: chill, energetic, dark, sad, happy, epic, romantic, dreamy, aggressive, nostalgic, cinematic, ambient, instrumental, acoustic.
Audio features reference
| Feature | Range | Meaning |
|---|---|---|
danceability |
0.0 – 1.0 | Suitability for dancing (tempo, rhythm, beat strength) |
energy |
0.0 – 1.0 | Intensity and activity (loud, fast, noisy = high) |
valence |
0.0 – 1.0 | Musical positiveness (happy = high, sad/angry = low) |
tempo |
BPM | Estimated beats per minute |
speechiness |
0.0 – 1.0 | Presence of spoken words |
acousticness |
0.0 – 1.0 | Confidence the track is acoustic |
instrumentalness |
0.0 – 1.0 | Likelihood of no vocal content |
liveness |
0.0 – 1.0 | Presence of a live audience |
loudness |
dB | Overall loudness (typically −60 to 0) |
key |
0 – 11 | Pitch class (0 = C, 1 = C♯, …, 11 = B) |
mode |
0 or 1 | Modality (0 = minor, 1 = major) |
time_signature |
int | Estimated beats per bar |
Liked songs sync
ytmusic-sync sync-likes
Mirrors your YT Music Liked Songs to Spotify's Liked Songs library (/me/tracks). Uses the same 3-pass matcher and reuses Spotify URIs already discovered during regular playlist sync, so a track present in both places is matched only once. It needs the YT Music login and the Spotify app keys, but no Spotify playlist.
Maintains its own snapshot under data/snapshots/likes/ so likes-diff state never collides with playlist-diff state. Outputs:
data/likes_enriched.csv(+.json) — one row per current like, matched or not, so un-liking an old song is mirroreddata/likes_unmatched.csv— likes that couldn't be matched; a later playlist match is reused for free, and--retry-unmatched(or the menu's question) searches them again
All your likes are fetched (no 10,000 cap). If a large share of them suddenly disappears from YT Music's answer — more than 50 and more than 20 % of the last run — nothing is unsaved: that is far more likely an incomplete answer than a real mass un-like. Run sync-likes --force (or answer yes in the menu) if you really un-liked them. The menu's yes covers only the likes it listed: if YT Music's next answer lacks even more, those are held back again.
Un-liking a song unsaves the Spotify track sync-likes saved for it (the playlist sync's match is only a fallback), and nothing is unsaved while another current like still maps to the same Spotify track (a song and its music video, say). An un-like whose Spotify track is unknown is searched once, unless it was searched before without a match or skipped (a video without album), since nothing was saved for it then.
Nothing is written off on errors: a like whose save failed is saved by the next run (without a new search), an unsave that failed is retried, and a failed search skips only that like (five in a row, a rate limit or Ctrl+C stop the run, keeping its matches). sync-likes exits with code 1 when Spotify refused saves or unsaves or stopped answering while it wrote them.
Recreated the Spotify playlist? Use repush
If you delete and recreate your Spotify playlist, sync won't repopulate it — sync only pushes newly-matched tracks since the last snapshot, and an empty destination playlist isn't a "new match". Point the tool at the new playlist (menu C → Spotify playlist), then choose 6 Re-fill Spotify playlist or run:
ytmusic-sync repush # idempotent — only adds URIs missing from the playlist
ytmusic-sync repush --replace # empty the playlist first, then add (cleans dupes)
ytmusic-sync repush --dry-run # preview the push
Reads every spotify_uri from data/playlist_enriched.csv and adds the missing ones in batches of 100. No Spotify search calls — uses the URIs already on disk, so it's fast. Idempotent by default: running it twice does not duplicate tracks, and a
Spotify track two YT Music entries match is added once. repush exits with code 1 when tracks could not
be added, or Spotify stopped answering. With --replace, a wipe that fails partway adds only what the
playlist still lacks (it never doubles the playlist) and exits 1. Local files in the playlist are
left in place: the API cannot remove them.
JSON copies
Every time the enriched CSV is written, a JSON copy with the same name is written beside it:
data/playlist_enriched.json and, for likes, data/likes_enriched.json. Each is a structured
document — { exported_at, track_count, tracks: [{...}, ...] } — with every enrichment field per
track, easier to feed into jq, dashboards or other tools than the CSV. To put a copy somewhere
else, use menu 9 or:
ytmusic-sync export -o /tmp/my_data.json
YT Music backup: export & import
Works with only the YT Music login — no Spotify setup needed. Menu 12 exports, menu 13 imports.
ytmusic-sync export-yt --playlist liked # Liked Songs
ytmusic-sync export-yt --playlist <ID-or-link>
ytmusic-sync export-yt --all # Liked Songs + every playlist in your library
Each export is a JSON file (portable, re-importable) plus a CSV with the usual playlist columns, so
it also works with 10 Use a CSV export. Files go to data/exports/ in the settings folder;
--all writes one dated backup-<date> folder.
ytmusic-sync import-yt data/exports/<file>.json # new private playlist, same name
ytmusic-sync import-yt <file>.json --to <your-playlist> # add what is missing to a playlist you own
ytmusic-sync import-yt <ID-or-link> --new-name "My copy" # copy a playlist directly, no file
Imports add tracks by YouTube video ID and skip ones the target already has, so an interrupted
import can simply be run again (with --to and the new playlist's ID, which is printed right after
it is created). New playlists are private unless you pass --privacy unlisted|public. To copy a
playlist into another YT Music account, export it, switch the login (menu C → YT Music login),
import, then switch back.
Skip filter & debug stats
- Skip filter: YT Music tracks with no album metadata (typically YouTube uploads, fan edits, mixes) are filtered out before Spotify search and written to
data/skipped.csvwithskip_reason=no_album. Saves API time and keepsunmatched.csvfocused on tracks that genuinely should match but didn't. - Debug stats: every
syncrun writesdata/debug/run_<timestamp>.json(and a rollinglatest.json) with totals, diff deltas, match rate, method distribution, average confidence, skip-reason histogram, and a few unmatched/skipped examples. Useful for graphing sync quality over time or feeding into a monitoring dashboard.
Known Limitations
These are YT Music, Spotify and Last.fm platform constraints — not tool limitations:
| Data | Status | Notes |
|---|---|---|
| Spotify developer app | Premium required | Since February 2026 Spotify runs Development Mode apps only while the app owner has Premium, and new apps allow up to 5 users |
| Spotify search | Rate-limited | Development Mode apps get 10 results per request and a daily quota. Use --limit N to spread a large first sync over several days |
| Audio features, popularity, artist genres | Usually empty | /v1/audio-features, /v1/tracks and /v1/artists return 403 for standard (non-extended-access) apps since late 2024. The tool detects each 403, marks the skip-flag and stops retrying; sync still works and Last.fm fills genre/mood |
| YT Music-exclusive tracks | Not matchable | Unreleased, region-locked and user-uploaded tracks have no Spotify equivalent; they are listed in data/unmatched.csv |
| YT Music login | Browser headers only | ytmusicapi OAuth is broken in v1.11.x, so the tool uses browser-header auth (valid ~2 years, ends if you log out of YT Music in that browser) |
| Liked Songs endpoint | Best-effort | YT Music sometimes serves a signed-out page for the Liked Music (LM) endpoint even when playlists work; the tool explains how to refresh the login |
| Last.fm track tags | Sparse on niche music | Track-level tags are user-submitted and thin for game OSTs, regional uploads and remix edits; artist-level tags are preferred |
| YT Music import | Video IDs only | Only rows with a YouTube video ID can be imported (rows that came from Spotify have none); unavailable or region-locked videos are reported, not added |
Project Structure
ytmusic-to-spotify-sync/
├── playlist_sync.py # Entry point for git clones (same as the ytmusic-sync command)
├── .env.example # Settings template (the wizard writes .env for you; .env is gitignored)
├── requirements.txt # Python dependencies
├── pyproject.toml # Packaging; console script ytmusic-sync
│
├── playlist_sync/
│ ├── __init__.py # Package version (canonical version source)
│ ├── cli.py # Subcommands (argparse) and console-script entry point
│ ├── menu.py # Interactive menu and setup wizard
│ ├── yt_transfer.py # YT Music export (JSON + CSV) and import
│ ├── config.py # Settings folder, .env loading/writing, validation
│ ├── models.py # Track, MatchResult, DiffResult dataclasses
│ ├── utils.py # Text normalisation and logging setup
│ ├── csv_manager.py # CSV I/O (BOM-aware)
│ ├── ytmusic_client.py # YT Music API wrapper and login-header parsing
│ ├── spotify_client.py # Spotify API wrapper with rate-limit handling
│ ├── matcher.py # 3-pass track matching engine
│ ├── differ.py # Snapshot diff engine
│ ├── enricher.py # Metadata, audio features, classification
│ └── lastfm_client.py # Last.fm API wrapper (track + artist endpoints)
│
├── tests/ # Offline pytest suite (no network, fakes for all APIs)
│
└── data/ # Created at runtime in the settings folder (gitignored)
├── snapshots/ # JSON snapshots
├── playlist_enriched.csv # Full enriched output (+ playlist_enriched.json copy)
├── exports/ # YT Music exports (export-yt)
├── review.csv # Low-confidence matches worth a manual check
├── match_cache.json # Matches of a stopped run not yet added to Spotify
├── enriched_source.json # Which source playlist_enriched.csv was synced from
└── unmatched.csv # Tracks that could not be matched
Configuration Reference
The setup wizard writes these to .env in the settings folder; you can also edit .env by hand
or set them as environment variables (real environment variables win over .env). Values still
reading your_..._here from the template count as not set.
| Variable | Required | Default | Description |
|---|---|---|---|
SPOTIPY_CLIENT_ID |
Yes | — | Spotify app Client ID (dashboard) |
SPOTIPY_CLIENT_SECRET |
Yes | — | Spotify app client secret |
SPOTIPY_REDIRECT_URI |
No | http://127.0.0.1:8888/callback |
OAuth redirect URI; must match the app settings exactly |
SPOTIFY_PLAYLIST_ID |
Yes* | — | Target Spotify playlist — ID or full open.spotify.com link (*not needed for sync-likes) |
YTMUSIC_PLAYLIST_ID |
Yes* | — | Source YT Music playlist — ID or full ...playlist?list=<ID> link (*not needed with --from-csv) |
YTMUSIC_AUTH_FILE |
No | browser.json |
YT Music login file; relative paths are inside the settings folder |
SOURCE_CSV |
No | — | CSV export to use instead of the live API; relative paths are inside the settings folder |
LASTFM_API_KEY |
No | — | Free key from last.fm/api/account/create — play counts, listeners, artist tags |
PLAYLIST_SYNC_HOME |
No | see Where your settings live | Settings folder override; set it in the environment, not in .env (created if missing) |
Requirements
- Python 3.10+
- Spotify account with Premium for the developer app (dashboard); not needed for the YT Music backup features
- YouTube Music account
pandas>= 2.0,spotipy>= 2.26,ytmusicapi>= 1.8,python-dotenv>= 1.2.3,tqdm>= 4.66
Changelog
See CHANGELOG.md for the full version history.
Contributing
See CONTRIBUTING.md for the versioning policy and development setup. In short:
no secrets in the repo, ruff check . and pytest -q must pass, ASCII-only console output,
test against your own non-production playlists.
License
Metadata
Release files for ytmusic-to-spotify-sync 0.8.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| ytmusic_to_spotify_sync-0.8.0.tar.gz | 123.5 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ytmusic_to_spotify_sync-0.8.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 229.0 kB
Release files / ytmusic_to_spotify_sync-0.8.0.tar.gz
| Download URL | ytmusic_to_spotify_sync-0.8.0.tar.gz |
|---|---|
| Size | 123.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6e6d1ce6fe50fcc2e77a8bee18127b17a50b164f931ae2f1b44fbdf192395cc7
|
|
BLAKE2b-256 checksum How to use checksums |
e36a85320f20a62c09c501df56f935a723856d4b4870b3188ea298dcdbe6fccb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logRelease files / ytmusic_to_spotify_sync-0.8.0-py3-none-any.whl
| Download URL | ytmusic_to_spotify_sync-0.8.0-py3-none-any.whl |
|---|---|
| Size | 105.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
94dfa5b6c17ea78dfedb91688e654431e9341a4048037e059fb4fac4b8bc2140
|
|
BLAKE2b-256 checksum How to use checksums |
1b25fab7aa283f07ec7cae38f7e78c47fe13f89d5f9d3c6cfea8c6b72ac79c50
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logSecurity for ytmusic-to-spotify-sync 0.8.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for ytmusic_to_spotify_sync-0.8.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.