mirror of
https://github.com/Horizontal42/Sort_YM.git
synced 2026-11-10 15:42:02 +03:00
harden playlist read against None response from users_playlists
client.users_playlists(kind) silently returns None on any response-parse failure, not just when the playlist genuinely doesn't exist - this isn't an exception, so with_retries doesn't catch it. Previously this was treated as "playlist is empty", which risked re-inserting everything on top of existing tracks. Now retry with backoff and raise instead of guessing.
This commit is contained in:
parent
1c814830e5
commit
55b3ee832e
2 changed files with 68 additions and 5 deletions
|
|
@ -13,11 +13,30 @@ def _existing_playlists_by_title(client: Client) -> dict[str, Playlist]:
|
|||
return {p.title: p for p in playlists if p.title}
|
||||
|
||||
|
||||
def _existing_track_ids(client: Client, playlist: Playlist) -> set[str]:
|
||||
def _existing_track_ids(client: Client, playlist: Playlist, max_attempts: int = 4) -> set[str]:
|
||||
"""Возвращает set track_id уже находящихся в плейлисте треков.
|
||||
|
||||
client.users_playlists(kind) может вернуть None не только когда плейлиста не существует,
|
||||
но и при любом сбое разбора ответа (усечённое/повреждённое тело при 200 OK) - это тихий
|
||||
фолбэк библиотеки, а не исключение, поэтому with_retries его не перехватывает. Плейлист
|
||||
только что получен из users_playlists_list(), так что None здесь почти наверняка означает
|
||||
именно такой сбой, а не "плейлист пуст". Раньше это тихо трактовалось как пустой плейлист,
|
||||
из-за чего apply_classification вставлял все треки заново поверх уже существующих (дубли).
|
||||
Теперь повторяем запрос и, если так и не получили данные, падаем явно вместо угадывания.
|
||||
"""
|
||||
for attempt in range(max_attempts):
|
||||
full = with_retries(lambda: client.users_playlists(playlist.kind))
|
||||
if full is None:
|
||||
return set()
|
||||
if full is not None:
|
||||
return {t.track_id for t in full.tracks}
|
||||
if attempt < max_attempts - 1:
|
||||
wait = 2.0 * (2**attempt)
|
||||
print(f" пустой ответ при чтении плейлиста «{playlist.title}», повтор через {wait:.0f}с...")
|
||||
time.sleep(wait)
|
||||
|
||||
raise RuntimeError(
|
||||
f"не удалось прочитать содержимое плейлиста «{playlist.title}» (kind={playlist.kind}) "
|
||||
"после нескольких попыток - прерываю, чтобы не вставить треки повторно поверх существующих"
|
||||
)
|
||||
|
||||
|
||||
def _get_or_create_playlist(client: Client, title: str, existing: dict[str, Playlist]) -> Playlist:
|
||||
|
|
|
|||
|
|
@ -135,3 +135,47 @@ def test_apply_continues_after_bad_request_on_single_track():
|
|||
apply_mod.apply_classification(client, rows, delay=0)
|
||||
|
||||
assert client.inserted == [(200, 2, 20, 1)], "второй трек должен вставиться, несмотря на ошибку первого"
|
||||
|
||||
|
||||
class NoneOnceClient(FakeClient):
|
||||
"""users_playlists возвращает None (сбой разбора ответа) на первой попытке, затем нормальные данные."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.calls = 0
|
||||
|
||||
def users_playlists(self, kind):
|
||||
self.calls += 1
|
||||
if self.calls == 1:
|
||||
return None
|
||||
return super().users_playlists(kind)
|
||||
|
||||
|
||||
def test_apply_retries_on_none_response_instead_of_treating_as_empty_playlist(monkeypatch):
|
||||
# Регрессия: client.users_playlists() тихо возвращает None при любом сбое разбора ответа
|
||||
# (не только когда плейлиста реально не существует). Раньше это трактовалось как "плейлист
|
||||
# пуст", и apply вставлял поверх уже существующих треков дубли.
|
||||
monkeypatch.setattr(apply_mod.time, "sleep", lambda s: None)
|
||||
client = NoneOnceClient()
|
||||
rows = [{"target_playlist": "Рок — RU", "id": 1, "album_id": 10}]
|
||||
|
||||
apply_mod.apply_classification(client, rows, delay=0)
|
||||
|
||||
assert client.inserted == [], "трек уже был в плейлисте, дубль вставляться не должен"
|
||||
|
||||
|
||||
class AlwaysNoneClient(FakeClient):
|
||||
def users_playlists(self, kind):
|
||||
return None
|
||||
|
||||
|
||||
def test_apply_raises_instead_of_silently_treating_persistent_none_as_empty(monkeypatch):
|
||||
monkeypatch.setattr(apply_mod.time, "sleep", lambda s: None)
|
||||
client = AlwaysNoneClient()
|
||||
rows = [{"target_playlist": "Рок — RU", "id": 1, "album_id": 10}]
|
||||
|
||||
try:
|
||||
apply_mod.apply_classification(client, rows, delay=0)
|
||||
assert False, "должно было упасть, а не молча решить, что плейлист пуст"
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue