Sort_YM/sort_ym/apply.py
Andrew Pimenov f69d961064 fix duplicate track/playlist inserts on network timeout
users_playlists_insert_track и users_playlists_create - операции записи:
таймаут не значит, что запрос не дошёл до сервера. Прошлый фикс слепо
повторял такой вызов при NetworkError, и если сервер уже применил
вставку (а ответ просто потерялся), трек добавлялся в плейлист второй
раз. Теперь при сетевой ошибке сначала перепроверяется фактическое
состояние плейлиста/списка плейлистов, и повтор происходит только если
трек/плейлист там правда ещё отсутствует.
2026-07-06 12:04:18 +03:00

112 lines
4.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 time
from yandex_music import Client, Playlist
from yandex_music.exceptions import NetworkError
from .ymclient import with_retries
def _existing_playlists_by_title(client: Client) -> dict[str, Playlist]:
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 = with_retries(lambda: client.users_playlists(playlist.kind))
if full is None:
return set()
return {t.track_id for t in full.tracks}
def _get_or_create_playlist(client: Client, title: str, existing: dict[str, Playlist]) -> Playlist:
if title in existing:
return existing[title]
try:
playlist = with_retries(lambda: client.users_playlists_create(title, visibility="private"), max_attempts=1)
except NetworkError:
# Таймаут не значит, что запрос не дошёл до сервера - плейлист мог всё же
# создаться. Перепроверяем по имени вместо слепого повтора (иначе - дубль плейлиста).
fresh = _existing_playlists_by_title(client)
if title in fresh:
existing[title] = fresh[title]
return fresh[title]
raise
existing[title] = playlist
return playlist
def _insert_track_verified(
client: Client,
playlist: Playlist,
row: dict,
track_key: str,
revision: int,
max_attempts: int = 4,
) -> int:
"""Добавляет трек в плейлист, возвращает новую revision.
users_playlists_insert_track - операция записи, а не чтения: при таймауте
непонятно, применилась ли вставка на сервере. Слепой повтор такого вызова
рискует добавить трек второй раз. Поэтому при сетевой ошибке перепроверяем
реальное содержимое плейлиста и повторяем вставку, только если трека там
действительно ещё нет.
"""
for attempt in range(max_attempts):
try:
updated = with_retries(
lambda: client.users_playlists_insert_track(
playlist.kind, row["id"], row["album_id"], revision=revision
),
max_attempts=1,
)
return updated.revision if updated is not None and updated.revision is not None else revision
except NetworkError as e:
last_attempt = attempt == max_attempts - 1
print(f" сетевая ошибка при вставке трека ({e}), проверяю фактическое состояние плейлиста...")
fresh = with_retries(lambda: client.users_playlists(playlist.kind))
if fresh is not None:
revision = fresh.revision or revision
if track_key in {t.track_id for t in fresh.tracks}:
return revision
if last_attempt:
raise
time.sleep(2.0 * (2**attempt))
raise RuntimeError("unreachable")
def apply_classification(
client: Client,
rows: list[dict],
delay: float,
limit: int | None = None,
) -> None:
if limit is not None:
rows = rows[:limit]
existing_playlists = _existing_playlists_by_title(client)
by_playlist: dict[str, list[dict]] = {}
for row in rows:
by_playlist.setdefault(row["target_playlist"], []).append(row)
for title, playlist_rows in by_playlist.items():
playlist = _get_or_create_playlist(client, title, existing_playlists)
already = _existing_track_ids(client, playlist)
revision = playlist.revision or 1
added = 0
for row in playlist_rows:
track_key = f"{row['id']}:{row['album_id']}" if row["album_id"] else str(row["id"])
if track_key in already:
continue
revision = _insert_track_verified(client, playlist, row, track_key, revision)
already.add(track_key)
added += 1
time.sleep(delay)
skipped = len(playlist_rows) - added
print(f"{title}: добавлено {added}, уже было {skipped}")