Aggregates liked tracks into top artists, genres, decades and top albums with fixed size ceilings instead of a per-track dump, so it can be pasted into an LLM chat regardless of library size. Fully offline - reads only the existing cache, no Client/network needed. Adds album_title/year to the track cache (one-time re-fetch) to support the decade and album views.
7.6 KiB
Sort YM
Sorts your Yandex Music liked tracks into playlists named Genre — RU / Genre — INT.
$ python -m sort_ym report
Playlist breakdown:
Rock — INT: 143
Rock — RU: 287
Rap — RU: 512
...
Report saved: out/report.csv
Install
Requirements: Python 3.11+ (uses tomllib from the standard library), Windows/Linux/macOS, internet access.
python -m venv .venv
.venv\Scripts\pip install -r requirements.txt
For development (tests):
.venv\Scripts\pip install -r requirements-dev.txt
What it does
.venv\Scripts\python -m sort_ym auth # log in to a Yandex account via device-flow and save the token to .token
.venv\Scripts\python -m sort_ym fetch # download liked tracks (cache/tracks.json) and their artists' genres (cache/artist_genres.json); does not change the account
.venv\Scripts\python -m sort_ym report # compute genre and language per track, save out/report.csv (does not change the account)
.venv\Scripts\python -m sort_ym apply --limit 20 --yes # dry run: create playlists and add the first 20 tracks
.venv\Scripts\python -m sort_ym apply --yes # create playlists and add all tracks (changes the account)
.venv\Scripts\python -m sort_ym digest # library summary (top artists, genres) to out/digest.md - to paste into an LLM chat; no network used
On repeated runs fetch only downloads new tracks, and apply does not duplicate tracks already added.
Full cache cleanup
To start over from scratch (re-fetch everything), delete the cache/ folder:
Remove-Item -Recurse -Force cache
The cache consists of four files:
cache/tracks.json— liked tracks and their metadata (including artist ids);cache/artist_genres.json— genre tags for each artist (an artist can have several);cache/genre_catalog.json— a snapshot of Yandex's genre tree as of the last run;cache/lyrics_lang.json— detected lyrics language per track.
Deleting cache/ makes fetch and report re-fetch everything from scratch on the next run.
You can also delete the generated reports (out/report.csv, out/digest.md):
Remove-Item -Recurse -Force out
It's just analysis output — deleting it doesn't affect the next report/digest run.
Important: the .token file (OAuth token) is not part of the cache and is not removed by clearing cache/. Clearing the cache does not log you out. To force re-authentication, delete .token separately:
Remove-Item .token
How genre and language are determined
-
Genre is determined at the sub-genre level (Rock, Punk, Indie, Alternative, Metal, ..., not just the 11 top-level buckets), combining two signals:
- The track's album genre (
genre_raw) — a per-track signal. - The artist's genres (
Artist.genres, an artist can have several tags, more precise than a single album genre) — a per-artist signal.
If the signals agree, the sub-genre is used as is. On conflict: if the album tag is generic ("umbrella-like", such as
rock/alternative/pop— Yandex's typical low-confidence default), the artist's genre wins; if the album tag is precise/specific (numetal,dnb,shanson), it wins, because it's more likely a genuine genre experiment by the artist rather than noise. This way the same artist with tracks of different character (say, a rock track and a punk track) ends up in different playlists instead of everything being lumped into one "main" genre.Sub-genre groups smaller than
small_group_min(12 tracks by default,config.toml) collapse into the parent top-level bucket (Pop, Rock, Metal, Rap, Electronic, Jazz & Blues, Classical, Folk & World, Indie & Alternative, Soundtracks, Other) to avoid playlists with 1-2 tracks. Implementation —sort_ym/genres.py(classify_track,ROOT_BUCKET) — the root table is easy to extend if Yandex returns an unfamiliar root genre. - The track's album genre (
-
Language is a hybrid:
- If the track has lyrics — the real language from the API (
ru→ RU, otherwise → INT). - Otherwise — a genre-based hint (
rusrap,shanson,bard, ...). - Otherwise — an alphabet heuristic on the title/artist (Cyrillic/Latin).
- If nothing matched — the track goes into
... — Undetermined.
- If the track has lyrics — the real language from the API (
After report, the console also prints a list of unrecognized genres (that fell into "Other") so
the table in sort_ym/genres.py can be extended.
Digest for an LLM
An LLM can't digest a full per-track dump of ~2000 tracks as well as a compact summary — so
digest doesn't export tracks line by line, it aggregates the library into a few sections with
hard size ceilings: at most 11 lines for top-level genres, ~34 for sub-genres, 3 for languages,
~13 for decades, and top artists/top albums are bounded by config.toml, not by library size.
Anything past the top collapses into a single sentence with aggregate numbers. The resulting file
stays a couple hundred lines at most, regardless of whether the library has 500 tracks or 10000.
The command is fully offline: it makes no network calls and needs no .token, only the already
downloaded cache/. report must have run at least once first (for cache/genre_catalog.json).
$ python -m sort_ym digest
Genres:
Indie & Alternative: 680
Rock: 590
...
Digest saved: out/digest.md
Example out/digest.md (abridged):
## Genres (top-level buckets)
- Indie & Alternative — 680 (34%): INT 571 / RU 108
- Rock — 590 (29%): INT 518 / RU 72
...
## Decades
- 2010s — 620 (31%): Rock 280, Indie & Alternative 200, Metal 90
...
## Top 40 artists
1. Billy Talent — 66 (Rock; rock, punk)
...
1140 more artists with 1-4 tracks, 1737 mentions total; 835 of them with a single track.
## Top 15 albums
1. Slipknot — Iowa (2001) — 12
...
1401 more albums with 1-3 liked tracks. Tracks from albums with 2+ liked tracks: 732 of 2005 (37%).
Important: the year and album title fields weren't in the track cache from the start — if
cache/tracks.json was created by an older version of fetch, the "Decades" and "Top albums"
sections will show "Unknown" until the next python -m sort_ym fetch run (it re-downloads the
whole cache once, then goes back to only fetching new tracks as usual).
Handy things
- Run
applyfirst with--limit 20 --yesand check the result in the app before running it against all ~2000 tracks. - Delays between requests are configured in
config.toml([throttle]). - The minimum sub-genre playlist size is
config.toml([classify] small_group_min). - How many artists/albums the digest names individually —
config.toml([digest] top_artists,top_albums) or thedigestcommand's--top/--top-albumsflags.
Risks (unofficial API)
There is no official API for personal Yandex Music library access — this tool uses the
unofficial yandex-music library. The OAuth token in .token grants full access to your Yandex
account — keep it local and never publish it. The tool only performs safe actions (reading likes,
creating playlists, adding tracks) and never deletes anything.
For developers
Python 3.11+, yandex-music==3.0.0, standard library (tomllib, csv, unicodedata).
.venv\Scripts\python -m pytest tests/ -v
.venv\Scripts\python -m sort_ym auth
.venv\Scripts\python -m sort_ym fetch
.venv\Scripts\python -m sort_ym report
.venv\Scripts\python -m sort_ym apply --limit 20 --yes
.venv\Scripts\python -m sort_ym digest
Code structure and data flow are in ARCHITECTURE.md.
Credits
Andrew Pimenov