"""Cover management for GUI - Handles cover loading, caching and display.""" from contextlib import suppress from PySide6 import QtGui from tidal_dl_ng.cache import CoverPixmapCache from tidal_dl_ng.download import Download from tidal_dl_ng.helper.path import resource_path from tidal_dl_ng.logger import logger_gui from tidal_dl_ng.worker import Worker class CoverManager: """Manages cover art loading, caching and display operations.""" def __init__(self, parent_window, threadpool, info_tab_widget): """Initialize the cover manager. Args: parent_window: Main window instance threadpool: QThreadPool for async operations info_tab_widget: InfoTabWidget instance for display """ self.parent = parent_window self.threadpool = threadpool self.info_tab = info_tab_widget self.cache = CoverPixmapCache() self.cover_url_current = "" def load_cover(self, media, use_cache_check: bool = True): """Load and display cover for media item. Args: media: Media object (Track, Album, etc.) use_cache_check: If True, check cache before loading """ if use_cache_check: # Try cache first for instant display cover_url = self._get_cover_url(media) if cover_url: if cover_url == self.cover_url_current: return # Already displayed cached_pixmap = self.cache.get(cover_url) if cached_pixmap: self._display_cover(cached_pixmap, cover_url) return # Load asynchronously worker = Worker(self._load_cover_async, media) self.threadpool.start(worker) def _load_cover_async(self, media): """Load cover in background thread.""" # Emit spinner on tab widget instead of InfoTabWidget (which is QObject, not QWidget) tab_widget = self.info_tab.tab_widget if hasattr(self.info_tab, "tab_widget") else None if tab_widget: self.parent.s_spinner_start.emit(tab_widget) try: cover_url = self._get_cover_url(media) if cover_url and self.cover_url_current != cover_url: # Check cache again (thread-safe) cached_pixmap = self.cache.get(cover_url) if cached_pixmap: self._display_cover(cached_pixmap, cover_url) else: # Download and cache data_cover = Download.cover_data(cover_url) pixmap = QtGui.QPixmap() pixmap.loadFromData(data_cover) self.cache.set(cover_url, pixmap) self._display_cover(pixmap, cover_url) elif not cover_url: self._display_default_cover() except Exception as e: logger_gui.warning(f"Failed to load cover: {e}") self._display_default_cover() finally: self.parent.s_spinner_stop.emit() def _get_cover_url(self, media) -> str | None: """Extract cover URL from media object.""" with suppress(Exception): if hasattr(media, "album") and media.album: return media.album.image() if hasattr(media, "image") and callable(getattr(media, "image", None)): return media.image() return None def _display_cover(self, pixmap: QtGui.QPixmap, url: str): """Display a pixmap on the info tab.""" self.info_tab.set_cover_pixmap(pixmap) self.info_tab.cover_url_current = url self.cover_url_current = url def _display_default_cover(self): """Display default cover image.""" path_image = resource_path("tidal_dl_ng/ui/default_album_image.png") pixmap = QtGui.QPixmap(path_image) self.info_tab.set_cover_pixmap(pixmap) self.info_tab.cover_url_current = "" self.cover_url_current = "" def preload_covers_for_playlist(self, items: list) -> None: """Preload cover pixmaps for a list of tracks in background. Args: items: List of Track/Video objects to preload covers for. """ def worker() -> None: # Extract unique cover URLs cover_urls = set() for item in items[:50]: # Limit to first 50 with suppress(Exception): url = self._get_cover_url(item) if url: cover_urls.add(url) # Preload each unique cover for cover_url in cover_urls: if self.cache.get(cover_url): continue # Already cached with suppress(Exception): data_cover = Download.cover_data(cover_url) pixmap = QtGui.QPixmap() pixmap.loadFromData(data_cover) self.cache.set(cover_url, pixmap) logger_gui.debug(f"Preloaded cover: {cover_url[:50]}...") worker_obj = Worker(worker) self.threadpool.start(worker_obj) def _queue_cover_fetch(self, media): """Queue a cover fetch operation for a media item.""" with suppress(Exception): # previously try/except/pass self.threadpool.start(Worker(self.load_cover, media)) def _fetch_cover_pixmap(self, media, use_cache_check: bool): """Fetch cover pixmap for media, with optional cache check.""" with suppress(Exception): # previously try/except/continue inside loop pixmap = self.cache.get(self._get_cover_url(media)) if pixmap: return pixmap # If not in cache, download cover cover_url = self._get_cover_url(media) if cover_url: data_cover = Download.cover_data(cover_url) pixmap = QtGui.QPixmap() pixmap.loadFromData(data_cover) self.cache.set(cover_url, pixmap) return pixmap return None