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.
Playlist deletes are positional (from/to index), so a network error
during dedupe just aborts that playlist instead of retrying blindly -
indices may have already shifted. Re-running dedupe is safe since it
recomputes ranges from the live playlist state each time.
Раньше жанр брался только с альбома (одна строка, часто общая:
rock/alternative/pop) - разные по звучанию артисты сваливались в одну
крупную корзину. Теперь classify_track сводит жанр трека и жанры
артиста (client.artists(), несколько тегов на артиста) на уровне
под-жанра: при согласии сигналов или отсутствии одного из них берём
имеющийся; при конфликте - если альбомный тег зонтичный (GENERIC_SLUGS)
доверяем артисту, если точный - треку (вероятный осознанный эксперимент
артиста в другом жанре). Один и тот же артист с разными по духу треками
теперь раскладывается по разным плейлистам.
Под-жанровые группы меньше config.small_group_min схлопываются в
родительскую крупную корзину (report.build_rows, двухпроходно,
без циклов - coarse_of детерминирована от fine).
Попутно: artist_ids трека может быть None (артист без привязки к
каталогу Яндекса, встречается у стороннего/пиратского аплоада) - раньше
это улетало в client.artists() как строка "None" и валилось
BadRequestError на батче; теперь такие id отфильтровываются на записи
и дополнительно на чтении кэша.
Медленная/нестабильная сеть до api.music.yandex.net роняла весь процесс
при первом же таймауте (5с по умолчанию у библиотеки). Добавлен
with_retries с экспоненциальной паузой на всех сетевых точках входа
(fetch, language, genres, apply, client.init), таймаут запроса поднят
до 20с (настраивается в config.toml). Заодно поправлен скрытый баг:
language.py раньше кэшировал таймаут как "текста нет" навсегда -
теперь только настоящий NotFoundError трактуется так.