Support “albumart” command (close #30)

Load the album covers using MPD’s new “albumart” command instead of
reading the covers from the harddrive. Remove the corresponding UI
elements and configuration option.
This commit is contained in:
coderkun 2019-09-22 18:41:02 +02:00
parent c53681ea82
commit bb8b816e8f
5 changed files with 264 additions and 228 deletions

View file

@ -16,11 +16,6 @@
<summary>MPD port</summary>
<description>MPD port to connect to</description>
</key>
<key type="s" name="image-dir">
<default>''</default>
<summary>Image directory</summary>
<description>Directory which a webserver is providing images on</description>
</key>
<key type="b" name="connected">
<default>false</default>
<summary>Connection state</summary>

View file

@ -664,7 +664,7 @@
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
<property name="height">8</property>
<property name="height">6</property>
</packing>
</child>
<child>
@ -680,18 +680,6 @@
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="server-image-dir">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="placeholder_text" translatable="yes">Enter URL or local path</property>
<signal name="focus-out-event" handler="on_server-image-dir_focus_out_event" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">7</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="server-password">
<property name="visible">True</property>
@ -756,18 +744,6 @@
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Image Directory:</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">6</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
@ -1813,6 +1789,7 @@
<object class="GtkStack" id="library-standalone-stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="transition_type">crossfade</property>
<child>
<object class="GtkSpinner" id="library-standalone-spinner">
<property name="visible">True</property>

View file

@ -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:

View file

@ -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

View file

@ -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