mirror of
https://github.com/Horizontal42/Sort_YM.git
synced 2026-11-10 19:11: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
|
import time
|
||||||
|
|
||||||
from yandex_music import Client, Playlist
|
from yandex_music import Client, Playlist
|
||||||
from yandex_music.exceptions import NetworkError
|
from yandex_music.exceptions import BadRequestError, NetworkError
|
||||||
|
|
||||||
from .ymclient import with_retries
|
from .ymclient import with_retries
|
||||||
|
|
||||||
|
|
@ -62,6 +62,10 @@ def _insert_track_verified(
|
||||||
max_attempts=1,
|
max_attempts=1,
|
||||||
)
|
)
|
||||||
return updated.revision if updated is not None and updated.revision is not None else revision
|
return updated.revision if updated is not None and updated.revision is not None else revision
|
||||||
|
except BadRequestError:
|
||||||
|
# Детерминированный отказ сервера (например, невалидные данные трека) - ответ
|
||||||
|
# точно получен, трек точно не вставлен. Повторять бессмысленно, пробрасываем сразу.
|
||||||
|
raise
|
||||||
except NetworkError as e:
|
except NetworkError as e:
|
||||||
last_attempt = attempt == max_attempts - 1
|
last_attempt = attempt == max_attempts - 1
|
||||||
print(f" сетевая ошибка при вставке трека ({e}), проверяю фактическое состояние плейлиста...")
|
print(f" сетевая ошибка при вставке трека ({e}), проверяю фактическое состояние плейлиста...")
|
||||||
|
|
@ -98,15 +102,34 @@ def apply_classification(
|
||||||
revision = playlist.revision or 1
|
revision = playlist.revision or 1
|
||||||
|
|
||||||
added = 0
|
added = 0
|
||||||
|
no_album = 0
|
||||||
|
failed = 0
|
||||||
for row in playlist_rows:
|
for row in playlist_rows:
|
||||||
track_key = f"{row['id']}:{row['album_id']}" if row["album_id"] else str(row["id"])
|
track_key = f"{row['id']}:{row['album_id']}" if row["album_id"] else str(row["id"])
|
||||||
if track_key in already:
|
if track_key in already:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if row["album_id"] is None:
|
||||||
|
# Самозалитые/пиратские треки без альбома - Яндекс не даёт вставить такой
|
||||||
|
# трек в плейлист (albumId обязателен), это не временный сбой. Пропускаем.
|
||||||
|
no_album += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
revision = _insert_track_verified(client, playlist, row, track_key, revision)
|
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)
|
already.add(track_key)
|
||||||
added += 1
|
added += 1
|
||||||
time.sleep(delay)
|
time.sleep(delay)
|
||||||
|
|
||||||
skipped = len(playlist_rows) - added
|
already_had = len(playlist_rows) - added - no_album - failed
|
||||||
print(f"{title}: добавлено {added}, уже было {skipped}")
|
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 typing import Callable, Iterator, List, TypeVar
|
||||||
|
|
||||||
from yandex_music import Client
|
from yandex_music import Client
|
||||||
from yandex_music.exceptions import NetworkError, NotFoundError
|
from yandex_music.exceptions import BadRequestError, NetworkError, NotFoundError
|
||||||
|
|
||||||
T = TypeVar("T")
|
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:
|
def with_retries(fn: Callable[[], T], max_attempts: int = 4, base_delay: float = 2.0) -> T:
|
||||||
"""Повторяет вызов при временных сетевых сбоях (таймауты и т.п.) с экспоненциальной паузой.
|
"""Повторяет вызов при временных сетевых сбоях (таймауты и т.п.) с экспоненциальной паузой.
|
||||||
|
|
||||||
NotFoundError не повторяется - это окончательный ответ API, а не сбой сети,
|
NotFoundError и BadRequestError не повторяются - это окончательные ответы сервера
|
||||||
хотя формально он тоже унаследован от NetworkError.
|
(данные точно не найдены / запрос точно невалиден), а не сбои сети, хотя формально
|
||||||
|
оба тоже унаследованы от NetworkError. Повтор такого запроса даст тот же результат.
|
||||||
"""
|
"""
|
||||||
last_exc: NetworkError | None = None
|
last_exc: NetworkError | None = None
|
||||||
for attempt in range(max_attempts):
|
for attempt in range(max_attempts):
|
||||||
try:
|
try:
|
||||||
return fn()
|
return fn()
|
||||||
except NotFoundError:
|
except (NotFoundError, BadRequestError):
|
||||||
raise
|
raise
|
||||||
except NetworkError as e:
|
except NetworkError as e:
|
||||||
last_exc = 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
|
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.insert_calls == 1, "insert_track не должен вызываться повторно вслепую при таймауте"
|
||||||
assert client.server_tracks == [(1, 10)], "трек должен оказаться в плейлисте ровно один раз"
|
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