mirror of
https://github.com/Horizontal42/Sort_YM.git
synced 2026-11-10 15:42:02 +03:00
fix docs to match code: dedupe command, deletion claim, analysis field names
README and ARCHITECTURE were missing the dedupe command and claimed the tool never deletes anything, but dedupe --yes removes duplicate playlist entries. lyrics_analysis.json field list (tone/description) didn't match the actual schema (register/concreteness/stance/summary/resonance/key_line/confidence). lyrics_text.json is also written by report/apply as a side effect of language detection, not only by the lyrics command. Sub-genre count corrected 34->36.
This commit is contained in:
parent
b19eadd744
commit
f36526a9c9
5 changed files with 69 additions and 26 deletions
|
|
@ -22,7 +22,8 @@ sort_ym/
|
||||||
genres into a single sub-genre (root_id); ROOT_BUCKET — sub-genre -> 11 top-level buckets
|
genres into a single sub-genre (root_id); ROOT_BUCKET — sub-genre -> 11 top-level buckets
|
||||||
language.py — hybrid language detection (lyrics API / genre hint / alphabet)
|
language.py — hybrid language detection (lyrics API / genre hint / alphabet)
|
||||||
lyrics.py — text lyrics of RU-tracks: track_supplement().lyrics.full_lyrics ->
|
lyrics.py — text lyrics of RU-tracks: track_supplement().lyrics.full_lyrics ->
|
||||||
cache/lyrics_text.json (incrementally, atomic-write)
|
cache/lyrics_text.json (incrementally, atomic-write); the same file is also
|
||||||
|
populated as a side effect of language.fetch_api_languages on report/apply
|
||||||
analyze.py — lyric analysis via local Ollama (/api/chat, think + JSON Schema) ->
|
analyze.py — lyric analysis via local Ollama (/api/chat, think + JSON Schema) ->
|
||||||
cache/lyrics_analysis.json (written after each track)
|
cache/lyrics_analysis.json (written after each track)
|
||||||
classify.py — (sub-genre | top-level bucket, language) -> playlist name; with_label — source
|
classify.py — (sub-genre | top-level bucket, language) -> playlist name; with_label — source
|
||||||
|
|
@ -32,8 +33,8 @@ sort_ym/
|
||||||
digest.py — aggregated library summary (top artists, genres, decades, top albums)
|
digest.py — aggregated library summary (top artists, genres, decades, top albums)
|
||||||
-> out/digest.md, fully offline without --source; Wording/playlist_wording —
|
-> out/digest.md, fully offline without --source; Wording/playlist_wording —
|
||||||
digest text for likes vs an arbitrary playlist
|
digest text for likes vs an arbitrary playlist
|
||||||
cli.py — auth / fetch / report / apply / dedupe / digest commands; --source/--label
|
cli.py — auth / fetch / report / apply / dedupe / digest / lyrics / analyze commands;
|
||||||
on report/apply/digest (see "Key decisions")
|
--source/--label on report/apply/digest (see "Key decisions")
|
||||||
|
|
||||||
tests/
|
tests/
|
||||||
test_genres.py — classify_track, dominant, bucket_for, genre tree
|
test_genres.py — classify_track, dominant, bucket_for, genre tree
|
||||||
|
|
@ -64,6 +65,7 @@ report → genres.load_or_fetch_catalog() → cache/genre_catalog.json
|
||||||
pass 2 — groups < small_group_min collapse into the top-level bucket
|
pass 2 — groups < small_group_min collapse into the top-level bucket
|
||||||
→ out/report.csv (dry-run, account unchanged)
|
→ out/report.csv (dry-run, account unchanged)
|
||||||
apply --yes → users_playlists_create / users_playlists_insert_track
|
apply --yes → users_playlists_create / users_playlists_insert_track
|
||||||
|
dedupe --yes → users_playlists(kind) → find_duplicate_ranges() → users_playlists_delete_track
|
||||||
digest → genres.load_catalog() + language.load_lang_cache() (cache only, no network)
|
digest → genres.load_catalog() + language.load_lang_cache() (cache only, no network)
|
||||||
→ build_rows() → out/digest.md
|
→ build_rows() → out/digest.md
|
||||||
lyrics → ru_lyric_track_ids() → client.track_supplement() → cache/lyrics_text.json
|
lyrics → ru_lyric_track_ids() → client.track_supplement() → cache/lyrics_text.json
|
||||||
|
|
@ -154,7 +156,15 @@ report/apply/digest --source <url> → source.parse_playlist_url() → (user_id,
|
||||||
|
|
||||||
- **`apply` idempotency**: before adding tracks to a playlist, its current contents are read
|
- **`apply` idempotency**: before adding tracks to a playlist, its current contents are read
|
||||||
(`users_playlists(kind)`), and tracks already present are skipped. Re-running `apply` is safe and
|
(`users_playlists(kind)`), and tracks already present are skipped. Re-running `apply` is safe and
|
||||||
creates no duplicates. Likes are never touched, nothing is ever deleted.
|
creates no duplicates. Likes are never touched, and `apply_classification` itself never deletes
|
||||||
|
anything — the only deletion in `apply.py` is `dedupe_playlists` (see below), a separate,
|
||||||
|
explicitly-invoked path.
|
||||||
|
|
||||||
|
- **`dedupe`**: a separate cleanup pass, not run as part of `apply`. Reads a playlist's current
|
||||||
|
contents (`users_playlists(kind)`), finds consecutive/duplicate insertions of the same track via
|
||||||
|
`find_duplicate_ranges`, and — only with `dedupe --yes` — removes the extras with
|
||||||
|
`users_playlists_delete_track`. Without `--yes` it's a dry run that only reports what it would
|
||||||
|
remove. This is the one place in the codebase that deletes anything from a playlist.
|
||||||
|
|
||||||
- **Throttling and resumability**: every network loop (fetching tracks, languages) pauses between
|
- **Throttling and resumability**: every network loop (fetching tracks, languages) pauses between
|
||||||
requests (`config.toml`, `[throttle]`) and writes the cache atomically (temp file + rename) after
|
requests (`config.toml`, `[throttle]`) and writes the cache atomically (temp file + rename) after
|
||||||
|
|
@ -219,9 +229,13 @@ report/apply/digest --source <url> → source.parse_playlist_url() → (user_id,
|
||||||
`report --extra`-only purpose as `added_at` above.
|
`report --extra`-only purpose as `added_at` above.
|
||||||
- `cache/genre_catalog.json` — a flat snapshot of Yandex's genre tree (id -> title, root_id).
|
- `cache/genre_catalog.json` — a flat snapshot of Yandex's genre tree (id -> title, root_id).
|
||||||
- `cache/lyrics_lang.json` — lyrics language per track (id -> language code | null).
|
- `cache/lyrics_lang.json` — lyrics language per track (id -> language code | null).
|
||||||
- `cache/lyrics_text.json` — lyrics text per track (numeric id -> text | null, RU tracks only).
|
- `cache/lyrics_text.json` — lyrics text per track (numeric id -> text | null). RU tracks only
|
||||||
|
unless fetched with `--all-languages`. Written incrementally not just by `lyrics.fetch_lyrics_text`
|
||||||
|
but also, as a side effect, by `language.fetch_api_languages` on every `report`/`apply` run — it
|
||||||
|
caches the RU lyrics text it already fetched while resolving the lyrics language.
|
||||||
- `cache/lyrics_analysis.json` — per-track analysis record: `{track_id: {model, prompt_version,
|
- `cache/lyrics_analysis.json` — per-track analysis record: `{track_id: {model, prompt_version,
|
||||||
lyrics_hash, mood, themes, emotional_arc, pov, tone, description, error?}}`.
|
lyrics_hash, analyzed_at, mood, themes, emotional_arc, pov, register, concreteness, stance,
|
||||||
|
confidence, summary, resonance, key_line, key_line_verified, error?}}`.
|
||||||
- `out/lyrics_digest.md` — full per-track reading grouped by primary mood.
|
- `out/lyrics_digest.md` — full per-track reading grouped by primary mood.
|
||||||
- `config.toml` — includes `[ollama]` block with `host`, `model`, `prompt_version`, `timeout`,
|
- `config.toml` — includes `[ollama]` block with `host`, `model`, `prompt_version`, `timeout`,
|
||||||
`keep_alive`.
|
`keep_alive`.
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ sort_ym/
|
||||||
артиста в один под-жанр (root_id); ROOT_BUCKET — под-жанр -> 11 крупных корзин
|
артиста в один под-жанр (root_id); ROOT_BUCKET — под-жанр -> 11 крупных корзин
|
||||||
language.py — гибридная детекция языка (API текста / жанр-подсказка / алфавит)
|
language.py — гибридная детекция языка (API текста / жанр-подсказка / алфавит)
|
||||||
lyrics.py — тексты песен RU-треков: track_supplement().lyrics.full_lyrics ->
|
lyrics.py — тексты песен RU-треков: track_supplement().lyrics.full_lyrics ->
|
||||||
cache/lyrics_text.json (инкрементально, atomic-write)
|
cache/lyrics_text.json (инкрементально, atomic-write); тот же файл попутно
|
||||||
|
пополняет language.fetch_api_languages на report/apply
|
||||||
analyze.py — разбор текстов локальной Ollama (/api/chat, think + JSON Schema) ->
|
analyze.py — разбор текстов локальной Ollama (/api/chat, think + JSON Schema) ->
|
||||||
cache/lyrics_analysis.json (запись после каждого трека)
|
cache/lyrics_analysis.json (запись после каждого трека)
|
||||||
classify.py — (под-жанр | крупная корзина, язык) -> имя плейлиста; with_label — суффикс метки
|
classify.py — (под-жанр | крупная корзина, язык) -> имя плейлиста; with_label — суффикс метки
|
||||||
|
|
@ -32,8 +33,8 @@ sort_ym/
|
||||||
digest.py — агрегированная сводка библиотеки (топ-исполнители, жанры, десятилетия,
|
digest.py — агрегированная сводка библиотеки (топ-исполнители, жанры, десятилетия,
|
||||||
топ-альбомы) -> out/digest.md, полностью офлайн без --source; Wording/
|
топ-альбомы) -> out/digest.md, полностью офлайн без --source; Wording/
|
||||||
playlist_wording — тексты дайджеста для лайков vs произвольного плейлиста
|
playlist_wording — тексты дайджеста для лайков vs произвольного плейлиста
|
||||||
cli.py — команды auth / fetch / report / apply / dedupe / digest; --source/--label
|
cli.py — команды auth / fetch / report / apply / dedupe / digest / lyrics / analyze;
|
||||||
на report/apply/digest (см. "Ключевые решения")
|
--source/--label на report/apply/digest (см. "Ключевые решения")
|
||||||
|
|
||||||
tests/
|
tests/
|
||||||
test_genres.py — classify_track, dominant, bucket_for, дерево жанров
|
test_genres.py — classify_track, dominant, bucket_for, дерево жанров
|
||||||
|
|
@ -65,6 +66,7 @@ report → genres.load_or_fetch_catalog() → cache/genre_catalog.json
|
||||||
проход 2 — группы < small_group_min схлопываются в крупную корзину
|
проход 2 — группы < small_group_min схлопываются в крупную корзину
|
||||||
→ out/report.csv (dry-run, аккаунт не меняется)
|
→ out/report.csv (dry-run, аккаунт не меняется)
|
||||||
apply --yes → users_playlists_create / users_playlists_insert_track
|
apply --yes → users_playlists_create / users_playlists_insert_track
|
||||||
|
dedupe --yes → users_playlists(kind) → find_duplicate_ranges() → users_playlists_delete_track
|
||||||
digest → genres.load_catalog() + language.load_lang_cache() (только кэш, сеть не используется)
|
digest → genres.load_catalog() + language.load_lang_cache() (только кэш, сеть не используется)
|
||||||
→ build_rows() → out/digest.md
|
→ build_rows() → out/digest.md
|
||||||
lyrics → ru_lyric_track_ids() → client.track_supplement() → cache/lyrics_text.json
|
lyrics → ru_lyric_track_ids() → client.track_supplement() → cache/lyrics_text.json
|
||||||
|
|
@ -155,7 +157,15 @@ report/apply/digest --source <url> → source.parse_playlist_url() → (user_id,
|
||||||
|
|
||||||
- **Идемпотентность `apply`**: перед добавлением треков в плейлист читается его текущий состав
|
- **Идемпотентность `apply`**: перед добавлением треков в плейлист читается его текущий состав
|
||||||
(`users_playlists(kind)`), уже присутствующие треки пропускаются. Повторный запуск `apply`
|
(`users_playlists(kind)`), уже присутствующие треки пропускаются. Повторный запуск `apply`
|
||||||
безопасен и не создаёт дублей. Лайки не трогаются, ничего не удаляется.
|
безопасен и не создаёт дублей. Лайки не трогаются, и сама `apply_classification` ничего не
|
||||||
|
удаляет — единственное удаление в `apply.py` делает `dedupe_playlists` (см. ниже), отдельный,
|
||||||
|
явно вызываемый путь.
|
||||||
|
|
||||||
|
- **`dedupe`**: отдельный проход очистки, не часть `apply`. Читает текущий состав плейлиста
|
||||||
|
(`users_playlists(kind)`), находит повторные вставки одного трека через `find_duplicate_ranges`
|
||||||
|
и — только при `dedupe --yes` — удаляет лишние записи через `users_playlists_delete_track`. Без
|
||||||
|
`--yes` это dry-run, который только сообщает, что было бы удалено. Это единственное место в
|
||||||
|
коде, которое вообще что-то удаляет из плейлиста.
|
||||||
|
|
||||||
- **Троттлинг и резюмируемость**: все сетевые циклы (загрузка треков, языков) делают паузу между
|
- **Троттлинг и резюмируемость**: все сетевые циклы (загрузка треков, языков) делают паузу между
|
||||||
запросами (`config.toml`, `[throttle]`) и пишут кэш атомарно (temp-файл + rename) после каждого
|
запросами (`config.toml`, `[throttle]`) и пишут кэш атомарно (temp-файл + rename) после каждого
|
||||||
|
|
@ -217,9 +227,13 @@ report/apply/digest --source <url> → source.parse_playlist_url() → (user_id,
|
||||||
и `added_at` выше, только для `report --extra`.
|
и `added_at` выше, только для `report --extra`.
|
||||||
- `cache/genre_catalog.json` — плоский снимок дерева жанров Яндекса (id -> title, root_id).
|
- `cache/genre_catalog.json` — плоский снимок дерева жанров Яндекса (id -> title, root_id).
|
||||||
- `cache/lyrics_lang.json` — язык текста песни по треку (id -> language code | null).
|
- `cache/lyrics_lang.json` — язык текста песни по треку (id -> language code | null).
|
||||||
- `cache/lyrics_text.json` — текст песни по треку (numeric id -> текст | null, только RU-треки).
|
- `cache/lyrics_text.json` — текст песни по треку (numeric id -> текст | null). Только RU-треки,
|
||||||
|
если не загружено через `--all-languages`. Пишется инкрементально не только
|
||||||
|
`lyrics.fetch_lyrics_text`, но и попутно `language.fetch_api_languages` на каждом `report`/
|
||||||
|
`apply` — она кэширует RU-текст, который уже получила, определяя язык текста песни.
|
||||||
- `cache/lyrics_analysis.json` — запись анализа на трек: `{track_id: {model, prompt_version,
|
- `cache/lyrics_analysis.json` — запись анализа на трек: `{track_id: {model, prompt_version,
|
||||||
lyrics_hash, mood, themes, emotional_arc, pov, tone, description, error?}}`.
|
lyrics_hash, analyzed_at, mood, themes, emotional_arc, pov, register, concreteness, stance,
|
||||||
|
confidence, summary, resonance, key_line, key_line_verified, error?}}`.
|
||||||
- `out/lyrics_digest.md` — полный построчный разбор, сгруппированный по основному настроению.
|
- `out/lyrics_digest.md` — полный построчный разбор, сгруппированный по основному настроению.
|
||||||
- `config.toml` — включает блок `[ollama]` с полями `host`, `model`, `prompt_version`, `timeout`,
|
- `config.toml` — включает блок `[ollama]` с полями `host`, `model`, `prompt_version`, `timeout`,
|
||||||
`keep_alive`.
|
`keep_alive`.
|
||||||
|
|
|
||||||
20
README.md
20
README.md
|
|
@ -45,9 +45,11 @@ All commands below use the Windows `.venv\Scripts\...` path — on Linux/macOS s
|
||||||
.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 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 --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 apply --yes # create playlists and add all tracks (changes the account)
|
||||||
|
.venv\Scripts\python -m sort_ym dedupe # dry run: report duplicate track insertions within playlists
|
||||||
|
.venv\Scripts\python -m sort_ym dedupe --yes # remove those duplicates (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
|
.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
|
||||||
.venv\Scripts\python -m sort_ym lyrics # download the lyrics text of Russian-language tracks (cache/lyrics_text.json); does not change the account
|
.venv\Scripts\python -m sort_ym lyrics # download the lyrics text of Russian-language tracks (cache/lyrics_text.json); does not change the account
|
||||||
.venv\Scripts\python -m sort_ym analyze # analyze those lyrics with a local Ollama model (cache/lyrics_analysis.json); no internet, runs for hours, resumable
|
.venv\Scripts\python -m sort_ym analyze # analyze those lyrics with a local Ollama model (cache/lyrics_analysis.json); no internet, runs for hours, resumable; --model overrides the Ollama model from config.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
On repeated runs `fetch` only downloads new tracks, and `apply` does not duplicate tracks already added.
|
On repeated runs `fetch` only downloads new tracks, and `apply` does not duplicate tracks already added.
|
||||||
|
|
@ -92,7 +94,7 @@ The cache consists of six files:
|
||||||
- `cache/artist_genres.json` — genre tags for each artist (an artist can have several);
|
- `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/genre_catalog.json` — a snapshot of Yandex's genre tree as of the last run;
|
||||||
- `cache/lyrics_lang.json` — detected lyrics language per track;
|
- `cache/lyrics_lang.json` — detected lyrics language per track;
|
||||||
- `cache/lyrics_text.json` — lyrics text of Russian-language tracks;
|
- `cache/lyrics_text.json` — lyrics text of Russian-language tracks (or of every track with lyrics if fetched via `--all-languages`); populated not only by `lyrics` but also, as a side effect, by `report`/`apply`, which look up the same per-track lyrics language and cache the RU text if found;
|
||||||
- `cache/lyrics_analysis.json` — per-track lyric analysis from the local model.
|
- `cache/lyrics_analysis.json` — per-track lyric analysis from the local model.
|
||||||
|
|
||||||
Deleting `cache/` makes `fetch` and `report` re-fetch everything from scratch on the next run.
|
Deleting `cache/` makes `fetch` and `report` re-fetch everything from scratch on the next run.
|
||||||
|
|
@ -147,7 +149,7 @@ the table in `sort_ym/genres.py` can be extended.
|
||||||
|
|
||||||
An LLM can't digest a full per-track dump of ~2000 tracks as well as a compact summary — so
|
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
|
`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,
|
hard size ceilings: at most 11 lines for top-level genres, ~36 for sub-genres, 3 for languages,
|
||||||
~13 for decades, and top artists/top albums are bounded by `config.toml`, not by library size.
|
~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
|
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.
|
stays a couple hundred lines at most, regardless of whether the library has 500 tracks or 10000.
|
||||||
|
|
@ -173,8 +175,9 @@ Example `out/digest.md` (abridged):
|
||||||
## Lyric analysis (Russian tracks)
|
## Lyric analysis (Russian tracks)
|
||||||
|
|
||||||
`lyrics` downloads the lyrics text, `analyze` reads it with a local model and writes a per-track
|
`lyrics` downloads the lyrics text, `analyze` reads it with a local model and writes a per-track
|
||||||
profile: mood, themes, emotional arc, point of view, tone, plus a short description of what the
|
profile: mood, themes, emotional arc, point of view, register, concreteness, stance, a confidence
|
||||||
song is about and why it might resonate.
|
score, plus a short summary/resonance of what the song is about and why it might resonate, and a
|
||||||
|
verbatim key line quoted from the lyrics.
|
||||||
|
|
||||||
```
|
```
|
||||||
.venv\Scripts\python -m sort_ym lyrics # lyrics text of Russian-language tracks
|
.venv\Scripts\python -m sort_ym lyrics # lyrics text of Russian-language tracks
|
||||||
|
|
@ -272,8 +275,10 @@ python -m sort_ym apply --source https://music.yandex.ru/users/vasya/playlists/1
|
||||||
|
|
||||||
There is no official API for personal Yandex Music library access — this tool uses the
|
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
|
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,
|
account — keep it local and never publish it. The tool only performs safe actions on your library
|
||||||
creating playlists, adding tracks) and never deletes anything.
|
(reading likes, creating playlists, adding tracks) and never deletes liked tracks. The only
|
||||||
|
deletion it performs at all is `dedupe --yes`, which removes duplicate insertions of the same
|
||||||
|
track within a playlist it created.
|
||||||
|
|
||||||
## For developers
|
## For developers
|
||||||
|
|
||||||
|
|
@ -285,6 +290,7 @@ Python 3.11+, `yandex-music==3.0.0`, standard library (`tomllib`, `csv`, `unicod
|
||||||
.venv\Scripts\python -m sort_ym fetch
|
.venv\Scripts\python -m sort_ym fetch
|
||||||
.venv\Scripts\python -m sort_ym report
|
.venv\Scripts\python -m sort_ym report
|
||||||
.venv\Scripts\python -m sort_ym apply --limit 20 --yes
|
.venv\Scripts\python -m sort_ym apply --limit 20 --yes
|
||||||
|
.venv\Scripts\python -m sort_ym dedupe
|
||||||
.venv\Scripts\python -m sort_ym digest
|
.venv\Scripts\python -m sort_ym digest
|
||||||
.venv\Scripts\python -m sort_ym report --source https://music.yandex.ru/users/vasya/playlists/1001
|
.venv\Scripts\python -m sort_ym report --source https://music.yandex.ru/users/vasya/playlists/1001
|
||||||
```
|
```
|
||||||
|
|
|
||||||
21
README.ru.md
21
README.ru.md
|
|
@ -45,9 +45,11 @@ python -m venv .venv
|
||||||
.venv\Scripts\python -m sort_ym report # посчитать жанр и язык каждого трека, сохранить out/report.csv (аккаунт не меняет)
|
.venv\Scripts\python -m sort_ym report # посчитать жанр и язык каждого трека, сохранить out/report.csv (аккаунт не меняет)
|
||||||
.venv\Scripts\python -m sort_ym apply --limit 20 --yes # пробный запуск: создать плейлисты и добавить в них первые 20 треков
|
.venv\Scripts\python -m sort_ym apply --limit 20 --yes # пробный запуск: создать плейлисты и добавить в них первые 20 треков
|
||||||
.venv\Scripts\python -m sort_ym apply --yes # создать плейлисты и добавить в них все треки (меняет аккаунт)
|
.venv\Scripts\python -m sort_ym apply --yes # создать плейлисты и добавить в них все треки (меняет аккаунт)
|
||||||
|
.venv\Scripts\python -m sort_ym dedupe # пробный запуск: показать повторные вставки одного трека в плейлистах
|
||||||
|
.venv\Scripts\python -m sort_ym dedupe --yes # убрать эти дубли (меняет аккаунт)
|
||||||
.venv\Scripts\python -m sort_ym digest # сводка библиотеки (топ-исполнители, жанры) в out/digest.md - чтобы вставить в чат с LLM; сеть не используется
|
.venv\Scripts\python -m sort_ym digest # сводка библиотеки (топ-исполнители, жанры) в out/digest.md - чтобы вставить в чат с LLM; сеть не используется
|
||||||
.venv\Scripts\python -m sort_ym lyrics # загрузить тексты песен русскоязычных треков (cache/lyrics_text.json), аккаунт не меняется
|
.venv\Scripts\python -m sort_ym lyrics # загрузить тексты песен русскоязычных треков (cache/lyrics_text.json), аккаунт не меняется
|
||||||
.venv\Scripts\python -m sort_ym analyze # разобрать эти тексты локальной моделью Ollama (cache/lyrics_analysis.json), без интернета, идёт часами, продолжается после обрыва
|
.venv\Scripts\python -m sort_ym analyze # разобрать эти тексты локальной моделью Ollama (cache/lyrics_analysis.json), без интернета, идёт часами, продолжается после обрыва; --model переопределяет модель Ollama из config.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
При повторных запусках `fetch` докачивает только новые треки, а `apply` не дублирует уже добавленные треки.
|
При повторных запусках `fetch` докачивает только новые треки, а `apply` не дублирует уже добавленные треки.
|
||||||
|
|
@ -92,7 +94,10 @@ rm -rf cache # Linux/macOS
|
||||||
- `cache/artist_genres.json` — жанровые теги каждого артиста (может быть несколько на артиста);
|
- `cache/artist_genres.json` — жанровые теги каждого артиста (может быть несколько на артиста);
|
||||||
- `cache/genre_catalog.json` — снимок дерева жанров Яндекса на момент последнего запуска;
|
- `cache/genre_catalog.json` — снимок дерева жанров Яндекса на момент последнего запуска;
|
||||||
- `cache/lyrics_lang.json` — определённые языки текстов для каждого трека;
|
- `cache/lyrics_lang.json` — определённые языки текстов для каждого трека;
|
||||||
- `cache/lyrics_text.json` — тексты песен русскоязычных треков;
|
- `cache/lyrics_text.json` — тексты песен русскоязычных треков (либо всех треков с текстом, если
|
||||||
|
загружены через `--all-languages`); пополняется не только командой `lyrics`, но и попутно
|
||||||
|
командами `report`/`apply` — они запрашивают тот же язык текста песни на трек и кэшируют RU-текст,
|
||||||
|
если он нашёлся;
|
||||||
- `cache/lyrics_analysis.json` — разбор текста каждого трека локальной моделью.
|
- `cache/lyrics_analysis.json` — разбор текста каждого трека локальной моделью.
|
||||||
|
|
||||||
Удаление `cache/` заставит `fetch` и `report` переполучить всё с нуля при следующем запуске.
|
Удаление `cache/` заставит `fetch` и `report` переполучить всё с нуля при следующем запуске.
|
||||||
|
|
@ -147,7 +152,7 @@ rm .token # Linux/macOS
|
||||||
|
|
||||||
Полный построчный список из ~2000 треков LLM переварить не может так же качественно, как
|
Полный построчный список из ~2000 треков LLM переварить не может так же качественно, как
|
||||||
компактную сводку — поэтому `digest` не выгружает треки построчно, а агрегирует библиотеку в
|
компактную сводку — поэтому `digest` не выгружает треки построчно, а агрегирует библиотеку в
|
||||||
несколько разделов с жёстко ограниченным размером: не более 11 строк по крупным жанрам, ~34 по
|
несколько разделов с жёстко ограниченным размером: не более 11 строк по крупным жанрам, ~36 по
|
||||||
под-жанрам, 3 по языкам, ~13 по десятилетиям, а топ-исполнителей и топ-альбомов ограничен
|
под-жанрам, 3 по языкам, ~13 по десятилетиям, а топ-исполнителей и топ-альбомов ограничен
|
||||||
`config.toml`, а не размером библиотеки. Всё, что не попало в топ, схлопывается в одну фразу с
|
`config.toml`, а не размером библиотеки. Всё, что не попало в топ, схлопывается в одну фразу с
|
||||||
суммарными числами. Итоговый файл — не больше пары сотен строк независимо от того, 500 треков в
|
суммарными числами. Итоговый файл — не больше пары сотен строк независимо от того, 500 треков в
|
||||||
|
|
@ -174,8 +179,9 @@ $ python -m sort_ym digest
|
||||||
## Разбор текстов (русскоязычные треки)
|
## Разбор текстов (русскоязычные треки)
|
||||||
|
|
||||||
`lyrics` загружает текст песни, `analyze` читает его локальной моделью и пишет профиль каждого
|
`lyrics` загружает текст песни, `analyze` читает его локальной моделью и пишет профиль каждого
|
||||||
трека: настроение, темы, эмоциональную арку, ракурс, тон, плюс короткое описание того, о чём
|
трека: настроение, темы, эмоциональную арку, ракурс, регистр речи, конкретность, отношение к
|
||||||
песня и чем она может цеплять.
|
предмету (`stance`), оценку уверенности, плюс короткое summary/resonance о том, о чём песня и чем
|
||||||
|
она может цеплять, и дословную ключевую строчку из текста.
|
||||||
|
|
||||||
```
|
```
|
||||||
.venv\Scripts\python -m sort_ym lyrics # тексты песен русскоязычных треков
|
.venv\Scripts\python -m sort_ym lyrics # тексты песен русскоязычных треков
|
||||||
|
|
@ -274,7 +280,9 @@ python -m sort_ym apply --source https://music.yandex.ru/users/vasya/playlists/1
|
||||||
Официального API для личной библиотеки Яндекс.Музыки нет — инструмент использует
|
Официального API для личной библиотеки Яндекс.Музыки нет — инструмент использует
|
||||||
неофициальную библиотеку `yandex-music`. OAuth-токен в `.token` даёт полный доступ к аккаунту
|
неофициальную библиотеку `yandex-music`. OAuth-токен в `.token` даёт полный доступ к аккаунту
|
||||||
Яндекса — храните его локально и не публикуйте. Инструмент выполняет только безопасные действия
|
Яндекса — храните его локально и не публикуйте. Инструмент выполняет только безопасные действия
|
||||||
(чтение лайков, создание плейлистов, добавление треков) и ничего не удаляет.
|
с библиотекой (чтение лайков, создание плейлистов, добавление треков) и никогда не удаляет
|
||||||
|
лайкнутые треки. Единственное удаление, которое он вообще выполняет — `dedupe --yes`, убирающая
|
||||||
|
повторные вставки одного и того же трека внутри созданного им плейлиста.
|
||||||
|
|
||||||
## For developers
|
## For developers
|
||||||
|
|
||||||
|
|
@ -286,6 +294,7 @@ Python 3.11+, `yandex-music==3.0.0`, стандартная библиотека
|
||||||
.venv\Scripts\python -m sort_ym fetch
|
.venv\Scripts\python -m sort_ym fetch
|
||||||
.venv\Scripts\python -m sort_ym report
|
.venv\Scripts\python -m sort_ym report
|
||||||
.venv\Scripts\python -m sort_ym apply --limit 20 --yes
|
.venv\Scripts\python -m sort_ym apply --limit 20 --yes
|
||||||
|
.venv\Scripts\python -m sort_ym dedupe
|
||||||
.venv\Scripts\python -m sort_ym digest
|
.venv\Scripts\python -m sort_ym digest
|
||||||
.venv\Scripts\python -m sort_ym report --source https://music.yandex.ru/users/vasya/playlists/1001
|
.venv\Scripts\python -m sort_ym report --source https://music.yandex.ru/users/vasya/playlists/1001
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,7 @@ def genre_stats(rows: list[dict]) -> list[dict]:
|
||||||
|
|
||||||
|
|
||||||
def fine_stats(rows: list[dict]) -> list[dict]:
|
def fine_stats(rows: list[dict]) -> list[dict]:
|
||||||
"""[{"fine", "tracks"}], максимум ~34 строки (размер genres.ROOT_BUCKET)."""
|
"""[{"fine", "tracks"}], максимум ~36 строк (размер genres.ROOT_BUCKET)."""
|
||||||
counts = Counter(row["fine_bucket"] for row in rows)
|
counts = Counter(row["fine_bucket"] for row in rows)
|
||||||
result = [{"fine": f, "tracks": n} for f, n in counts.items()]
|
result = [{"fine": f, "tracks": n} for f, n in counts.items()]
|
||||||
result.sort(key=lambda g: (-g["tracks"], g["fine"]))
|
result.sort(key=lambda g: (-g["tracks"], g["fine"]))
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue