diff options
| author | Pinapelz <yukais@pinapelz.com> | 2026-02-18 01:53:26 -0800 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2026-02-18 01:53:26 -0800 |
| commit | 582a8dca61281f6b4c7885c3f0a24d762a19c1d6 (patch) | |
| tree | 7a191cc88b55daeb2f61977181d0df62a010c015 | |
| parent | 6a53a3429749d1f1d96724666de8857135066f77 (diff) | |
add caching optimization for playlist url fetch for faster startup
| -rw-r--r-- | yt_radio.py | 76 |
1 files changed, 73 insertions, 3 deletions
diff --git a/yt_radio.py b/yt_radio.py index ad208a9..aa6b896 100644 --- a/yt_radio.py +++ b/yt_radio.py @@ -18,10 +18,38 @@ app = Flask(__name__) BASE_URL = os.environ.get("BASE_URL", "http://localhost:8000") PLAYLIST_URL = os.environ.get("PLAYLIST_URL") RANDOMIZE_PLAYLIST = os.environ.get("RANDOMIZE_PLAYLIST", "False").lower() in ("1", "true", "yes") +YTDLP_CACHE_FILE = os.environ.get("YTDLP_CACHE_FILE", "yt_dlp_cache.json") if not PLAYLIST_URL: raise RuntimeError("Please set PLAYLIST_URL environment variable") METADATA = {} +# Simple thread-safe cache for yt-dlp JSON outputs +_CACHE_LOCK = threading.Lock() +try: + if os.path.exists(YTDLP_CACHE_FILE): + with open(YTDLP_CACHE_FILE, "r", encoding="utf-8") as f: + _CACHE = json.load(f) + else: + _CACHE = {} +except Exception: + logger.exception("Failed to load cache file, starting with empty cache") + _CACHE = {} + +def _save_cache(): + # atomic write + tmp = f"{YTDLP_CACHE_FILE}.tmp" + try: + with open(tmp, "w", encoding="utf-8") as f: + json.dump(_CACHE, f, ensure_ascii=False, indent=2) + os.replace(tmp, YTDLP_CACHE_FILE) + except Exception: + logger.exception("Failed to save cache to %s", YTDLP_CACHE_FILE) + try: + if os.path.exists(tmp): + os.remove(tmp) + except Exception: + pass + def convert_playlist_to_links(link: str): ydl_opts = { "quiet": True, @@ -30,28 +58,70 @@ def convert_playlist_to_links(link: str): urls = [] with YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(link, download=False) - for entry in info["entries"]: - urls.append(f"https://www.youtube.com/watch?v={entry['id']}") + entries = info.get("entries") if isinstance(info, dict) else None + if not entries: + logger.warning("No entries found in playlist info for %s", link) + return urls + for entry in entries: + entry_id = None + if isinstance(entry, dict): + entry_id = entry.get("id") or entry.get("url") + elif isinstance(entry, str): + entry_id = entry + if not entry_id: + continue + if entry_id.startswith("http"): + urls.append(entry_id) + else: + urls.append(f"https://www.youtube.com/watch?v={entry_id}") if RANDOMIZE_PLAYLIST: random.shuffle(urls) return urls def fetch_metadata(index, url): + cached = None + with _CACHE_LOCK: + cached = _CACHE.get(url) + if cached: + try: + # cached may be the full yt-dlp JSON dict + data = cached + METADATA[index] = { + "title": data.get("title", f"Track {index+1}"), + "artist": data.get("uploader", "Unknown"), + "duration": data.get("duration", -1) + } + logger.debug("Loaded metadata from cache for index %s: %s", index, METADATA[index]) + return + except Exception: + logger.exception("Failed to use cached metadata for %s, will refetch", url) + + # Not cached or failed to use cache: call yt-dlp to dump json try: result = subprocess.run( ["yt-dlp", "--dump-json", url], capture_output=True, text=True, - timeout=20 + timeout=30 ) + if result.returncode != 0 or not result.stdout: + raise RuntimeError(f"yt-dlp failed for {url}: {result.stderr.strip()}") data = json.loads(result.stdout) METADATA[index] = { "title": data.get("title", f"Track {index+1}"), "artist": data.get("uploader", "Unknown"), "duration": data.get("duration", -1) } + # store full json in cache + with _CACHE_LOCK: + _CACHE[url] = data + try: + _save_cache() + except Exception: + logger.exception("Failed to persist cache after fetching %s", url) logger.debug("Fetched metadata for index %s: %s", index, METADATA[index]) except Exception: + # Fallback defaults METADATA[index] = { "title": f"Track {index+1}", "artist": "Unknown", |
