mirror of
https://github.com/Horizontal42/Sort_YM.git
synced 2026-11-10 22:41:59 +03:00
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 на отдельном треке больше не прерывает весь прогон - пропускается со счётчиком, остальные треки обрабатываются дальше.
This commit is contained in:
parent
0902d86bcd
commit
384095f496
3 changed files with 74 additions and 9 deletions
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
import time
|
||||
|
||||
from yandex_music import Client, Playlist
|
||||
from yandex_music.exceptions import NetworkError
|
||||
from yandex_music.exceptions import BadRequestError, NetworkError
|
||||
|
||||
from .ymclient import with_retries
|
||||
|
||||
|
|
@ -62,6 +62,10 @@ def _insert_track_verified(
|
|||
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}), проверяю фактическое состояние плейлиста...")
|
||||
|
|
@ -98,15 +102,34 @@ def apply_classification(
|
|||
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)
|
||||
|
||||
skipped = len(playlist_rows) - added
|
||||
print(f"{title}: добавлено {added}, уже было {skipped}")
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import time
|
|||
from typing import Callable, Iterator, List, TypeVar
|
||||
|
||||
from yandex_music import Client
|
||||
from yandex_music.exceptions import NetworkError, NotFoundError
|
||||
from yandex_music.exceptions import BadRequestError, NetworkError, NotFoundError
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
|
@ -24,14 +24,15 @@ def chunked(items: List[T], size: int) -> Iterator[List[T]]:
|
|||
def with_retries(fn: Callable[[], T], max_attempts: int = 4, base_delay: float = 2.0) -> T:
|
||||
"""Повторяет вызов при временных сетевых сбоях (таймауты и т.п.) с экспоненциальной паузой.
|
||||
|
||||
NotFoundError не повторяется - это окончательный ответ API, а не сбой сети,
|
||||
хотя формально он тоже унаследован от NetworkError.
|
||||
NotFoundError и BadRequestError не повторяются - это окончательные ответы сервера
|
||||
(данные точно не найдены / запрос точно невалиден), а не сбои сети, хотя формально
|
||||
оба тоже унаследованы от NetworkError. Повтор такого запроса даст тот же результат.
|
||||
"""
|
||||
last_exc: NetworkError | None = None
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
return fn()
|
||||
except NotFoundError:
|
||||
except (NotFoundError, BadRequestError):
|
||||
raise
|
||||
except NetworkError as e:
|
||||
last_exc = e
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from yandex_music.exceptions import TimedOutError
|
||||
from yandex_music.exceptions import BadRequestError, TimedOutError
|
||||
|
||||
from sort_ym import apply as apply_mod
|
||||
|
||||
|
|
@ -92,3 +92,44 @@ def test_apply_does_not_duplicate_track_on_timeout_after_server_applied_write():
|
|||
|
||||
assert client.insert_calls == 1, "insert_track не должен вызываться повторно вслепую при таймауте"
|
||||
assert client.server_tracks == [(1, 10)], "трек должен оказаться в плейлисте ровно один раз"
|
||||
|
||||
|
||||
def test_apply_skips_tracks_without_album_id():
|
||||
# Самозалитые/пиратские треки без альбома - Яндекс не даёт вставить такой трек в
|
||||
# плейлист (albumId обязателен). Должны пропускаться без попытки вставки и без падения.
|
||||
client = FakeClient()
|
||||
rows = [
|
||||
{"target_playlist": "Поп — INT", "id": 1, "album_id": None, "title": "no album"},
|
||||
{"target_playlist": "Поп — INT", "id": 2, "album_id": 20, "title": "has album"},
|
||||
]
|
||||
|
||||
apply_mod.apply_classification(client, rows, delay=0)
|
||||
|
||||
assert client.inserted == [(200, 2, 20, 1)], "трек без альбома не должен уходить в insert_track"
|
||||
|
||||
|
||||
class BadRequestClient(FakeClient):
|
||||
"""insert_track кидает BadRequestError (детерминированный отказ) на конкретном треке."""
|
||||
|
||||
def __init__(self, bad_track_id):
|
||||
super().__init__()
|
||||
self.playlists = {200: FakePlaylist(200, "Поп — INT", revision=1, tracks=[])}
|
||||
self.next_kind = 201
|
||||
self._bad_track_id = bad_track_id
|
||||
|
||||
def users_playlists_insert_track(self, kind, track_id, album_id, revision=1):
|
||||
if track_id == self._bad_track_id:
|
||||
raise BadRequestError({"name": "wrong-json", "message": "Invalid JSON rules"})
|
||||
return super().users_playlists_insert_track(kind, track_id, album_id, revision)
|
||||
|
||||
|
||||
def test_apply_continues_after_bad_request_on_single_track():
|
||||
client = BadRequestClient(bad_track_id=1)
|
||||
rows = [
|
||||
{"target_playlist": "Поп — INT", "id": 1, "album_id": 10, "title": "broken"},
|
||||
{"target_playlist": "Поп — INT", "id": 2, "album_id": 20, "title": "ok"},
|
||||
]
|
||||
|
||||
apply_mod.apply_classification(client, rows, delay=0)
|
||||
|
||||
assert client.inserted == [(200, 2, 20, 1)], "второй трек должен вставиться, несмотря на ошибку первого"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue