mirror of
https://github.com/Horizontal42/Sort_YM.git
synced 2026-11-10 15:42:02 +03:00
Кэш ключевался по track.track_id, пересчитанному из albums[0] вернувшегося Track - а не по исходному id из списка лайков. Для трека, доступного на нескольких альбомах, "основной" альбом в ответе tracks() может отличаться от того, под которым трек лайкнут. Из-за этого 138 треков из ~1984 кэшировались под несовпадающим ключом и тут же вычищались как "больше не лайкнутые" - при том что реально лайкнуты. Теперь кэшируем по исходному id из likes.tracks_ids, сопоставляя по позиции в батче.
90 lines
3.4 KiB
Python
90 lines
3.4 KiB
Python
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
|