diff --git a/data/de.coderkun.mcg.gschema.xml b/data/de.coderkun.mcg.gschema.xml index f3994aa..c7149e2 100644 --- a/data/de.coderkun.mcg.gschema.xml +++ b/data/de.coderkun.mcg.gschema.xml @@ -16,11 +16,6 @@ MPD port MPD port to connect to - - '' - Image directory - Directory which a webserver is providing images on - false Connection state diff --git a/data/gtk.glade b/data/gtk.glade index 0395471..a167a33 100644 --- a/data/gtk.glade +++ b/data/gtk.glade @@ -664,7 +664,7 @@ 0 0 - 8 + 6 @@ -680,18 +680,6 @@ 1 - - - True - True - Enter URL or local path - - - - 1 - 7 - - True @@ -756,18 +744,6 @@ 4 - - - True - False - start - Image Directory: - - - 1 - 6 - - True @@ -1813,6 +1789,7 @@ True False + crossfade True diff --git a/mcg/client.py b/mcg/client.py index f72443a..08d6aa0 100644 --- a/mcg/client.py +++ b/mcg/client.py @@ -1,16 +1,14 @@ #!/usr/bin/env python3 +import concurrent.futures import configparser -import glob import logging import os import queue import re import socket -import sys import threading -import urllib.request from mcg.utils import SortOrder from mcg.utils import Utils @@ -21,14 +19,13 @@ from mcg.utils import Utils class MPDException(Exception): def __init__(self, error): super(MPDException, self).__init__(self._parse_error(error)) - self._error = error def _parse_error(self, error): if error: parts = re.match("\[(\d+)@(\d+)\]\s\{(\w+)\}\s(.*)", error) if parts: - self._error = int(parts.group(1)) + self._error_number = int(parts.group(1)) self._command_number = int(parts.group(2)) self._command_name = parts.group(3) return parts.group(4) @@ -61,6 +58,18 @@ class CommandException(MPDException): +class Future(concurrent.futures.Future): + def __init__(self, signal): + concurrent.futures.Future.__init__(self) + self._signal = signal + + + def get_signal(self): + return self._signal + + + + class Base(): def __init__(self): self._callbacks = {} @@ -88,6 +97,10 @@ class Base(): callback(*data) + def _callback_future(self, future): + self._callback(future.get_signal(), *future.result()) + + class Client(Base): @@ -104,6 +117,8 @@ class Client(Base): PROTOCOL_ERROR = 'ACK ' # Protocol: error: permission PROTOCOL_ERROR_PERMISSION = 4 + # Protocol: error: no exists + PROTOCOL_ERROR_NOEXISTS = 50 # Signal: connection status SIGNAL_CONNECTION = 'connection' # Signal: status @@ -120,6 +135,8 @@ class Client(Base): SIGNAL_LOAD_PLAYLIST = 'load-playlist' # Signal: load audio output devices SIGNAL_LOAD_OUTPUT_DEVICES = 'load-output-devices' + # Signal: load albumart + SIGNAL_LOAD_ALBUMART = 'albumart' # Signal: custom (dummy) event to trigger callback SIGNAL_CUSTOM = 'custom' # Signal: error @@ -143,7 +160,6 @@ class Client(Base): self._host = None self._albums = {} self._playlist = [] - self._image_dir = "" self._state = None @@ -153,13 +169,12 @@ class Client(Base): # Client commands - def connect(self, host, port, password=None, image_dir=""): + def connect(self, host, port, password=None): """Connect to MPD with the given host, port and password or with standard values. """ self._logger.info("connect") self._host = host - self._image_dir = image_dir self._add_action(self._connect, host, port, password) self._stop.clear() self._start_worker() @@ -184,19 +199,19 @@ class Client(Base): def get_status(self): """Determine the current status.""" self._logger.info("get status") - self._add_action(self._get_status) + self._add_action_signal(Client.SIGNAL_STATUS, self._get_status) def get_stats(self): """Load statistics.""" self._logger.info("get stats") - self._add_action(self._get_stats) + self._add_action_signal(Client.SIGNAL_STATS, self._get_stats) def get_output_devices(self): """Determine the list of audio output devices.""" self._logger.info("get output devices") - self._add_action(self._get_output_devices) + self._add_action_signal(Client.SIGNAL_LOAD_OUTPUT_DEVICES, self._get_output_devices) def enable_output_device(self, device, enabled): @@ -208,7 +223,7 @@ class Client(Base): def load_albums(self): self._logger.info("load albums") - self._add_action(self._load_albums) + self._add_action_signal(Client.SIGNAL_LOAD_ALBUMS, self._load_albums) def update(self): @@ -218,7 +233,7 @@ class Client(Base): def load_playlist(self): self._logger.info("load playlist") - self._add_action(self._load_playlist) + self._add_action_signal(Client.SIGNAL_LOAD_PLAYLIST, self._load_playlist) def clear_playlist(self): @@ -285,9 +300,22 @@ class Client(Base): self._add_action(self._set_volume, volume) + def get_albumart(self, album): + self._logger.info("get albumart") + self._add_action_signal(Client.SIGNAL_LOAD_ALBUMART, self._get_albumart, album) + + + def get_albumart_now(self, album): + self._logger.info("get albumart now") + future = concurrent.futures.Future() + self._add_action_future(future, self._get_albumart, album) + (_, albumart) = future.result() + return albumart + + def get_custom(self, name): self._logger.info("get custom \"%s\"", name) - self._add_action(self._get_custom, name) + self._add_action_signal(Client.SIGNAL_CUSTOM, self._get_custom, name) # Private methods @@ -442,7 +470,7 @@ class Client(Base): bitrate = None if 'bitrate' in status: bitrate = status['bitrate'] - self._callback(Client.SIGNAL_STATUS, state, album, pos, time, volume, file, audio, bitrate, error) + return (state, album, pos, time, volume, file, audio, bitrate, error) def _get_stats(self): @@ -475,7 +503,7 @@ class Client(Base): uptime = 0 if 'uptime' in stats: uptime = stats['uptime'] - self._callback(Client.SIGNAL_STATS, artists, albums, songs, dbplaytime, playtime, uptime) + return (artists, albums, songs, dbplaytime, playtime, uptime) def _get_output_devices(self): @@ -485,7 +513,7 @@ class Client(Base): device = OutputDevice(output['outputid'], output['outputname']) device.set_enabled(int(output['outputenabled']) == 1) devices.append(device) - self._callback(Client.SIGNAL_LOAD_OUTPUT_DEVICES, devices) + return (devices, ) def _enable_output_device(self, device, enabled): @@ -513,7 +541,7 @@ class Client(Base): if track: self._logger.debug("track: %r", track) album.add_track(track) - self._callback(Client.SIGNAL_LOAD_ALBUMS, self._albums) + return (self._albums, ) def _update(self): @@ -536,7 +564,7 @@ class Client(Base): self._logger.debug("album: %r", album) if track: album.add_track(track) - self._callback(Client.SIGNAL_LOAD_PLAYLIST, self._playlist) + return (self._playlist, ) def _clear_playlist(self): @@ -615,8 +643,62 @@ class Client(Base): self._call('setvol', volume) + def _get_albumart(self, album): + data = None + if album in self._albums: + album = self._albums[album] + if not album.get_tracks(): + return None + self._logger.debug("get albumart for album \"%s\"", album.get_title()) + size = 1 + offset = 0 + index = 0 + + # Read data until size is reached + try: + while offset < size: + self._write('albumart', args=[album.get_tracks()[0].get_file(), offset]) + + # Read first line which tells us whether there is an albumart + line = self._read_line() + if line.startswith(Client.PROTOCOL_ERROR): + error = line[len(Client.PROTOCOL_ERROR):].strip() + self._logger.debug("command failed: %r", error) + raise CommandException(error) + # First line is the file size + size = int(self._parse_dict([line])['size']) + self._logger.debug("size: %d", size) + # Second line is the count of bytes read + binary = int(self._parse_dict([self._read_line()])['binary']) + self._logger.debug("binary: %d", binary) + + # Create new data array on the first iteration + if not data: + data = bytearray(size) + # Create a view for the current chunk of data + data_view = memoryview(data)[offset:offset+binary] + # Read actual bytes + self._read_bytes(data_view, binary) + offset += binary + # Read line break to complete previous repsonse + self._read_line() + # Read command completion + end = self._read_line() + if not end.startswith(Client.PROTOCOL_COMPLETION): + self._logger.debug("albumart not completed") + data = None + break + except CommandException as e: + # If no albumart can be found, do not throw an exception + if e.get_error() == Client.PROTOCOL_ERROR_NOEXISTS: + data = None + else: + raise e + return (album, data) + + def _get_custom(self, name): - self._callback(Client.SIGNAL_CUSTOM, name) + return (name, ) def _start_worker(self): @@ -643,22 +725,46 @@ class Client(Base): def _add_action(self, method, *args): """Add an action to the action list.""" self._logger.debug("add action %r (%r)", method.__name__, args) - action = (method, args) + future = concurrent.futures.Future() + action = (future, method, args) + self._actions.put(action) + self._noidle() + + return future + + + def _add_action_signal(self, signal, method, *args): + """Add an action to the action list that triggers a callback.""" + self._logger.debug("add action signal %r: %r (%r)", signal, method.__name__, args) + future = Future(signal) + future.add_done_callback(self._callback_future) + self._add_action_future(future, method, *args) + + return future + + + def _add_action_future(self, future, method, *args): + """Add an action to the action list based on a futre.""" + self._logger.debug("add action future %r (%r)", method.__name__, args) + action = (future, method, args) self._actions.put(action) self._noidle() def _work(self, action): - (method, args) = action + (future, method, args) = action self._logger.debug("work: %r", method.__name__) try: - method(*args) + result = method(*args) + future.set_result(result) except ConnectionException as e: self._logger.exception(e) + future.set_exception(e) self._callback(Client.SIGNAL_ERROR, e) self._disconnect_socket() except Exception as e: self._logger.exception(e) + future.set_exception(e) self._callback(Client.SIGNAL_ERROR, e) @@ -808,7 +914,7 @@ class Client(Base): if lookup and id in self._albums.keys(): album = self._albums[id] else: - album = MCGAlbum(song['album'], self._host, self._image_dir) + album = MCGAlbum(song['album'], self._host) if lookup: self._albums[id] = album return album @@ -876,7 +982,7 @@ class MCGAlbum: _FILTER_DELIMITER = ' ' - def __init__(self, title, host, image_dir): + def __init__(self, title, host): self._artists = [] self._albumartists = [] self._pathes = [] @@ -885,11 +991,8 @@ class MCGAlbum: self._title = title self._dates = [] self._host = host - self._image_dir = image_dir self._tracks = [] self._length = 0 - self._cover = None - self._cover_searched = False self._id = Utils.generate_id(title) @@ -959,12 +1062,6 @@ class MCGAlbum: return self._length - def get_cover(self): - if self._cover is None and not self._cover_searched: - self._find_cover() - return self._cover - - def filter(self, filter_string): if len(filter_string) == 0: return True @@ -1017,55 +1114,6 @@ class MCGAlbum: return 1 - def _find_cover(self): - names = list(MCGAlbum._FILE_NAMES) - names.append(self._title) - - if self._host == "localhost" or self._host == "127.0.0.1" or self._host == "::1": - self._cover = self._find_cover_local(names) - else: - self._cover = self._find_cover_web(names) - self._cover_searched = True - - - def _find_cover_web(self, names): - for path in self._pathes: - for name in names: - for ext in self._FILE_EXTS: - url = '/'.join([ - 'http:/', - self._host, - urllib.request.quote(self._image_dir.strip("/")), - urllib.request.quote(path), - urllib.request.quote('.'.join([name, ext])) - ]) - request = urllib.request.Request(url) - try: - response = urllib.request.urlopen(request) - return url - except urllib.error.URLError as e: - pass - - - def _find_cover_local(self, names): - for path in self._pathes: - for name in names: - for ext in self._FILE_EXTS: - filename = os.path.join(self._image_dir, path, '.'.join([name, ext])) - if os.path.isfile(filename): - return filename - return self._find_cover_local_fallback() - - - def _find_cover_local_fallback(self): - for path in self._pathes: - for ext in self._FILE_EXTS: - filename = os.path.join(self._image_dir, path, "*."+ext) - files = glob.glob(filename) - if len(files) > 0: - return files[0] - - class MCGTrack: diff --git a/mcg/utils.py b/mcg/utils.py index 22bcbed..0cc53ec 100644 --- a/mcg/utils.py +++ b/mcg/utils.py @@ -14,45 +14,31 @@ from gi.repository import GdkPixbuf class Utils: - def load_cover(url): - if not url: - return None - if url.startswith('/'): - try: - return GdkPixbuf.Pixbuf.new_from_file(url) - except Exception as e: - print(e) - return None - else: - try: - response = urllib.request.urlopen(url) - loader = GdkPixbuf.PixbufLoader() - loader.write(response.read()) - loader.close() - return loader.get_pixbuf() - except Exception as e: - print(e) - return None - def load_thumbnail(cache, album, size): + def load_pixbuf(data): + loader = GdkPixbuf.PixbufLoader() + try: + loader.write(data) + finally: + loader.close() + return loader.get_pixbuf() + + + def load_thumbnail(cache, client, album, size): cache_url = cache.create_filename(album) pixbuf = None if os.path.isfile(cache_url): - try: - pixbuf = GdkPixbuf.Pixbuf.new_from_file(cache_url) - except Exception as e: - print(e) + pixbuf = GdkPixbuf.Pixbuf.new_from_file(cache_url) else: - url = album.get_cover() - pixbuf = Utils.load_cover(url) + # Load cover from server + albumart = client.get_albumart_now(album.get_id()) + if albumart: + pixbuf = Utils.load_pixbuf(albumart) if pixbuf is not None: pixbuf = pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.HYPER) - filetype = os.path.splitext(url)[1][1:] - if filetype == 'jpg': - filetype = 'jpeg' - pixbuf.savev(cache.create_filename(album), filetype, [], []) + pixbuf.savev(cache_url, 'jpeg', [], []) return pixbuf diff --git a/mcg/widgets.py b/mcg/widgets.py index c983c12..a0197b5 100644 --- a/mcg/widgets.py +++ b/mcg/widgets.py @@ -96,7 +96,6 @@ class Window(): SETTING_HOST = 'host' SETTING_PORT = 'port' SETTING_CONNECTED = 'connected' - SETTING_IMAGE_DIR = 'image-dir' SETTING_WINDOW_WIDTH = 'width' SETTING_WINDOW_HEIGHT = 'height' SETTING_WINDOW_MAXIMIZED = 'is-maximized' @@ -128,8 +127,8 @@ class Window(): # Panels self._panels.append(ServerPanel(builder)) self._panels.append(CoverPanel(builder)) - self._panels.append(PlaylistPanel(builder)) - self._panels.append(LibraryPanel(builder)) + self._panels.append(PlaylistPanel(builder, self._mcg)) + self._panels.append(LibraryPanel(builder, self._mcg)) # Widgets # InfoBar @@ -148,7 +147,6 @@ class Window(): self._connection_panel.set_port(self._settings.get_int(Window.SETTING_PORT)) if use_keyring: self._connection_panel.set_password(keyring.get_password(ZeroconfProvider.KEYRING_SYSTEM, ZeroconfProvider.KEYRING_USERNAME)) - self._connection_panel.set_image_dir(self._settings.get_string(Window.SETTING_IMAGE_DIR)) self._panels[Window._PANEL_INDEX_PLAYLIST].set_item_size(self._settings.get_int(Window.SETTING_ITEM_SIZE)) self._panels[Window._PANEL_INDEX_LIBRARY].set_item_size(self._settings.get_int(Window.SETTING_ITEM_SIZE)) self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_order(self._settings.get_enum(Window.SETTING_SORT_ORDER)) @@ -163,10 +161,12 @@ class Window(): self._panels[Window._PANEL_INDEX_SERVER].connect('change-output-device', self.on_server_panel_output_device_changed) self._panels[Window._PANEL_INDEX_COVER].connect('toggle-fullscreen', self.on_cover_panel_toggle_fullscreen) self._panels[Window._PANEL_INDEX_COVER].connect('set-song', self.on_cover_panel_set_song) + self._panels[Window._PANEL_INDEX_COVER].connect('albumart', self.on_cover_panel_albumart) self._panels[Window._PANEL_INDEX_PLAYLIST].connect('clear-playlist', self.on_playlist_panel_clear_playlist) self._panels[Window._PANEL_INDEX_PLAYLIST].connect('remove', self.on_playlist_panel_remove) self._panels[Window._PANEL_INDEX_PLAYLIST].connect('remove-multiple', self.on_playlist_panel_remove_multiple) self._panels[Window._PANEL_INDEX_PLAYLIST].connect('play', self.on_playlist_panel_play) + self._panels[Window._PANEL_INDEX_PLAYLIST].connect('albumart', self.on_playlist_panel_albumart) self._panels[Window._PANEL_INDEX_LIBRARY].connect('update', self.on_library_panel_update) self._panels[Window._PANEL_INDEX_LIBRARY].connect('play', self.on_library_panel_play) self._panels[Window._PANEL_INDEX_LIBRARY].connect('queue', self.on_library_panel_queue) @@ -174,6 +174,7 @@ class Window(): self._panels[Window._PANEL_INDEX_LIBRARY].connect('item-size-changed', self.on_library_panel_item_size_changed) self._panels[Window._PANEL_INDEX_LIBRARY].connect('sort-order-changed', self.on_library_panel_sort_order_changed) self._panels[Window._PANEL_INDEX_LIBRARY].connect('sort-type-changed', self.on_library_panel_sort_type_changed) + self._panels[Window._PANEL_INDEX_LIBRARY].connect('albumart', self.on_library_panel_albumart) self._mcg.connect_signal(client.Client.SIGNAL_CONNECTION, self.on_mcg_connect) self._mcg.connect_signal(client.Client.SIGNAL_STATUS, self.on_mcg_status) self._mcg.connect_signal(client.Client.SIGNAL_STATS, self.on_mcg_stats) @@ -182,6 +183,7 @@ class Window(): self._mcg.connect_signal(client.Client.SIGNAL_PULSE_ALBUMS, self.on_mcg_pulse_albums) self._mcg.connect_signal(client.Client.SIGNAL_INIT_ALBUMS, self.on_mcg_init_albums) self._mcg.connect_signal(client.Client.SIGNAL_LOAD_ALBUMS, self.on_mcg_load_albums) + self._mcg.connect_signal(client.Client.SIGNAL_LOAD_ALBUMART, self.on_mcg_load_albumart) self._mcg.connect_signal(client.Client.SIGNAL_CUSTOM, self.on_mcg_custom) self._mcg.connect_signal(client.Client.SIGNAL_ERROR, self.on_mcg_error) self._settings.connect('changed::'+Window.SETTING_PANEL, self.on_settings_panel_changed) @@ -312,7 +314,7 @@ class Window(): # Panel callbacks - def on_connection_panel_connection_changed(self, widget, host, port, password, image_dir): + def on_connection_panel_connection_changed(self, widget, host, port, password): self._settings.set_string(Window.SETTING_HOST, host) self._settings.set_int(Window.SETTING_PORT, port) if use_keyring: @@ -321,7 +323,6 @@ class Window(): else: if keyring.get_password(ZeroconfProvider.KEYRING_SYSTEM, ZeroconfProvider.KEYRING_USERNAME): keyring.delete_password(ZeroconfProvider.KEYRING_SYSTEM, ZeroconfProvider.KEYRING_USERNAME) - self._settings.set_string(Window.SETTING_IMAGE_DIR, image_dir) def on_playlist_panel_clear_playlist(self, widget): @@ -340,6 +341,10 @@ class Window(): self._mcg.play_album_from_playlist(album) + def on_playlist_panel_albumart(self, widget, album): + self._mcg.get_albumart(album) + + def on_server_panel_output_device_changed(self, widget, device, enabled): self._mcg.enable_output_device(device, enabled) @@ -355,6 +360,10 @@ class Window(): self._mcg.seek(pos, time) + def on_cover_panel_albumart(self, widget, album): + self._mcg.get_albumart(album) + + def on_library_panel_update(self, widget): self._mcg.update() @@ -384,6 +393,10 @@ class Window(): self._settings.set_boolean(Window.SETTING_SORT_TYPE, self._panels[Window._PANEL_INDEX_LIBRARY].get_sort_type()) + def on_library_panel_albumart(self, widget, album): + self._mcg.get_albumart(album) + + # MCG callbacks def on_mcg_connect(self, connected): @@ -456,6 +469,12 @@ class Window(): self._panels[self._PANEL_INDEX_LIBRARY].set_albums(self._connection_panel.get_host(), albums) + def on_mcg_load_albumart(self, album, data): + GObject.idle_add(self._panels[self._PANEL_INDEX_COVER].set_albumart, album, data) + GObject.idle_add(self._panels[self._PANEL_INDEX_PLAYLIST].set_albumart, album, data) + GObject.idle_add(self._panels[self._PANEL_INDEX_LIBRARY].set_albumart, album, data) + + def on_mcg_custom(self, name): if name == Window._CUSTOM_STARTUP_COMPLETE: for panel in self._panels: @@ -506,8 +525,7 @@ class Window(): host = self._connection_panel.get_host() port = self._connection_panel.get_port() password = self._connection_panel.get_password() - image_dir = self._connection_panel.get_image_dir() - self._mcg.connect(host, port, password, image_dir) + self._mcg.connect(host, port, password) self._settings.set_boolean(Window.SETTING_CONNECTED, True) @@ -741,7 +759,7 @@ class InfoBar(): class ConnectionPanel(GObject.GObject): __gsignals__ = { - 'connection-changed': (GObject.SIGNAL_RUN_FIRST, None, (str, int, str, str)) + 'connection-changed': (GObject.SIGNAL_RUN_FIRST, None, (str, int, str)) } @@ -764,8 +782,6 @@ class ConnectionPanel(GObject.GObject): self._port_spinner = builder.get_object('server-port') # Passwort self._password_entry = builder.get_object('server-password') - # Image directory - self._image_dir_entry = builder.get_object('server-image-dir') # Zeroconf provider self._zeroconf_provider = ZeroconfProvider() @@ -782,8 +798,7 @@ class ConnectionPanel(GObject.GObject): 'on_server-zeroconf-list_focus_out_event': self.on_zeroconf_list_outfocused, 'on_server-host_focus_out_event': self.on_host_entry_outfocused, 'on_server-port_value_changed': self.on_port_spinner_value_changed, - 'on_server-password_focus_out_event': self.on_password_entry_outfocused, - 'on_server-image-dir_focus_out_event': self.on_image_dir_entry_outfocused + 'on_server-password_focus_out_event': self.on_password_entry_outfocused } @@ -816,10 +831,6 @@ class ConnectionPanel(GObject.GObject): self._call_back() - def on_image_dir_entry_outfocused(self, widget, event): - self._call_back() - - def set_host(self, host): self._host_entry.set_text(host) @@ -849,16 +860,8 @@ class ConnectionPanel(GObject.GObject): return self._password_entry.get_text() - def set_image_dir(self, image_dir): - self._image_dir_entry.set_text(image_dir) - - - def get_image_dir(self): - return self._image_dir_entry.get_text() - - def _call_back(self): - self.emit('connection-changed', self.get_host(), self.get_port(), self.get_password(), self.get_image_dir()) + self.emit('connection-changed', self.get_host(), self.get_port(), self.get_password(),) @@ -983,7 +986,8 @@ class ServerPanel(GObject.GObject): class CoverPanel(GObject.GObject): __gsignals__ = { 'toggle-fullscreen': (GObject.SIGNAL_RUN_FIRST, None, ()), - 'set-song': (GObject.SIGNAL_RUN_FIRST, None, (int, int,)) + 'set-song': (GObject.SIGNAL_RUN_FIRST, None, (int, int,)), + 'albumart': (GObject.SIGNAL_RUN_FIRST, None, (str,)) } @@ -991,6 +995,7 @@ class CoverPanel(GObject.GObject): GObject.GObject.__init__(self) self._current_album = None + self._current_cover_album = None self._cover_pixbuf = None self._timer = None self._properties = {} @@ -1088,15 +1093,17 @@ class CoverPanel(GObject.GObject): # Set tracks self._set_tracks(album) + # Load cover + if album != self._current_cover_album: + self._cover_stack.set_visible_child(self._cover_spinner) + self._cover_spinner.start() + self.emit('albumart', album.get_id() if album else None) + # Set current album - old_album = self._current_album self._current_album = album self._enable_tracklist() self._toolbar_fullscreen_button.set_sensitive(self._current_album is not None) - # Load cover - threading.Thread(target=self._set_cover, args=(old_album, album,)).start() - def set_play(self, pos, time): if self._timer is not None: @@ -1137,21 +1144,24 @@ class CoverPanel(GObject.GObject): ) - def _set_cover(self, current_album, new_album): - self._cover_stack.set_visible_child(self._cover_spinner) - self._cover_spinner.start() - if not current_album or not new_album or current_album != new_album: - url = new_album.get_cover() if new_album else None - if url: + def set_albumart(self, album, data): + if album == self._current_album: + if data: # Load image and draw it - self._cover_pixbuf = Utils.load_cover(url) + try: + self._cover_pixbuf = Utils.load_pixbuf(data) + except Exception as e: + self._logger.exception("Failed to set albumart") + self._cover_pixbuf = self._get_default_image() else: # Reset image self._cover_pixbuf = self._get_default_image() self._current_size = None + # Show image self._resize_image() - self._cover_stack.set_visible_child(self._cover_scroll) - self._cover_spinner.stop() + self._cover_stack.set_visible_child(self._cover_scroll) + self._cover_spinner.stop() + self._current_cover_album = album def _set_tracks(self, album): @@ -1242,12 +1252,15 @@ class PlaylistPanel(GObject.GObject): 'clear-playlist': (GObject.SIGNAL_RUN_FIRST, None, ()), 'remove': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)), 'remove-multiple': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)), - 'play': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)) + 'play': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)), + 'albumart': (GObject.SIGNAL_RUN_FIRST, None, (str,)) } - def __init__(self, builder): + def __init__(self, builder, client): GObject.GObject.__init__(self) + self._logger = logging.getLogger(__name__) + self._client = client self._host = None self._item_size = 150 self._playlist = None @@ -1353,6 +1366,7 @@ class PlaylistPanel(GObject.GObject): hash = self._playlist_grid_model.get_value(iter, 2) album = self._playlist_albums[hash] self._selected_albums = [album] + self.emit('albumart', hash) # Show standalone album if widget.get_selection_mode() == Gtk.SelectionMode.SINGLE: @@ -1363,8 +1377,9 @@ class PlaylistPanel(GObject.GObject): # Show panel self._open_standalone() - # Load cover - threading.Thread(target=self._show_standalone_image, args=(album,)).start() + # Set cover loading indicator + self._standalone_stack.set_visible_child(self._standalone_spinner) + self._standalone_spinner.start() def on_playlist_grid_selection_changed(self, widget): @@ -1418,6 +1433,23 @@ class PlaylistPanel(GObject.GObject): threading.Thread(target=self._set_playlist, args=(host, playlist, self._item_size,)).start() + def set_albumart(self, album, data): + if album in self._selected_albums: + if data: + # Load image and draw it + try: + self._standalone_pixbuf = Utils.load_pixbuf(data) + except Exception as e: + self._logger.exception("Failed to set albumart") + self._cover_pixbuf = self._get_default_image() + else: + self._cover_pixbuf = self._get_default_image() + # Show image + self._resize_standalone_image() + self._standalone_stack.set_visible_child(self._standalone_scroll) + self._standalone_spinner.stop() + + def stop_threads(self): self._playlist_stop.set() @@ -1444,11 +1476,14 @@ class PlaylistPanel(GObject.GObject): cache = client.MCGCache(host, size) for album in playlist: pixbuf = None - if album.get_cover() is not None: - try: - pixbuf = Utils.load_thumbnail(cache, album, size) - except Exception as e: - print(e) + # Load albumart thumbnail + try: + pixbuf = Utils.load_thumbnail(cache, self._client, album, size) + except client.CommandException: + # Exception is handled by client + pass + except Exception: + self._logger.exception("Failed to load albumart") if pixbuf is None: pixbuf = self._icon_theme.load_icon( Window.STOCK_ICON_DEFAULT, @@ -1493,21 +1528,6 @@ class PlaylistPanel(GObject.GObject): self._appwindow.set_titlebar(self._headerbar) - def _show_standalone_image(self, album): - self._standalone_stack.set_visible_child(self._standalone_spinner) - self._standalone_spinner.start() - url = album.get_cover() - if url: - # Load image and draw it - self._standalone_pixbuf = Utils.load_cover(url) - else: - # Reset image - self._standalone_pixbuf = self._get_default_image() - self._resize_standalone_image() - self._standalone_stack.set_visible_child(self._standalone_scroll) - self._standalone_spinner.stop() - - def _resize_standalone_image(self): """Diese Methode skaliert das geladene Bild aus dem Pixelpuffer auf die Größe des Fensters unter Beibehalt der Seitenverhältnisse @@ -1553,12 +1573,15 @@ class LibraryPanel(GObject.GObject): 'queue-multiple': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)), 'item-size-changed': (GObject.SIGNAL_RUN_FIRST, None, (int,)), 'sort-order-changed': (GObject.SIGNAL_RUN_FIRST, None, (int,)), - 'sort-type-changed': (GObject.SIGNAL_RUN_FIRST, None, (Gtk.SortType,)) + 'sort-type-changed': (GObject.SIGNAL_RUN_FIRST, None, (Gtk.SortType,)), + 'albumart': (GObject.SIGNAL_RUN_FIRST, None, (str,)) } - def __init__(self, builder): + def __init__(self, builder, client): GObject.GObject.__init__(self) + self._logger = logging.getLogger(__name__) + self._client = client self._buttons = {} self._albums = None self._host = "localhost" @@ -1775,6 +1798,7 @@ class LibraryPanel(GObject.GObject): id = self._library_grid_model.get_value(iter, 2) album = self._albums[id] self._selected_albums = [album] + self.emit('albumart', id) # Show standalone album if widget.get_selection_mode() == Gtk.SelectionMode.SINGLE: @@ -1785,8 +1809,9 @@ class LibraryPanel(GObject.GObject): # Show panel self._open_standalone() - # Load cover - threading.Thread(target=self._show_standalone_image, args=(album,)).start() + # Set cover loading indicator + self._standalone_stack.set_visible_child(self._standalone_spinner) + self._standalone_spinner.start() def on_library_grid_selection_changed(self, widget): @@ -1889,6 +1914,23 @@ class LibraryPanel(GObject.GObject): threading.Thread(target=self._set_albums, args=(host, albums, self._item_size,)).start() + def set_albumart(self, album, data): + if album in self._selected_albums: + if data: + # Load image and draw it + try: + self._standalone_pixbuf = Utils.load_pixbuf(data) + except Exception as e: + self._logger.exception("Failed to set albumart") + self._standalone_pixbuf = self._get_default_image() + else: + self._standalone_pixbuf = self._get_default_image() + # Show image + self._resize_standalone_image() + self._standalone_stack.set_visible_child(self._standalone_scroll) + self._standalone_spinner.stop() + + def compare_albums(self, model, row1, row2, criterion): id1 = model.get_value(row1, 2) id2 = model.get_value(row2, 2) @@ -1934,9 +1976,12 @@ class LibraryPanel(GObject.GObject): album = albums[album_id] pixbuf = None try: - pixbuf = Utils.load_thumbnail(cache, album, size) + pixbuf = Utils.load_thumbnail(cache, self._client, album, size) + except client.CommandException: + # Exception is handled by client + pass except Exception as e: - print(e) + self._logger.exception("Failed to load albumart") if pixbuf is None: pixbuf = self._icon_theme.load_icon( Window.STOCK_ICON_DEFAULT, @@ -2040,21 +2085,6 @@ class LibraryPanel(GObject.GObject): self._appwindow.set_titlebar(self._headerbar) - def _show_standalone_image(self, album): - self._standalone_stack.set_visible_child(self._standalone_spinner) - self._standalone_spinner.start() - url = album.get_cover() - if url: - # Load image and draw it - self._standalone_pixbuf = Utils.load_cover(url) - else: - # Reset image - self._standalone_pixbuf = self._get_default_image() - self._resize_standalone_image() - self._standalone_stack.set_visible_child(self._standalone_scroll) - self._standalone_spinner.stop() - - def _resize_standalone_image(self): """Diese Methode skaliert das geladene Bild aus dem Pixelpuffer auf die Größe des Fensters unter Beibehalt der Seitenverhältnisse