mirror of
https://github.com/Horizontal42/Sort_YM.git
synced 2026-11-10 15:42:02 +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
|
fetch_batch_delay = 0.4
|
||||||
lyrics_request_delay = 0.25
|
lyrics_request_delay = 0.25
|
||||||
apply_request_delay = 0.3
|
apply_request_delay = 0.3
|
||||||
|
# Таймаут одного запроса, секунды (библиотека по умолчанию ставит 5 - мало при медленной сети)
|
||||||
|
request_timeout = 20
|
||||||
|
|
||||||
[fetch]
|
[fetch]
|
||||||
# Сколько id треков в одном батч-запросе client.tracks(...)
|
# Сколько id треков в одном батч-запросе client.tracks(...)
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,16 @@ import time
|
||||||
|
|
||||||
from yandex_music import Client, Playlist
|
from yandex_music import Client, Playlist
|
||||||
|
|
||||||
|
from .ymclient import with_retries
|
||||||
|
|
||||||
|
|
||||||
def _existing_playlists_by_title(client: Client) -> dict[str, Playlist]:
|
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}
|
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) -> set[str]:
|
||||||
full = client.users_playlists(playlist.kind)
|
full = with_retries(lambda: client.users_playlists(playlist.kind))
|
||||||
if full is None:
|
if full is None:
|
||||||
return set()
|
return set()
|
||||||
return {t.track_id for t in full.tracks}
|
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:
|
def _get_or_create_playlist(client: Client, title: str, existing: dict[str, Playlist]) -> Playlist:
|
||||||
if title in existing:
|
if title in existing:
|
||||||
return existing[title]
|
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
|
existing[title] = playlist
|
||||||
return playlist
|
return playlist
|
||||||
|
|
||||||
|
|
@ -51,11 +53,13 @@ def apply_classification(
|
||||||
if track_key in already:
|
if track_key in already:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
updated = client.users_playlists_insert_track(
|
updated = with_retries(
|
||||||
playlist.kind,
|
lambda: client.users_playlists_insert_track(
|
||||||
row["id"],
|
playlist.kind,
|
||||||
row["album_id"],
|
row["id"],
|
||||||
revision=revision,
|
row["album_id"],
|
||||||
|
revision=revision,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if updated is not None and updated.revision is not None:
|
if updated is not None and updated.revision is not None:
|
||||||
revision = updated.revision
|
revision = updated.revision
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,13 @@ def cmd_auth(args: argparse.Namespace) -> None:
|
||||||
|
|
||||||
def cmd_fetch(args: argparse.Namespace) -> None:
|
def cmd_fetch(args: argparse.Namespace) -> None:
|
||||||
cfg = load_config()
|
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)
|
fetch.fetch_liked_tracks(client, cfg.cache_dir, cfg.batch_size, cfg.fetch_batch_delay)
|
||||||
|
|
||||||
|
|
||||||
def cmd_report(args: argparse.Namespace) -> None:
|
def cmd_report(args: argparse.Namespace) -> None:
|
||||||
cfg = load_config()
|
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)
|
tracks_cache = fetch.load_tracks_cache(cfg.cache_dir)
|
||||||
if not tracks_cache:
|
if not tracks_cache:
|
||||||
|
|
@ -53,7 +53,7 @@ def cmd_apply(args: argparse.Namespace) -> None:
|
||||||
raise SystemExit("Это изменит ваш аккаунт. Добавьте флаг --yes для подтверждения.")
|
raise SystemExit("Это изменит ваш аккаунт. Добавьте флаг --yes для подтверждения.")
|
||||||
|
|
||||||
cfg = load_config()
|
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)
|
tracks_cache = fetch.load_tracks_cache(cfg.cache_dir)
|
||||||
if not tracks_cache:
|
if not tracks_cache:
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ class Config:
|
||||||
fetch_batch_delay: float
|
fetch_batch_delay: float
|
||||||
lyrics_request_delay: float
|
lyrics_request_delay: float
|
||||||
apply_request_delay: float
|
apply_request_delay: float
|
||||||
|
request_timeout: float
|
||||||
batch_size: int
|
batch_size: int
|
||||||
token_file: Path
|
token_file: Path
|
||||||
cache_dir: Path
|
cache_dir: Path
|
||||||
|
|
@ -27,6 +28,7 @@ def load_config(path: Path | None = None) -> Config:
|
||||||
fetch_batch_delay=data["throttle"]["fetch_batch_delay"],
|
fetch_batch_delay=data["throttle"]["fetch_batch_delay"],
|
||||||
lyrics_request_delay=data["throttle"]["lyrics_request_delay"],
|
lyrics_request_delay=data["throttle"]["lyrics_request_delay"],
|
||||||
apply_request_delay=data["throttle"]["apply_request_delay"],
|
apply_request_delay=data["throttle"]["apply_request_delay"],
|
||||||
|
request_timeout=data["throttle"]["request_timeout"],
|
||||||
batch_size=data["fetch"]["batch_size"],
|
batch_size=data["fetch"]["batch_size"],
|
||||||
token_file=ROOT / data["paths"]["token_file"],
|
token_file=ROOT / data["paths"]["token_file"],
|
||||||
cache_dir=ROOT / data["paths"]["cache_dir"],
|
cache_dir=ROOT / data["paths"]["cache_dir"],
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from pathlib import Path
|
||||||
|
|
||||||
from yandex_music import Client, Track
|
from yandex_music import Client, Track
|
||||||
|
|
||||||
from .ymclient import chunked
|
from .ymclient import chunked, with_retries
|
||||||
|
|
||||||
TRACKS_CACHE_FILE = "tracks.json"
|
TRACKS_CACHE_FILE = "tracks.json"
|
||||||
|
|
||||||
|
|
@ -53,7 +53,7 @@ def fetch_liked_tracks(
|
||||||
cache_file = cache_dir / TRACKS_CACHE_FILE
|
cache_file = cache_dir / TRACKS_CACHE_FILE
|
||||||
cache = load_tracks_cache(cache_dir)
|
cache = load_tracks_cache(cache_dir)
|
||||||
|
|
||||||
likes = client.users_likes_tracks()
|
likes = with_retries(lambda: client.users_likes_tracks())
|
||||||
if likes is None:
|
if likes is None:
|
||||||
print("Не удалось получить список лайкнутых треков.")
|
print("Не удалось получить список лайкнутых треков.")
|
||||||
return cache
|
return cache
|
||||||
|
|
@ -64,7 +64,7 @@ def fetch_liked_tracks(
|
||||||
print(f"Лайкнутых треков: {len(all_ids)}, новых для загрузки: {len(missing_ids)}")
|
print(f"Лайкнутых треков: {len(all_ids)}, новых для загрузки: {len(missing_ids)}")
|
||||||
|
|
||||||
for batch in chunked(missing_ids, batch_size):
|
for batch in chunked(missing_ids, batch_size):
|
||||||
tracks = client.tracks(batch)
|
tracks = with_retries(lambda: client.tracks(batch))
|
||||||
for track in tracks:
|
for track in tracks:
|
||||||
cache[track.track_id] = _serialize(track)
|
cache[track.track_id] = _serialize(track)
|
||||||
_atomic_write_json(cache_file, cache)
|
_atomic_write_json(cache_file, cache)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ from pathlib import Path
|
||||||
|
|
||||||
from yandex_music import Client, Genre
|
from yandex_music import Client, Genre
|
||||||
|
|
||||||
|
from .ymclient import with_retries
|
||||||
|
|
||||||
GENRE_CATALOG_FILE = "genre_catalog.json"
|
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():
|
if cache_file.exists():
|
||||||
return json.loads(cache_file.read_text(encoding="utf-8"))
|
return json.loads(cache_file.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
roots = client.genres()
|
roots = with_retries(lambda: client.genres())
|
||||||
flat = flatten_catalog(roots)
|
flat = flatten_catalog(roots)
|
||||||
cache_file.write_text(json.dumps(flat, ensure_ascii=False, indent=2), encoding="utf-8")
|
cache_file.write_text(json.dumps(flat, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
return flat
|
return flat
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@ import unicodedata
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from yandex_music import Client
|
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"
|
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:
|
def _fetch_one_language(client: Client, track_id: str) -> str | None:
|
||||||
try:
|
try:
|
||||||
supplement = client.track_supplement(track_id)
|
supplement = with_retries(lambda: client.track_supplement(track_id))
|
||||||
except YandexMusicError:
|
except NotFoundError:
|
||||||
return None
|
return None
|
||||||
if supplement is None or supplement.lyrics is None:
|
if supplement is None or supplement.lyrics is None:
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,44 @@
|
||||||
from __future__ import annotations
|
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 import Client
|
||||||
|
from yandex_music.exceptions import NetworkError, NotFoundError
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
def make_client(token: str) -> Client:
|
def make_client(token: str, request_timeout: float = 20) -> Client:
|
||||||
client = Client(token=token)
|
client = Client(token=token)
|
||||||
client.init()
|
client.request.set_timeout(request_timeout)
|
||||||
|
with_retries(lambda: client.init())
|
||||||
return client
|
return client
|
||||||
|
|
||||||
|
|
||||||
def chunked(items: List[T], size: int) -> Iterator[List[T]]:
|
def chunked(items: List[T], size: int) -> Iterator[List[T]]:
|
||||||
for i in range(0, len(items), size):
|
for i in range(0, len(items), size):
|
||||||
yield items[i : i + 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