Sort_YM/tests/test_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.4 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 yandex_music.exceptions import BadRequestError, TimedOutError
from sort_ym import apply as apply_mod
class FakePlaylist:
def __init__(self, kind, title, revision=1, tracks=None):
self.kind = kind
self.title = title
self.revision = revision
self.tracks = tracks or []
class FakeTrackShort:
def __init__(self, track_id):
self.track_id = track_id
class FakeClient:
def __init__(self):
self.playlists = {100: FakePlaylist(100, "Рок — RU", revision=5, tracks=[FakeTrackShort("1:10")])}
self.created = []
self.inserted = []
self.next_kind = 200
def users_playlists_list(self):
return list(self.playlists.values())
def users_playlists(self, kind):
return self.playlists.get(kind)
def users_playlists_create(self, title, visibility="private"):
kind = self.next_kind
self.next_kind += 1
p = FakePlaylist(kind, title)
self.playlists[kind] = p
self.created.append(title)
return p
def users_playlists_insert_track(self, kind, track_id, album_id, revision=1):
self.inserted.append((kind, track_id, album_id, revision))
p = self.playlists[kind]
p.revision = revision + 1
return p
class FlakyClient:
"""insert_track кидает таймаут на первой попытке, но трек всё же
успевает примениться на сервере (ответ теряется, а не сам запрос)."""
def __init__(self):
self.playlist = FakePlaylist(100, "Рок — RU", revision=5, tracks=[])
self.insert_calls = 0
self.server_tracks: list[tuple[int, int]] = []
def users_playlists_list(self):
return [self.playlist]
def users_playlists(self, kind):
self.playlist.tracks = [FakeTrackShort(f"{i}:{a}") for (i, a) in self.server_tracks]
return self.playlist
def users_playlists_insert_track(self, kind, track_id, album_id, revision=1):
self.insert_calls += 1
self.server_tracks.append((track_id, album_id))
if self.insert_calls == 1:
raise TimedOutError()
self.playlist.revision += 1
self.playlist.tracks = [FakeTrackShort(f"{i}:{a}") for (i, a) in self.server_tracks]
return self.playlist
def test_apply_skips_existing_and_creates_only_missing_playlist():
client = FakeClient()
rows = [
{"target_playlist": "Рок — RU", "id": 1, "album_id": 10}, # уже в плейлисте
{"target_playlist": "Рок — RU", "id": 5, "album_id": 50}, # новый трек в существующий плейлист
{"target_playlist": "Поп — INT", "id": 9, "album_id": 90}, # совсем новый плейлист
]
apply_mod.apply_classification(client, rows, delay=0)
assert client.created == ["Поп — INT"]
assert client.inserted == [(100, 5, 50, 5), (200, 9, 90, 1)]
def test_apply_does_not_duplicate_track_on_timeout_after_server_applied_write():
client = FlakyClient()
rows = [{"target_playlist": "Рок — RU", "id": 1, "album_id": 10}]
apply_mod.apply_classification(client, rows, delay=0)
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)], "второй трек должен вставиться, несмотря на ошибку первого"