fix fetch caching tracks under wrong key, causing false stale purge

Кэш ключевался по track.track_id, пересчитанному из albums[0] вернувшегося
Track - а не по исходному id из списка лайков. Для трека, доступного на
нескольких альбомах, "основной" альбом в ответе tracks() может отличаться
от того, под которым трек лайкнут. Из-за этого 138 треков из ~1984
кэшировались под несовпадающим ключом и тут же вычищались как "больше не
лайкнутые" - при том что реально лайкнуты. Теперь кэшируем по исходному
id из likes.tracks_ids, сопоставляя по позиции в батче.
This commit is contained in:
Andrew Pimenov 2026-07-06 12:16:14 +03:00
parent a8dd8d3bc8
commit b93ac0d04a
2 changed files with 85 additions and 2 deletions

View file

@ -65,8 +65,17 @@ def fetch_liked_tracks(
for batch in chunked(missing_ids, batch_size): for batch in chunked(missing_ids, batch_size):
tracks = with_retries(lambda: client.tracks(batch)) tracks = with_retries(lambda: client.tracks(batch))
for track in tracks: if len(tracks) != len(batch):
cache[track.track_id] = _serialize(track) print(
f" предупреждение: запрошено {len(batch)} треков, получено {len(tracks)} - "
"часть могла быть удалена из каталога Яндекса, попробуем ещё раз при следующем fetch"
)
# Кэшируем по исходному id из списка лайков (likes.tracks_ids), а не по track.track_id,
# пересчитанному из albums[0] вернувшегося Track - для трека, доступного на нескольких
# альбомах, "основной" альбом в ответе может отличаться от того, под которым трек лайкнут.
# Иначе такой трек тут же попадает под stale-очистку ниже как "больше не лайкнутый".
for original_id, track in zip(batch, tracks):
cache[original_id] = _serialize(track)
_atomic_write_json(cache_file, cache) _atomic_write_json(cache_file, cache)
print(f" загружено {len(cache)}/{len(all_ids)}") print(f" загружено {len(cache)}/{len(all_ids)}")
time.sleep(batch_delay) time.sleep(batch_delay)

74
tests/test_fetch.py Normal file
View file

@ -0,0 +1,74 @@
from pathlib import Path
from sort_ym import fetch
class FakeArtist:
def __init__(self, name):
self.name = name
class FakeAlbum:
def __init__(self, id_, genre=None):
self.id = id_
self.genre = genre
class FakeTrack:
def __init__(self, id_, albums, title="Title", artists=None, lyrics_available=False):
self.id = id_
self.albums = albums
self.title = title
self.artists = artists or [FakeArtist("Artist")]
self.lyrics_available = lyrics_available
class FakeLikes:
def __init__(self, tracks_ids):
self.tracks_ids = tracks_ids
class FakeClient:
"""Симулирует трек, у которого "основной" альбом в ответе tracks()
отличается от альбома, под которым трек значится в списке лайков -
это реальное поведение API Яндекса для треков на нескольких альбомах."""
def __init__(self, likes_ids, tracks_by_batch):
self._likes_ids = likes_ids
self._tracks_by_batch = tracks_by_batch
def users_likes_tracks(self):
return FakeLikes(self._likes_ids)
def tracks(self, batch):
return self._tracks_by_batch[tuple(batch)]
def test_fetch_caches_under_original_liked_id_not_recomputed_track_id(tmp_path: Path):
# Трек лайкнут как "100:10", но Track.albums[0].id из ответа API - 999 (другой альбом).
track = FakeTrack(id_=100, albums=[FakeAlbum(999, genre="pop")])
client = FakeClient(
likes_ids=["100:10"],
tracks_by_batch={("100:10",): [track]},
)
cache = fetch.fetch_liked_tracks(client, tmp_path, batch_size=100, batch_delay=0)
assert "100:10" in cache, "трек должен кэшироваться под id из списка лайков"
assert "100:999" not in cache, "не должен пересчитывать ключ из albums[0] вернувшегося Track"
assert cache["100:10"]["id"] == 100
assert cache["100:10"]["genre_raw"] == "pop"
def test_fetch_does_not_purge_track_with_mismatched_album_as_stale(tmp_path: Path):
# Регрессия: раньше такой трек кэшировался под "неправильным" ключом track.track_id,
# который не совпадал с likes.tracks_ids, и тут же удалялся как "больше не лайкнутый".
track = FakeTrack(id_=100, albums=[FakeAlbum(999)])
client = FakeClient(
likes_ids=["100:10"],
tracks_by_batch={("100:10",): [track]},
)
cache = fetch.fetch_liked_tracks(client, tmp_path, batch_size=100, batch_delay=0)
assert len(cache) == 1, f"трек не должен быть вычищен как stale сразу после загрузки: {cache}"