import os import threading import time from flask import Blueprint, jsonify from yandex_music import Client from yandex_music.exceptions import YandexMusicError ym_bp = Blueprint("ym", __name__) YM_TOKEN = os.environ.get("YM_TOKEN", "").strip() HISTORY_TTL = 30 HISTORY_LIMIT = 30 _history_lock = threading.Lock() _history_cache: dict = {"at": 0.0, "payload": None, "error": None} _ym_client = None _ym_client_lock = threading.Lock() def _get_ym_client(): global _ym_client if _ym_client is not None: return _ym_client with _ym_client_lock: if _ym_client is None: _ym_client = Client(YM_TOKEN).init() return _ym_client def _track_url(track_id, album_id): if album_id: return f"https://music.yandex.ru/album/{album_id}/track/{track_id}" return f"https://music.yandex.ru/track/{track_id}" def _serialize_track(track, day, context_title, track_id=None, album_id=None): cover = None try: cover = track.get_cover_url("400x400") except (YandexMusicError, AttributeError, TypeError): cover = None return { "id": str(track_id or track.id), "album_id": str(album_id) if album_id else None, "title": track.title or "Без названия", "artists": track.artists_name() if hasattr(track, "artists_name") else [], "cover": cover, "duration_ms": track.duration_ms, "day": day, "context": context_title, "url": _track_url(track_id or track.id, album_id), } def _context_title(ctx): if ctx is None: return None for attr in ("album", "playlist", "artist", "wave"): obj = getattr(ctx, attr, None) title = getattr(obj, "title", None) or getattr(obj, "name", None) if title: return title return None def _fetch_history(): client = _get_ym_client() history = client.music_history(full_models_count=HISTORY_LIMIT) entries = [] pending_ids = [] for tab in ((history.history_tabs if history else None) or []): day = tab.date for group in tab.items or []: ctx = group.context.data.full_model if group.context and group.context.data else None context_title = _context_title(ctx) for item in group.tracks or []: if item.type != "track" or item.data is None: continue item_id = item.data.item_id track_id = item_id.track_id if item_id else None album_id = item_id.album_id if item_id else None track = item.data.full_model if track is not None: entries.append(_serialize_track(track, day, context_title, track_id, album_id)) elif track_id: pending_ids.append((str(track_id), str(album_id) if album_id else None, day, context_title)) if pending_ids: ids = [f"{t}:{a}" if a else t for t, a, _, _ in pending_ids[:HISTORY_LIMIT]] try: fetched = client.tracks(ids) or [] except YandexMusicError: fetched = [] by_id = {} for tr in fetched: if tr is None: continue if isinstance(tr, list): tr = next((x for x in tr if x is not None), None) if tr is None: continue by_id[str(tr.id)] = tr for track_id, album_id, day, context_title in pending_ids: tr = by_id.get(track_id) if tr is not None: entries.append(_serialize_track(tr, day, context_title, track_id, album_id)) entries = entries[:HISTORY_LIMIT] return { "ok": True, "updated_at": int(time.time()), "count": len(entries), "tracks": entries, } @ym_bp.route("/api/ym/history") def ym_history(): if not YM_TOKEN: return jsonify(ok=False, error="YM_TOKEN is not set"), 503 now = time.monotonic() with _history_lock: if _history_cache["payload"] is not None and now - _history_cache["at"] < HISTORY_TTL: return jsonify(_history_cache["payload"]) try: payload = _fetch_history() except (YandexMusicError, OSError, ValueError, KeyError, AttributeError) as exc: if _history_cache["payload"] is not None: stale = dict(_history_cache["payload"]) stale["stale"] = True return jsonify(stale) _history_cache["error"] = str(exc) return jsonify(ok=False, error=str(exc)), 502 _history_cache["at"] = now _history_cache["payload"] = payload return jsonify(payload)