Sort_YM/sort_ym/apply.py
Andrew Pimenov 384095f496 skip tracks without album on insert, stop retrying BadRequestError
apply --yes падал целиком на первом же самозалитом/пиратском треке без
альбома (105 из 1984) - Яндекс отвечает BadRequestError (wrong-json),
т.к. albumId обязателен, а with_retries лечил это как временный сетевой
сбой и вслепую повторял заведомо безнадёжный запрос 4 раза. Теперь
BadRequestError (как и NotFoundError) не ретраится нигде - это
окончательный ответ сервера, а не таймаут. Треки без album_id
пропускаются проактивно с понятным сообщением, а неожиданный
BadRequestError на отдельном треке больше не прерывает весь прогон -
пропускается со счётчиком, остальные треки обрабатываются дальше.
2026-07-06 15:06:07 +03:00

135 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import time
from yandex_music import Client, Playlist
from yandex_music.exceptions import BadRequestError, NetworkError
from .ymclient import with_retries
def _existing_playlists_by_title(client: Client) -> dict[str, Playlist]:
playlists = with_retries(lambda: client.users_playlists_list())
return {p.title: p for p in playlists if p.title}
def _existing_track_ids(client: Client, playlist: Playlist) -> set[str]:
full = with_retries(lambda: client.users_playlists(playlist.kind))
if full is None:
return set()
return {t.track_id for t in full.tracks}
def _get_or_create_playlist(client: Client, title: str, existing: dict[str, Playlist]) -> Playlist:
if title in existing:
return existing[title]
try:
playlist = with_retries(lambda: client.users_playlists_create(title, visibility="private"), max_attempts=1)
except NetworkError:
# Таймаут не значит, что запрос не дошёл до сервера - плейлист мог всё же
# создаться. Перепроверяем по имени вместо слепого повтора (иначе - дубль плейлиста).
fresh = _existing_playlists_by_title(client)
if title in fresh:
existing[title] = fresh[title]
return fresh[title]
raise
existing[title] = playlist
return playlist
def _insert_track_verified(
client: Client,
playlist: Playlist,
row: dict,
track_key: str,
revision: int,
max_attempts: int = 4,
) -> int:
"""Добавляет трек в плейлист, возвращает новую revision.
users_playlists_insert_track - операция записи, а не чтения: при таймауте
непонятно, применилась ли вставка на сервере. Слепой повтор такого вызова
рискует добавить трек второй раз. Поэтому при сетевой ошибке перепроверяем
реальное содержимое плейлиста и повторяем вставку, только если трека там
действительно ещё нет.
"""
for attempt in range(max_attempts):
try:
updated = with_retries(
lambda: client.users_playlists_insert_track(
playlist.kind, row["id"], row["album_id"], revision=revision
),
max_attempts=1,
)
return updated.revision if updated is not None and updated.revision is not None else revision
except BadRequestError:
# Детерминированный отказ сервера (например, невалидные данные трека) - ответ
# точно получен, трек точно не вставлен. Повторять бессмысленно, пробрасываем сразу.
raise
except NetworkError as e:
last_attempt = attempt == max_attempts - 1
print(f" сетевая ошибка при вставке трека ({e}), проверяю фактическое состояние плейлиста...")
fresh = with_retries(lambda: client.users_playlists(playlist.kind))
if fresh is not None:
revision = fresh.revision or revision
if track_key in {t.track_id for t in fresh.tracks}:
return revision
if last_attempt:
raise
time.sleep(2.0 * (2**attempt))
raise RuntimeError("unreachable")
def apply_classification(
client: Client,
rows: list[dict],
delay: float,
limit: int | None = None,
) -> None:
if limit is not None:
rows = rows[:limit]
existing_playlists = _existing_playlists_by_title(client)
by_playlist: dict[str, list[dict]] = {}
for row in rows:
by_playlist.setdefault(row["target_playlist"], []).append(row)
for title, playlist_rows in by_playlist.items():
playlist = _get_or_create_playlist(client, title, existing_playlists)
already = _existing_track_ids(client, playlist)
revision = playlist.revision or 1
added = 0
no_album = 0
failed = 0
for row in playlist_rows:
track_key = f"{row['id']}:{row['album_id']}" if row["album_id"] else str(row["id"])
if track_key in already:
continue
if row["album_id"] is None:
# Самозалитые/пиратские треки без альбома - Яндекс не даёт вставить такой
# трек в плейлист (albumId обязателен), это не временный сбой. Пропускаем.
no_album += 1
continue
try:
revision = _insert_track_verified(client, playlist, row, track_key, revision)
except BadRequestError as e:
print(f" пропущен трек id={row['id']} ({row.get('title', '?')!r}): {e}")
failed += 1
continue
already.add(track_key)
added += 1
time.sleep(delay)
already_had = len(playlist_rows) - added - no_album - failed
summary = f"{title}: добавлено {added}, уже было {already_had}"
if no_album:
summary += f", пропущено без альбома {no_album}"
if failed:
summary += f", ошибок {failed}"
print(summary)