[English](README.md) # Sort YM Раскладывает лайкнутые треки Яндекс.Музыки по плейлистам вида `Жанр — RU` / `Жанр — INT`. ``` $ python -m sort_ym report Распределение по плейлистам: Рок — INT: 143 Рок — RU: 287 Рэп — RU: 512 ... Отчёт сохранён: out/report.csv ``` ## Установка Требования: Python 3.11+ (используется `tomllib` из стандартной библиотеки), Windows/Linux/macOS, доступ в интернет. ``` python -m venv .venv .venv\Scripts\pip install -r requirements.txt # Windows .venv/bin/pip install -r requirements.txt # Linux/macOS ``` Для разработки (тесты): ``` .venv\Scripts\pip install -r requirements-dev.txt # Windows .venv/bin/pip install -r requirements-dev.txt # Linux/macOS ``` Все команды ниже используют путь Windows `.venv\Scripts\...` — на Linux/macOS вместо него `.venv/bin/...`. ## Что делает ### Основные команды ``` .venv\Scripts\python -m sort_ym auth # войти в аккаунт Яндекса через device-flow и сохранить токен в .token .venv\Scripts\python -m sort_ym fetch # скачать лайкнутые треки (cache/tracks.json) и жанры их артистов (cache/artist_genres.json); аккаунт не меняет .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 --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 lyrics # загрузить тексты песен русскоязычных треков (cache/lyrics_text.json), аккаунт не меняется .venv\Scripts\python -m sort_ym analyze # разобрать эти тексты локальной моделью Ollama (cache/lyrics_analysis.json), без интернета, идёт часами, продолжается после обрыва; --model переопределяет модель Ollama из config.toml ``` При повторных запусках `fetch` докачивает только новые треки, а `apply` не дублирует уже добавленные треки. ### Дополнительно: другой плейлист вместо лайков ``` .venv\Scripts\python -m sort_ym report --source <ссылка> # то же самое, но для чужого/другого плейлиста по ссылке .venv\Scripts\python -m sort_ym digest --source <ссылка> # дайджест чужого/другого плейлиста .venv\Scripts\python -m sort_ym apply --source <ссылка> --label Друг --yes # раскидать его треки по своим плейлистам "Жанр — RU (Друг)" ``` Подробнее — раздел «Анализ и раскидка другого плейлиста» ниже. ### Дополнительно: гибкая настройка report.csv По умолчанию `report` группирует и сортирует треки по целевому плейлисту с базовым набором колонок. Эти флаги это меняют — от самого востребованного к более редкому: ``` .venv\Scripts\python -m sort_ym report --order playlist # без сортировки по плейлистам - треки в исходном порядке источника (как в лайках/--source плейлисте) .venv\Scripts\python -m sort_ym report --timestamp # + дата добавления трека (added_at) .venv\Scripts\python -m sort_ym report --album # + дата релиза и лайки альбома (release_date, album_likes_count) .venv\Scripts\python -m sort_ym report --artist # + счётчики/рейтинг первого артиста трека .venv\Scripts\python -m sort_ym report --duration # + длительность трека (duration_ms) .venv\Scripts\python -m sort_ym report --version # + пометки версии трека/альбома (ремикс, юбилейное издание и т.п.) .venv\Scripts\python -m sort_ym report --timestamp --album # флаги можно комбинировать .venv\Scripts\python -m sort_ym report --extra --order playlist # --extra = сразу все доп. колонки + порядок как в источнике ``` ## Полная очистка кэша Если требуется начать с чистого листа (переполучить все данные с нуля), удалите папку `cache/`: ``` Remove-Item -Recurse -Force cache # Windows (PowerShell) rm -rf cache # Linux/macOS ``` Кэш состоит из шести файлов: - `cache/tracks.json` — список лайкнутых треков и их метаданные (включая id артистов); - `cache/artist_genres.json` — жанровые теги каждого артиста (может быть несколько на артиста); - `cache/genre_catalog.json` — снимок дерева жанров Яндекса на момент последнего запуска; - `cache/lyrics_lang.json` — определённые языки текстов для каждого трека; - `cache/lyrics_text.json` — тексты песен русскоязычных треков (либо всех треков с текстом, если загружены через `--all-languages`); пополняется не только командой `lyrics`, но и попутно командами `report`/`apply` — они запрашивают тот же язык текста песни на трек и кэшируют RU-текст, если он нашёлся; - `cache/lyrics_analysis.json` — разбор текста каждого трека локальной моделью. Удаление `cache/` заставит `fetch` и `report` переполучить всё с нуля при следующем запуске. Также можно удалить сгенерированные отчёты (`out/report.csv`, `out/digest.md`): ``` Remove-Item -Recurse -Force out # Windows (PowerShell) rm -rf out # Linux/macOS ``` Это просто результаты анализа, их удаление не повлияет на следующий запуск `report`/`digest`. **Важно:** файл `.token` (OAuth-токен) не входит в кэш и не удаляется при очистке `cache/`. Очистка кэша вас не разлогирует. Если нужна повторная авторизация, удалите `.token` отдельно: ``` Remove-Item .token # Windows (PowerShell) rm .token # Linux/macOS ``` ## Как определяется жанр и язык - **Жанр** определяется на **под-жанровом** уровне (Рок, Панк, Инди, Альтернатива, Метал, ..., не только 11 крупных групп), комбинируя два сигнала: 1. Жанр **альбома** трека (`genre_raw`) — сигнал по конкретному треку. 2. Жанры **артиста** (`Artist.genres`, может быть несколько тегов на артиста, точнее одного альбомного жанра) — сигнал по исполнителю в целом. Если сигналы совпадают — берётся под-жанр как есть. При конфликте: если альбомный тег общий ("зонтичный", вроде `rock`/`alternative`/`pop` — типичный дефолт Яндекса низкой уверенности) — побеждает жанр артиста; если альбомный тег точный/специфичный (`numetal`, `dnb`, `shanson`) — побеждает он, потому что это, скорее всего, реальный эксперимент артиста в другом жанре, а не шум. Так один и тот же артист с разными по духу треками (например, рОк-трек и панк-трек) раскладывается по разным плейлистам, а не весь скопом по одному "основному" жанру. Под-жанровые группы меньше `small_group_min` (по умолчанию 12 треков, `config.toml`) схлопываются в родительскую крупную корзину (Поп, Рок, Метал, Рэп, Электроника, Джаз и блюз, Классика, Фолк и world, Инди и альтернатива, Саундтреки, Разное), чтобы не плодить плейлисты на 1-2 трека. Реализация — `sort_ym/genres.py` (`classify_track`, `ROOT_BUCKET`) — таблица корней легко дополняется, если Яндекс вернёт незнакомый корневой жанр. - **Язык** — гибрид: 1. Если у трека есть текст песни — реальный язык из API (`ru` → RU, иначе → INT). 2. Иначе — подсказка по жанру (`rusrap`, `shanson`, `bard`, ...). 3. Иначе — по алфавиту названия/исполнителя (кириллица/латиница). 4. Если ничего не сработало — трек попадает в `... — Не определено`. После `report` в консоли также печатается список нераспознанных жанров (попавших в «Разное»), чтобы можно было дополнить таблицу в `sort_ym/genres.py`. ## Дайджест для LLM Полный построчный список из ~2000 треков LLM переварить не может так же качественно, как компактную сводку — поэтому `digest` не выгружает треки построчно, а агрегирует библиотеку в несколько разделов с жёстко ограниченным размером: не более 11 строк по крупным жанрам, ~36 по под-жанрам, 3 по языкам, ~13 по десятилетиям, а топ-исполнителей и топ-альбомов ограничен `config.toml`, а не размером библиотеки. Всё, что не попало в топ, схлопывается в одну фразу с суммарными числами. Итоговый файл — не больше пары сотен строк независимо от того, 500 треков в библиотеке или 10000. Без `--source` команда полностью офлайн: не обращается к сети, не требует `.token`, читает только уже загруженный `cache/` (нужно, чтобы `report` был запущен хотя бы раз, для `cache/genre_catalog.json`). С `--source` (см. раздел «Анализ и раскидка другого плейлиста» ниже) нужны и сеть, и токен, но `fetch` заранее не требуется. ``` $ python -m sort_ym digest Жанры: Инди и альтернатива: 680 Рок: 590 ... Дайджест сохранён: out/digest.md ``` Пример `out/digest.md` (сокращённо): ## Разбор текстов (русскоязычные треки) `lyrics` загружает текст песни, `analyze` читает его локальной моделью и пишет профиль каждого трека: настроение, темы, эмоциональную арку, ракурс, регистр речи, конкретность, отношение к предмету (`stance`), оценку уверенности, плюс короткое summary/resonance о том, о чём песня и чем она может цеплять, и дословную ключевую строчку из текста. ``` .venv\Scripts\python -m sort_ym lyrics # тексты песен русскоязычных треков .venv\Scripts\python -m sort_ym analyze # разбор локальной Ollama, часы локальных вычислений .venv\Scripts\python -m sort_ym digest # out/lyrics_digest.md + агрегат в out/digest.md ``` По умолчанию разбираются только русскоязычные треки — промпт и нарративные поля (`summary`/`resonance`/`key_line`) написаны на русском и проверены на пилотном прогоне по русской лирике; качество на других языках не проверялось. Флаг `--all-languages` у `lyrics` снимает фильтр и загружает/разбирает все треки с доступным текстом независимо от языка. Что нужно: запущенная локально Ollama с моделью из `config.toml`, блок `[ollama]` (проверить — `ollama list`). Интернет не используется: `analyze` не обращается к Яндексу вообще. Скорость — примерно 40 секунд на трек, около трёх часов на ~266 треков. Прогон прерываемый: результат пишется после каждого трека, Ctrl+C стоит максимум один трек, повторный запуск продолжает с места обрыва. Треки, упавшие по таймауту, повторяются при следующем запуске. Увеличение `prompt_version` в `config.toml` обесценивает весь кэш и запускает разбор заново. Пример `out/digest.md` (сокращённо): ``` ## Жанры (крупные корзины) - Инди и альтернатива — 680 (34%): INT 571 / RU 108 - Рок — 590 (29%): INT 518 / RU 72 ... ## Десятилетия - 2010-е — 620 (31%): Рок 280, Инди и альтернатива 200, Метал 90 ... ## Топ-40 исполнителей 1. Billy Talent — 66 (Рок; rock, punk) ... Ещё 1140 исполнителей с 1-4 треками, 1737 упоминаний суммарно; из них 835 — по одному треку. ## Топ-15 альбомов 1. Slipknot — Iowa (2001) — 12 ... Ещё 1401 альбом с 1-3 лайкнутыми треками. Треков с альбомов, где лайкнуто 2 и более: 732 из 2005 (37%). ``` **Важно:** поля `год выхода` и `название альбома` появились в кэше треков не с самого начала — если `cache/tracks.json` был создан старой версией `fetch`, разделы «Десятилетия» и «Топ альбомов» покажут «Неизвестно» до первого перезапуска `python -m sort_ym fetch` (он один раз доперекачает весь кэш, дальше — как обычно, только новые треки). ## Анализ и раскидка другого плейлиста `report`, `digest` и `apply` умеют работать не только с лайками, но и с любым плейлистом Яндекс.Музыки (своим другим или чужим публичным) — по ссылке, флагом `--source`. Как получить ссылку: откройте плейлист в веб-плеере Яндекс.Музыки и скопируйте адрес из строки браузера — вида `https://music.yandex.ru/users/<логин>/playlists/<номер>`. ``` python -m sort_ym report --source https://music.yandex.ru/users/vasya/playlists/1001 python -m sort_ym digest --source https://music.yandex.ru/users/vasya/playlists/1001 python -m sort_ym apply --source https://music.yandex.ru/users/vasya/playlists/1001 --label Вася --yes ``` - **`--label <имя>`** (только для `apply`) — треки лягут в отдельные плейлисты вида «Рок — RU (Вася)» вместо общих «Рок — RU». Без `--label` они попадут в те же плейлисты, что и ваши лайкнутые треки того же жанра/языка — это осознанный дефолт, но именно поэтому для чужого плейлиста стоит явно решить, нужно ли смешивать чужой вкус со своим. - **Никакого кэша и истории**: данные внешнего плейлиста загружаются заново при каждом запуске и никогда не сохраняются на диск (в отличие от лайков в `cache/tracks.json`) — так и задумано. Повторный запуск на той же ссылке снова скачает всё с нуля. - **Приватные плейлисты**: если плейлист недоступен (приватный либо истёк токен), команда выдаст понятную ошибку — либо попросите владельца сделать плейлист публичным, либо перезайдите (`python -m sort_ym auth`). - **Точность языка чуть ниже**: для внешнего источника язык не уточняется через API текста песни (это отдельный запрос на трек, и без кэша он был бы медленным на каждый запуск) — только по уже накопленному `cache/lyrics_lang.json` и эвристикам. Без `--label` трек может попасть не в тот языковой плейлист, а если его потом лайкнуть — окажется добавлен в оба. - Для `apply` нет пробного режима с `--source` (только `--limit`) — сначала проверьте `report --source`, чтобы увидеть распределение по плейлистам до реальной вставки. - Результаты `report --source`/`digest --source` сохраняются в отдельные файлы — `out/report_source.csv`/`out/digest_source.md` — и не затирают ваши основные `out/report.csv`/ `out/digest.md`. - `report --source <ссылка> --order playlist` — треки в CSV пойдут в том же порядке, что и в самом плейлисте, без пересортировки по целевому плейлисту. ## Handy things - Первый прогон `apply` делайте с `--limit 20 --yes` и проверьте результат в приложении, прежде чем запускать на все ~2000 треков. - Настройки задержек между запросами — в `config.toml` (`[throttle]`). - Минимальный размер под-жанрового плейлиста — `config.toml` (`[classify] small_group_min`). - Сколько исполнителей/альбомов показывать поимённо в дайджесте — `config.toml` (`[digest] top_artists`, `top_albums`) или флаги `--top`/`--top-albums` команды `digest`. ## Риски (неофициальный API) Официального API для личной библиотеки Яндекс.Музыки нет — инструмент использует неофициальную библиотеку `yandex-music`. OAuth-токен в `.token` даёт полный доступ к аккаунту Яндекса — храните его локально и не публикуйте. Инструмент выполняет только безопасные действия с библиотекой (чтение лайков, создание плейлистов, добавление треков) и никогда не удаляет лайкнутые треки. Единственное удаление, которое он вообще выполняет — `dedupe --yes`, убирающая повторные вставки одного и того же трека внутри созданного им плейлиста. ## For developers Python 3.11+, `yandex-music==3.0.0`, стандартная библиотека (`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 dedupe .venv\Scripts\python -m sort_ym digest .venv\Scripts\python -m sort_ym report --source https://music.yandex.ru/users/vasya/playlists/1001 ``` Структура кода и поток данных — в [ARCHITECTURE.ru.md](ARCHITECTURE.ru.md). ## Credits Andrew Pimenov ## License [MIT License](LICENSE)