summaryrefslogtreecommitdiff
path: root/tidal_dl_ng/gui_covers.py
blob: 858ab11fa11b4044d867f0570d640ee2a0bd2f24 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
"""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