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:
parent
c53681ea82
commit
bb8b816e8f
5 changed files with 264 additions and 228 deletions
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
214
mcg/client.py
214
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:
|
||||
|
|
46
mcg/utils.py
46
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
|
||||
|
||||
|
||||
|
|
200
mcg/widgets.py
200
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
|
||||
|
|
Loading…
Reference in a new issue