mirror of
https://github.com/Horizontal42/Sort_YM.git
synced 2026-11-11 02:12:00 +03:00
retry network errors and raise request timeout
Медленная/нестабильная сеть до api.music.yandex.net роняла весь процесс при первом же таймауте (5с по умолчанию у библиотеки). Добавлен with_retries с экспоненциальной паузой на всех сетевых точках входа (fetch, language, genres, apply, client.init), таймаут запроса поднят до 20с (настраивается в config.toml). Заодно поправлен скрытый баг: language.py раньше кэшировал таймаут как "текста нет" навсегда - теперь только настоящий NotFoundError трактуется так.
This commit is contained in:
parent
353be65e3a
commit
942c2d19e4
8 changed files with 59 additions and 21 deletions
|
|
@ -3,6 +3,8 @@
|
|||
fetch_batch_delay = 0.4
|
||||
lyrics_request_delay = 0.25
|
||||
apply_request_delay = 0.3
|
||||
# Таймаут одного запроса, секунды (библиотека по умолчанию ставит 5 - мало при медленной сети)
|
||||
request_timeout = 20
|
||||
|
||||
[fetch]
|
||||
# Сколько id треков в одном батч-запросе client.tracks(...)
|
||||
|
|
|
|||
|
|
@ -4,14 +4,16 @@ import time
|
|||
|
||||
from yandex_music import Client, Playlist
|
||||
|
||||
from .ymclient import with_retries
|
||||
|
||||
|
||||
def _existing_playlists_by_title(client: Client) -> dict[str, Playlist]:
|
||||
playlists = client.users_playlists_list()
|
||||
playlists = with_retries(lambda: client.users_playlists_list())
|
||||
return {p.title: p for p in playlists if p.title}
|
||||
|
||||
|
||||
def _existing_track_ids(client: Client, playlist: Playlist) -> set[str]:
|
||||
full = client.users_playlists(playlist.kind)
|
||||
full = with_retries(lambda: client.users_playlists(playlist.kind))
|
||||
if full is None:
|
||||
return set()
|
||||
return {t.track_id for t in full.tracks}
|
||||
|
|
@ -20,7 +22,7 @@ def _existing_track_ids(client: Client, playlist: Playlist) -> set[str]:
|
|||
def _get_or_create_playlist(client: Client, title: str, existing: dict[str, Playlist]) -> Playlist:
|
||||
if title in existing:
|
||||
return existing[title]
|
||||
playlist = client.users_playlists_create(title, visibility="private")
|
||||
playlist = with_retries(lambda: client.users_playlists_create(title, visibility="private"))
|
||||
existing[title] = playlist
|
||||
return playlist
|
||||
|
||||
|
|
@ -51,11 +53,13 @@ def apply_classification(
|
|||
if track_key in already:
|
||||
continue
|
||||
|
||||
updated = client.users_playlists_insert_track(
|
||||
playlist.kind,
|
||||
row["id"],
|
||||
row["album_id"],
|
||||
revision=revision,
|
||||
updated = with_retries(
|
||||
lambda: client.users_playlists_insert_track(
|
||||
playlist.kind,
|
||||
row["id"],
|
||||
row["album_id"],
|
||||
revision=revision,
|
||||
)
|
||||
)
|
||||
if updated is not None and updated.revision is not None:
|
||||
revision = updated.revision
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@ def cmd_auth(args: argparse.Namespace) -> None:
|
|||
|
||||
def cmd_fetch(args: argparse.Namespace) -> None:
|
||||
cfg = load_config()
|
||||
client = make_client(auth.get_token(cfg.token_file))
|
||||
client = make_client(auth.get_token(cfg.token_file), cfg.request_timeout)
|
||||
fetch.fetch_liked_tracks(client, cfg.cache_dir, cfg.batch_size, cfg.fetch_batch_delay)
|
||||
|
||||
|
||||
def cmd_report(args: argparse.Namespace) -> None:
|
||||
cfg = load_config()
|
||||
client = make_client(auth.get_token(cfg.token_file))
|
||||
client = make_client(auth.get_token(cfg.token_file), cfg.request_timeout)
|
||||
|
||||
tracks_cache = fetch.load_tracks_cache(cfg.cache_dir)
|
||||
if not tracks_cache:
|
||||
|
|
@ -53,7 +53,7 @@ def cmd_apply(args: argparse.Namespace) -> None:
|
|||
raise SystemExit("Это изменит ваш аккаунт. Добавьте флаг --yes для подтверждения.")
|
||||
|
||||
cfg = load_config()
|
||||
client = make_client(auth.get_token(cfg.token_file))
|
||||
client = make_client(auth.get_token(cfg.token_file), cfg.request_timeout)
|
||||
|
||||
tracks_cache = fetch.load_tracks_cache(cfg.cache_dir)
|
||||
if not tracks_cache:
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class Config:
|
|||
fetch_batch_delay: float
|
||||
lyrics_request_delay: float
|
||||
apply_request_delay: float
|
||||
request_timeout: float
|
||||
batch_size: int
|
||||
token_file: Path
|
||||
cache_dir: Path
|
||||
|
|
@ -27,6 +28,7 @@ def load_config(path: Path | None = None) -> Config:
|
|||
fetch_batch_delay=data["throttle"]["fetch_batch_delay"],
|
||||
lyrics_request_delay=data["throttle"]["lyrics_request_delay"],
|
||||
apply_request_delay=data["throttle"]["apply_request_delay"],
|
||||
request_timeout=data["throttle"]["request_timeout"],
|
||||
batch_size=data["fetch"]["batch_size"],
|
||||
token_file=ROOT / data["paths"]["token_file"],
|
||||
cache_dir=ROOT / data["paths"]["cache_dir"],
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from pathlib import Path
|
|||
|
||||
from yandex_music import Client, Track
|
||||
|
||||
from .ymclient import chunked
|
||||
from .ymclient import chunked, with_retries
|
||||
|
||||
TRACKS_CACHE_FILE = "tracks.json"
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ def fetch_liked_tracks(
|
|||
cache_file = cache_dir / TRACKS_CACHE_FILE
|
||||
cache = load_tracks_cache(cache_dir)
|
||||
|
||||
likes = client.users_likes_tracks()
|
||||
likes = with_retries(lambda: client.users_likes_tracks())
|
||||
if likes is None:
|
||||
print("Не удалось получить список лайкнутых треков.")
|
||||
return cache
|
||||
|
|
@ -64,7 +64,7 @@ def fetch_liked_tracks(
|
|||
print(f"Лайкнутых треков: {len(all_ids)}, новых для загрузки: {len(missing_ids)}")
|
||||
|
||||
for batch in chunked(missing_ids, batch_size):
|
||||
tracks = client.tracks(batch)
|
||||
tracks = with_retries(lambda: client.tracks(batch))
|
||||
for track in tracks:
|
||||
cache[track.track_id] = _serialize(track)
|
||||
_atomic_write_json(cache_file, cache)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ from pathlib import Path
|
|||
|
||||
from yandex_music import Client, Genre
|
||||
|
||||
from .ymclient import with_retries
|
||||
|
||||
GENRE_CATALOG_FILE = "genre_catalog.json"
|
||||
|
||||
# Корневые жанры каталога Яндекс.Музыки -> наши крупные корзины.
|
||||
|
|
@ -83,7 +85,7 @@ def load_or_fetch_catalog(client: Client, cache_dir: Path) -> dict[str, dict]:
|
|||
if cache_file.exists():
|
||||
return json.loads(cache_file.read_text(encoding="utf-8"))
|
||||
|
||||
roots = client.genres()
|
||||
roots = with_retries(lambda: client.genres())
|
||||
flat = flatten_catalog(roots)
|
||||
cache_file.write_text(json.dumps(flat, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
return flat
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ import unicodedata
|
|||
from pathlib import Path
|
||||
|
||||
from yandex_music import Client
|
||||
from yandex_music.exceptions import YandexMusicError
|
||||
from yandex_music.exceptions import NotFoundError
|
||||
|
||||
from .ymclient import with_retries
|
||||
|
||||
LYRICS_LANG_CACHE_FILE = "lyrics_lang.json"
|
||||
|
||||
|
|
@ -99,8 +101,8 @@ def load_lang_cache(cache_dir: Path) -> dict[str, str | None]:
|
|||
|
||||
def _fetch_one_language(client: Client, track_id: str) -> str | None:
|
||||
try:
|
||||
supplement = client.track_supplement(track_id)
|
||||
except YandexMusicError:
|
||||
supplement = with_retries(lambda: client.track_supplement(track_id))
|
||||
except NotFoundError:
|
||||
return None
|
||||
if supplement is None or supplement.lyrics is None:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -1,18 +1,44 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Iterator, List, TypeVar
|
||||
import time
|
||||
from typing import Callable, Iterator, List, TypeVar
|
||||
|
||||
from yandex_music import Client
|
||||
from yandex_music.exceptions import NetworkError, NotFoundError
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def make_client(token: str) -> Client:
|
||||
def make_client(token: str, request_timeout: float = 20) -> Client:
|
||||
client = Client(token=token)
|
||||
client.init()
|
||||
client.request.set_timeout(request_timeout)
|
||||
with_retries(lambda: client.init())
|
||||
return client
|
||||
|
||||
|
||||
def chunked(items: List[T], size: int) -> Iterator[List[T]]:
|
||||
for i in range(0, len(items), size):
|
||||
yield items[i : i + size]
|
||||
|
||||
|
||||
def with_retries(fn: Callable[[], T], max_attempts: int = 4, base_delay: float = 2.0) -> T:
|
||||
"""Повторяет вызов при временных сетевых сбоях (таймауты и т.п.) с экспоненциальной паузой.
|
||||
|
||||
NotFoundError не повторяется - это окончательный ответ API, а не сбой сети,
|
||||
хотя формально он тоже унаследован от NetworkError.
|
||||
"""
|
||||
last_exc: NetworkError | None = None
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
return fn()
|
||||
except NotFoundError:
|
||||
raise
|
||||
except NetworkError as e:
|
||||
last_exc = e
|
||||
if attempt < max_attempts - 1:
|
||||
wait = base_delay * (2**attempt)
|
||||
print(f" сетевая ошибка ({e}), повтор через {wait:.0f}с...")
|
||||
time.sleep(wait)
|
||||
|
||||
assert last_exc is not None
|
||||
raise last_exc
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue