Sort_YM/sort_ym/fetch.py
Andrew Pimenov b93ac0d04a 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, сопоставляя по позиции в батче.
2026-07-06 12:16:14 +03:00

90 lines
3.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 __future__ import annotations
import json
import time
from pathlib import Path
from yandex_music import Client, Track
from .ymclient import chunked, with_retries
TRACKS_CACHE_FILE = "tracks.json"
def _track_genre(track: Track) -> str | None:
for album in track.albums:
if album.genre:
return album.genre
return None
def _serialize(track: Track) -> dict:
album_id = track.albums[0].id if track.albums else None
return {
"id": track.id,
"album_id": album_id,
"title": track.title or "",
"artists": [a.name for a in track.artists if a.name],
"genre_raw": _track_genre(track),
"lyrics_available": bool(track.lyrics_available),
}
def _atomic_write_json(path: Path, data: dict) -> None:
tmp = path.with_suffix(".tmp")
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
tmp.replace(path)
def load_tracks_cache(cache_dir: Path) -> dict[str, dict]:
cache_file = cache_dir / TRACKS_CACHE_FILE
if cache_file.exists():
return json.loads(cache_file.read_text(encoding="utf-8"))
return {}
def fetch_liked_tracks(
client: Client,
cache_dir: Path,
batch_size: int,
batch_delay: float,
) -> dict[str, dict]:
cache_dir.mkdir(parents=True, exist_ok=True)
cache_file = cache_dir / TRACKS_CACHE_FILE
cache = load_tracks_cache(cache_dir)
likes = with_retries(lambda: client.users_likes_tracks())
if likes is None:
print("Не удалось получить список лайкнутых треков.")
return cache
all_ids = likes.tracks_ids
missing_ids = [tid for tid in all_ids if tid not in cache]
print(f"Лайкнутых треков: {len(all_ids)}, новых для загрузки: {len(missing_ids)}")
for batch in chunked(missing_ids, batch_size):
tracks = with_retries(lambda: client.tracks(batch))
if len(tracks) != len(batch):
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)
print(f" загружено {len(cache)}/{len(all_ids)}")
time.sleep(batch_delay)
stale = set(cache) - set(all_ids)
for tid in stale:
del cache[tid]
if stale:
_atomic_write_json(cache_file, cache)
print(f" убрано из кэша (больше не лайкнуто): {len(stale)}")
return cache