import os from typing import Callable import yt_dlp class YtDlpClient: """Real YtClient backed by yt_dlp. Not unit-tested offline; see test_youtube_live.py.""" def search_album(self, artist: str, album: str) -> list[dict]: query = f"ytsearch5:{artist} {album} full album" opts = { "quiet": True, "no_warnings": True, "extract_flat": "in_playlist", "skip_download": True, } with yt_dlp.YoutubeDL(opts) as ydl: info = ydl.extract_info(query, download=False) results: list[dict] = [] for e in (info.get("entries") or []): if not e: continue results.append( { "source_ref": e.get("url") or e.get("webpage_url") or e.get("id"), "title": e.get("title") or "", "artist": e.get("uploader") or e.get("channel") or "", "track_count": e.get("playlist_count") or 1, } ) return results def download(self, source_ref: str, dest: str, on_progress: Callable[[float], None]) -> dict: os.makedirs(dest, exist_ok=True) def hook(d: dict) -> None: if d.get("status") == "downloading": total = d.get("total_bytes") or d.get("total_bytes_estimate") if total: on_progress(min(1.0, d.get("downloaded_bytes", 0) / total)) elif d.get("status") == "finished": on_progress(1.0) opts = { "quiet": True, "no_warnings": True, "format": "bestaudio/best", "outtmpl": os.path.join(dest, "%(playlist_index)s-%(title)s.%(ext)s"), "progress_hooks": [hook], "postprocessors": [{"key": "FFmpegExtractAudio", "preferredcodec": "opus"}], } with yt_dlp.YoutubeDL(opts) as ydl: info = ydl.extract_info(source_ref, download=True) entries = info.get("entries") if info.get("entries") is not None else [info] return {"track_count": len([e for e in entries if e]), "path": dest}