diff --git a/gui/gtk.py b/gui/gtk.py
index 07738e3..2cb14db 100755
--- a/gui/gtk.py
+++ b/gui/gtk.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""MPDCoverGrid is a client for the Music Player Daemon, focused on albums instead of single tracks."""
@@ -11,7 +11,9 @@ __status__ = "Development"
import math
+import logging
import os
+import sys
import threading
import urllib
@@ -23,1474 +25,1471 @@ import mcg
class Application(Gtk.Application):
- TITLE = "MPDCoverGrid (Gtk)"
- SETTINGS_BASE_KEY = 'de.coderkun.mcg'
- SETTING_WINDOW_SIZE = 'window-size'
- SETTING_WINDOW_MAXIMIZED = 'window-maximized'
- SETTING_PROFILE = 'profile'
- SETTING_PANEL = 'panel'
- SETTING_ITEM_SIZE = 'item-size'
- SETTING_SORT_ORDER = 'sort-order'
- SETTING_SORT_TYPE = 'sort-type'
+ TITLE = "MPDCoverGrid (Gtk)"
+ SETTINGS_BASE_KEY = 'de.coderkun.mcg'
+ SETTING_WINDOW_SIZE = 'window-size'
+ SETTING_WINDOW_MAXIMIZED = 'window-maximized'
+ SETTING_PROFILE = 'profile'
+ SETTING_PANEL = 'panel'
+ SETTING_ITEM_SIZE = 'item-size'
+ SETTING_SORT_ORDER = 'sort-order'
+ SETTING_SORT_TYPE = 'sort-type'
- def __init__(self):
- Gtk.Application.__init__(self, application_id="de.coderkun.mcg", flags=Gio.ApplicationFlags.FLAGS_NONE)
- self._window = None
- self._settings = Gio.Settings.new(Application.SETTINGS_BASE_KEY)
+ def __init__(self):
+ Gtk.Application.__init__(self, application_id="de.coderkun.mcg", flags=Gio.ApplicationFlags.FLAGS_NONE)
+ self._window = None
+ self._settings = Gio.Settings.new(Application.SETTINGS_BASE_KEY)
- def do_startup(self):
- Gtk.Application.do_startup(self)
+ def do_startup(self):
+ Gtk.Application.do_startup(self)
- def do_activate(self):
- if not self._window:
- self._window = Window(self, Application.TITLE, self._settings)
- self._window.present()
+ def do_activate(self):
+ if not self._window:
+ self._window = Window(self, Application.TITLE, self._settings)
+ self._window.present()
- def action_quit_cb(self, action, parameter):
- self._window.destroy()
- self.quit()
+ def action_quit_cb(self, action, parameter):
+ self._window.destroy()
+ self.quit()
- def load_thumbnail(cache, album, size):
- cache_url = cache.create_filename(album)
- pixbuf = None
+ def load_thumbnail(cache, 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)
- else:
- url = album.get_cover()
- if url is not None:
- if url.startswith('/'):
- try:
- pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(url, size, size)
- except Exception as e:
- print(e)
- else:
- try:
- response = urllib.request.urlopen(url)
- loader = GdkPixbuf.PixbufLoader()
- loader.write(response.read())
- loader.close()
- pixbuf = loader.get_pixbuf().scale_simple(size, size, GdkPixbuf.InterpType.HYPER)
- except Exception as e:
- print(e)
- if pixbuf is not None:
- filetype = os.path.splitext(url)[1][1:]
- if filetype == 'jpg':
- filetype = 'jpeg'
- pixbuf.savev(cache.create_filename(album), filetype, [], [])
- return pixbuf
+ if os.path.isfile(cache_url):
+ try:
+ pixbuf = GdkPixbuf.Pixbuf.new_from_file(cache_url)
+ except Exception as e:
+ print(e)
+ else:
+ url = album.get_cover()
+ if url is not None:
+ if url.startswith('/'):
+ try:
+ pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(url, size, size)
+ except Exception as e:
+ print(e)
+ else:
+ try:
+ response = urllib.request.urlopen(url)
+ loader = GdkPixbuf.PixbufLoader()
+ loader.write(response.read())
+ loader.close()
+ pixbuf = loader.get_pixbuf().scale_simple(size, size, GdkPixbuf.InterpType.HYPER)
+ except Exception as e:
+ print(e)
+ if pixbuf is not None:
+ filetype = os.path.splitext(url)[1][1:]
+ if filetype == 'jpg':
+ filetype = 'jpeg'
+ pixbuf.savev(cache.create_filename(album), filetype, [], [])
+ return pixbuf
class Window(Gtk.ApplicationWindow):
- STYLE_CLASS_BG_TEXTURE = 'bg-texture'
- STYLE_CLASS_NO_BG = 'no-bg'
- STYLE_CLASS_NO_BORDER = 'no-border'
- _PANEL_INDEX_CONNECTION = 0
- _PANEL_INDEX_COVER = 1
- _PANEL_INDEX_PLAYLIST = 2
- _PANEL_INDEX_LIBRARY = 3
-
-
- def __init__(self, app, title, settings):
- Gtk.Window.__init__(self, title=title, application=app)
- self._settings = settings
- self._panels = []
- self._mcg = mcg.MCGClient()
- self._size = self._settings.get_value(Application.SETTING_WINDOW_SIZE)
- self._maximized = self._settings.get_boolean(Application.SETTING_WINDOW_MAXIMIZED)
- self._fullscreened = False
-
- # Panels
- self._panels.append(ConnectionPanel())
- self._panels.append(CoverPanel())
- self._panels.append(PlaylistPanel())
- self._panels.append(LibraryPanel())
-
- # Widgets
- self._main_box = Gtk.VBox()
- self._main_box.get_style_context().add_class(Window.STYLE_CLASS_NO_BG)
- self.add(self._main_box)
- # InfoBar
- self._infobar = InfoBar()
- self._main_box.pack_start(self._infobar, False, True, 0)
- # Stack
- self._stack = Gtk.Stack()
- for panel in self._panels:
- self._stack.add_titled(panel, panel.get_name(), panel.get_title())
- self._stack.set_homogeneous(True)
- self._main_box.pack_end(self._stack, True, True, 0)
- # Header
- self._header_bar = HeaderBar(self._stack)
- self.set_titlebar(self._header_bar)
-
- # Properties
- self._header_bar.set_sensitive(False, False)
- styleProvider = Gtk.CssProvider()
- styleProvider.load_from_data(b"""
- GtkWidget.bg-texture {
- box-shadow:inset 4px 4px 10px rgba(0,0,0,0.3);
- background-image:url('data/noise-texture.png');
- }
- GtkWidget.no-bg {
- background:none;
- }
- GtkWidget.no-border {
- border:none;
- }
- GtkIconView.cell:selected,
- GtkIconView.cell:selected:focus {
- background-color:@theme_selected_bg_color;
- }
- """)
- self.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), styleProvider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
- self.get_style_context().add_class(Window.STYLE_CLASS_BG_TEXTURE)
- self._panels[Window._PANEL_INDEX_PLAYLIST].set_item_size(self._settings.get_int(Application.SETTING_ITEM_SIZE))
- self._panels[Window._PANEL_INDEX_LIBRARY].set_item_size(self._settings.get_int(Application.SETTING_ITEM_SIZE))
- self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_order(self._settings.get_string(Application.SETTING_SORT_ORDER))
- self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_type(self._settings.get_boolean(Application.SETTING_SORT_TYPE))
-
- # Signals
- self.connect('size-allocate', self.on_resize)
- self.connect('window-state-event', self.on_state)
- self.connect('destroy', self.on_destroy)
- self._header_bar.connect_signal(HeaderBar.SIGNAL_STACK_SWITCHED, self.on_header_bar_stack_switched)
- self._header_bar.connect_signal(HeaderBar.SIGNAL_CONNECT, self.on_header_bar_connect)
- self._header_bar.connect_signal(HeaderBar.SIGNAL_PLAYPAUSE, self.on_header_bar_playpause)
- self._header_bar.connect_signal(HeaderBar.SIGNAL_SET_VOLUME, self.on_header_bar_set_volume)
- self._panels[Window._PANEL_INDEX_CONNECTION].connect_signal(ConnectionPanel.SIGNAL_PROFILE_CHANGED, self.on_connection_panel_profile_changed)
- self._panels[Window._PANEL_INDEX_PLAYLIST].connect_signal(PlaylistPanel.SIGNAL_CLEAR_PLAYLIST, self.on_playlist_panel_clear_playlist)
- self._panels[Window._PANEL_INDEX_COVER].connect_signal(CoverPanel.SIGNAL_TOGGLE_FULLSCREEN, self.on_cover_panel_toggle_fullscreen)
- self._panels[Window._PANEL_INDEX_COVER].connect_signal(CoverPanel.SIGNAL_SET_SONG, self.on_cover_panel_set_song)
- self._panels[Window._PANEL_INDEX_LIBRARY].connect_signal(LibraryPanel.SIGNAL_UPDATE, self.on_library_panel_update)
- self._panels[Window._PANEL_INDEX_LIBRARY].connect_signal(LibraryPanel.SIGNAL_PLAY, self.on_library_panel_play)
- self._panels[Window._PANEL_INDEX_LIBRARY].connect_signal(LibraryPanel.SIGNAL_ITEM_SIZE_CHANGED, self.on_library_panel_item_size_changed)
- self._panels[Window._PANEL_INDEX_LIBRARY].connect_signal(LibraryPanel.SIGNAL_SORT_ORDER_CHANGED, self.on_library_panel_sort_order_changed)
- self._panels[Window._PANEL_INDEX_LIBRARY].connect_signal(LibraryPanel.SIGNAL_SORT_TYPE_CHANGED, self.on_library_panel_sort_type_changed)
- self._mcg.connect_signal(mcg.MCGClient.SIGNAL_CONNECT, self.on_mcg_connect)
- self._mcg.connect_signal(mcg.MCGClient.SIGNAL_STATUS, self.on_mcg_status)
- self._mcg.connect_signal(mcg.MCGClient.SIGNAL_LOAD_PLAYLIST, self.on_mcg_load_playlist)
- self._mcg.connect_signal(mcg.MCGClient.SIGNAL_LOAD_ALBUMS, self.on_mcg_load_albums)
- self._mcg.connect_signal(mcg.MCGClient.SIGNAL_ERROR, self.on_mcg_error)
- self._settings.connect('changed::'+Application.SETTING_PANEL, self.on_settings_panel_changed)
- self._settings.connect('changed::'+Application.SETTING_ITEM_SIZE, self.on_settings_item_size_changed)
- self._settings.connect('changed::'+Application.SETTING_SORT_ORDER, self.on_settings_sort_order_changed)
- self._settings.connect('changed::'+Application.SETTING_SORT_TYPE, self.on_settings_sort_type_changed)
-
- # Actions
- self.resize(int(self._size[0]), int(self._size[1]))
- if self._maximized:
- self.maximize()
- self.show_all()
- self._infobar.hide()
- self._stack.set_visible_child(self._panels[Window._PANEL_INDEX_CONNECTION])
- self._panels[Window._PANEL_INDEX_CONNECTION].select_profile(self._settings.get_int(Application.SETTING_PROFILE))
-
-
- def on_resize(self, widget, event):
- if not self._maximized:
- self._size = (self.get_allocation().width, self.get_allocation().height)
-
-
- def on_state(self, widget, state):
- self._maximized = (state.new_window_state & Gdk.WindowState.MAXIMIZED > 0)
- self._fullscreen((state.new_window_state & Gdk.WindowState.FULLSCREEN > 0))
- self._settings.set_boolean(Application.SETTING_WINDOW_MAXIMIZED, self._maximized)
-
-
- def on_destroy(self, window):
- self._settings.set_value(Application.SETTING_WINDOW_SIZE, GLib.Variant('ai', self._size))
-
-
- # HeaderBar callbacks
-
- def on_header_bar_stack_switched(self, widget):
- self._save_visible_panel()
-
-
- def on_header_bar_connect(self):
- self._connect()
+ STYLE_CLASS_BG_TEXTURE = 'bg-texture'
+ STYLE_CLASS_NO_BG = 'no-bg'
+ STYLE_CLASS_NO_BORDER = 'no-border'
+ _PANEL_INDEX_CONNECTION = 0
+ _PANEL_INDEX_COVER = 1
+ _PANEL_INDEX_PLAYLIST = 2
+ _PANEL_INDEX_LIBRARY = 3
+
+
+ def __init__(self, app, title, settings):
+ Gtk.Window.__init__(self, title=title, application=app)
+ self._settings = settings
+ self._panels = []
+ self._mcg = mcg.Client()
+ self._size = self._settings.get_value(Application.SETTING_WINDOW_SIZE)
+ self._maximized = self._settings.get_boolean(Application.SETTING_WINDOW_MAXIMIZED)
+ self._fullscreened = False
+
+ # Panels
+ self._panels.append(ConnectionPanel())
+ self._panels.append(CoverPanel())
+ self._panels.append(PlaylistPanel())
+ self._panels.append(LibraryPanel())
+
+ # Widgets
+ self._main_box = Gtk.VBox()
+ self._main_box.get_style_context().add_class(Window.STYLE_CLASS_NO_BG)
+ self.add(self._main_box)
+ # InfoBar
+ self._infobar = InfoBar()
+ self._main_box.pack_start(self._infobar, False, True, 0)
+ # Stack
+ self._stack = Gtk.Stack()
+ for panel in self._panels:
+ self._stack.add_titled(panel, panel.get_name(), panel.get_title())
+ self._stack.set_homogeneous(True)
+ self._main_box.pack_end(self._stack, True, True, 0)
+ # Header
+ self._header_bar = HeaderBar(self._stack)
+ self.set_titlebar(self._header_bar)
+
+ # Properties
+ self._header_bar.set_sensitive(False, False)
+ styleProvider = Gtk.CssProvider()
+ styleProvider.load_from_data(b"""
+ GtkWidget.bg-texture {
+ box-shadow:inset 4px 4px 10px rgba(0,0,0,0.3);
+ background-image:url('data/noise-texture.png');
+ }
+ GtkWidget.no-bg {
+ background:none;
+ }
+ GtkWidget.no-border {
+ border:none;
+ }
+ GtkIconView.cell:selected,
+ GtkIconView.cell:selected:focus {
+ background-color:@theme_selected_bg_color;
+ }
+ """)
+ self.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), styleProvider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
+ self.get_style_context().add_class(Window.STYLE_CLASS_BG_TEXTURE)
+ self._panels[Window._PANEL_INDEX_PLAYLIST].set_item_size(self._settings.get_int(Application.SETTING_ITEM_SIZE))
+ self._panels[Window._PANEL_INDEX_LIBRARY].set_item_size(self._settings.get_int(Application.SETTING_ITEM_SIZE))
+ self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_order(self._settings.get_string(Application.SETTING_SORT_ORDER))
+ self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_type(self._settings.get_boolean(Application.SETTING_SORT_TYPE))
+
+ # Signals
+ self.connect('size-allocate', self.on_resize)
+ self.connect('window-state-event', self.on_state)
+ self.connect('destroy', self.on_destroy)
+ self._header_bar.connect_signal(HeaderBar.SIGNAL_STACK_SWITCHED, self.on_header_bar_stack_switched)
+ self._header_bar.connect_signal(HeaderBar.SIGNAL_CONNECT, self.on_header_bar_connect)
+ self._header_bar.connect_signal(HeaderBar.SIGNAL_PLAYPAUSE, self.on_header_bar_playpause)
+ self._header_bar.connect_signal(HeaderBar.SIGNAL_SET_VOLUME, self.on_header_bar_set_volume)
+ self._panels[Window._PANEL_INDEX_CONNECTION].connect_signal(ConnectionPanel.SIGNAL_PROFILE_CHANGED, self.on_connection_panel_profile_changed)
+ self._panels[Window._PANEL_INDEX_PLAYLIST].connect_signal(PlaylistPanel.SIGNAL_CLEAR_PLAYLIST, self.on_playlist_panel_clear_playlist)
+ self._panels[Window._PANEL_INDEX_COVER].connect_signal(CoverPanel.SIGNAL_TOGGLE_FULLSCREEN, self.on_cover_panel_toggle_fullscreen)
+ self._panels[Window._PANEL_INDEX_COVER].connect_signal(CoverPanel.SIGNAL_SET_SONG, self.on_cover_panel_set_song)
+ self._panels[Window._PANEL_INDEX_LIBRARY].connect_signal(LibraryPanel.SIGNAL_UPDATE, self.on_library_panel_update)
+ self._panels[Window._PANEL_INDEX_LIBRARY].connect_signal(LibraryPanel.SIGNAL_PLAY, self.on_library_panel_play)
+ self._panels[Window._PANEL_INDEX_LIBRARY].connect_signal(LibraryPanel.SIGNAL_ITEM_SIZE_CHANGED, self.on_library_panel_item_size_changed)
+ self._panels[Window._PANEL_INDEX_LIBRARY].connect_signal(LibraryPanel.SIGNAL_SORT_ORDER_CHANGED, self.on_library_panel_sort_order_changed)
+ self._panels[Window._PANEL_INDEX_LIBRARY].connect_signal(LibraryPanel.SIGNAL_SORT_TYPE_CHANGED, self.on_library_panel_sort_type_changed)
+ self._mcg.connect_signal(mcg.Client.SIGNAL_CONNECTION, self.on_mcg_connect)
+ self._mcg.connect_signal(mcg.Client.SIGNAL_STATUS, self.on_mcg_status)
+ self._mcg.connect_signal(mcg.Client.SIGNAL_LOAD_PLAYLIST, self.on_mcg_load_playlist)
+ self._mcg.connect_signal(mcg.Client.SIGNAL_LOAD_ALBUMS, self.on_mcg_load_albums)
+ self._mcg.connect_signal(mcg.Client.SIGNAL_ERROR, self.on_mcg_error)
+ self._settings.connect('changed::'+Application.SETTING_PANEL, self.on_settings_panel_changed)
+ self._settings.connect('changed::'+Application.SETTING_ITEM_SIZE, self.on_settings_item_size_changed)
+ self._settings.connect('changed::'+Application.SETTING_SORT_ORDER, self.on_settings_sort_order_changed)
+ self._settings.connect('changed::'+Application.SETTING_SORT_TYPE, self.on_settings_sort_type_changed)
+
+ # Actions
+ self.resize(int(self._size[0]), int(self._size[1]))
+ if self._maximized:
+ self.maximize()
+ self.show_all()
+ self._infobar.hide()
+ self._stack.set_visible_child(self._panels[Window._PANEL_INDEX_CONNECTION])
+ self._panels[Window._PANEL_INDEX_CONNECTION].select_profile(self._settings.get_int(Application.SETTING_PROFILE))
+
+
+ def on_resize(self, widget, event):
+ if not self._maximized:
+ self._size = (self.get_allocation().width, self.get_allocation().height)
+
+
+ def on_state(self, widget, state):
+ self._maximized = (state.new_window_state & Gdk.WindowState.MAXIMIZED > 0)
+ self._fullscreen((state.new_window_state & Gdk.WindowState.FULLSCREEN > 0))
+ self._settings.set_boolean(Application.SETTING_WINDOW_MAXIMIZED, self._maximized)
+
+
+ def on_destroy(self, window):
+ self._settings.set_value(Application.SETTING_WINDOW_SIZE, GLib.Variant('ai', self._size))
+
+
+ # HeaderBar callbacks
+
+ def on_header_bar_stack_switched(self, widget):
+ self._save_visible_panel()
+
+
+ def on_header_bar_connect(self):
+ self._connect()
- def on_header_bar_playpause(self):
- self._mcg.playpause()
+ def on_header_bar_playpause(self):
+ self._mcg.playpause()
+ self._mcg.get_status()
- def on_header_bar_set_volume(self, volume):
- self._mcg.set_volume(volume)
+ def on_header_bar_set_volume(self, volume):
+ self._mcg.set_volume(volume)
- # Panel callbacks
+ # Panel callbacks
- def on_connection_panel_profile_changed(self, index, profile):
- self._settings.set_int(Application.SETTING_PROFILE, index)
- if ConnectionPanel.TAG_AUTOCONNECT in profile.get_tags():
- self._connect()
+ def on_connection_panel_profile_changed(self, index, profile):
+ self._settings.set_int(Application.SETTING_PROFILE, index)
+ if ConnectionPanel.TAG_AUTOCONNECT in profile.get_tags():
+ self._connect()
- def on_playlist_panel_clear_playlist(self):
- self._mcg.clear_playlist()
+ def on_playlist_panel_clear_playlist(self):
+ self._mcg.clear_playlist()
- def on_cover_panel_toggle_fullscreen(self):
- if not self._fullscreened:
- self.fullscreen()
- else:
- self.unfullscreen()
+ def on_cover_panel_toggle_fullscreen(self):
+ if not self._fullscreened:
+ self.fullscreen()
+ else:
+ self.unfullscreen()
- def on_cover_panel_set_song(self, pos, time):
- self._mcg.seek(pos, time)
+ def on_cover_panel_set_song(self, pos, time):
+ self._mcg.seek(pos, time)
- def on_library_panel_update(self):
- self._mcg.update()
+ def on_library_panel_update(self):
+ self._mcg.update()
- def on_library_panel_play(self, album):
- self._mcg.play_album(album)
+ def on_library_panel_play(self, album):
+ self._mcg.play_album(album)
- def on_library_panel_item_size_changed(self, size):
- self._panels[Window._PANEL_INDEX_PLAYLIST].set_item_size(size)
- self._settings.set_int(Application.SETTING_ITEM_SIZE, self._panels[Window._PANEL_INDEX_LIBRARY].get_item_size())
+ def on_library_panel_item_size_changed(self, size):
+ self._panels[Window._PANEL_INDEX_PLAYLIST].set_item_size(size)
+ self._settings.set_int(Application.SETTING_ITEM_SIZE, self._panels[Window._PANEL_INDEX_LIBRARY].get_item_size())
- def on_library_panel_sort_order_changed(self, sort_order):
- self._settings.set_string(Application.SETTING_SORT_ORDER, self._panels[Window._PANEL_INDEX_LIBRARY].get_sort_order())
+ def on_library_panel_sort_order_changed(self, sort_order):
+ self._settings.set_string(Application.SETTING_SORT_ORDER, self._panels[Window._PANEL_INDEX_LIBRARY].get_sort_order())
- def on_library_panel_sort_type_changed(self, sort_type):
- self._settings.set_boolean(Application.SETTING_SORT_TYPE, self._panels[Window._PANEL_INDEX_LIBRARY].get_sort_type())
+ def on_library_panel_sort_type_changed(self, sort_type):
+ self._settings.set_boolean(Application.SETTING_SORT_TYPE, self._panels[Window._PANEL_INDEX_LIBRARY].get_sort_type())
- # MCG callbacks
+ # MCG callbacks
- def on_mcg_connect(self, connected, error):
- if connected:
- GObject.idle_add(self._connect_connected)
- self._mcg.load_playlist()
- self._mcg.load_albums()
- self._mcg.get_status()
- else:
- if error:
- GObject.idle_add(self._show_error, str(error))
- GObject.idle_add(self._connect_disconnected)
+ def on_mcg_connect(self, connected):
+ if connected:
+ GObject.idle_add(self._connect_connected)
+ self._mcg.load_playlist()
+ self._mcg.load_albums()
+ self._mcg.get_status()
+ else:
+ # if error:
+ # GObject.idle_add(self._show_error, str(error))
+ GObject.idle_add(self._connect_disconnected)
- def on_mcg_status(self, state, album, pos, time, volume, error):
- # Album
- if album:
- GObject.idle_add(self._panels[Window._PANEL_INDEX_COVER].set_album, album)
- # State
- if state == 'play':
- GObject.idle_add(self._header_bar.set_play)
- GObject.idle_add(self._panels[Window._PANEL_INDEX_COVER].set_play, pos, time)
- elif state == 'pause' or state == 'stop':
- GObject.idle_add(self._header_bar.set_pause)
- GObject.idle_add(self._panels[Window._PANEL_INDEX_COVER].set_pause)
- # Volume
- GObject.idle_add(self._header_bar.set_volume, volume)
- # Error
- if error is None:
- self._infobar.hide()
- else:
- self._show_error(error)
+ def on_mcg_status(self, state, album, pos, time, volume, error):
+ # Album
+ if album:
+ GObject.idle_add(self._panels[Window._PANEL_INDEX_COVER].set_album, album)
+ # State
+ if state == 'play':
+ GObject.idle_add(self._header_bar.set_play)
+ GObject.idle_add(self._panels[Window._PANEL_INDEX_COVER].set_play, pos, time)
+ elif state == 'pause' or state == 'stop':
+ GObject.idle_add(self._header_bar.set_pause)
+ GObject.idle_add(self._panels[Window._PANEL_INDEX_COVER].set_pause)
+ # Volume
+ GObject.idle_add(self._header_bar.set_volume, volume)
+ # Error
+ if error is None:
+ self._infobar.hide()
+ else:
+ self._show_error(error)
- def on_mcg_load_playlist(self, playlist, error):
- self._panels[self._PANEL_INDEX_PLAYLIST].set_playlist(self._panels[self._PANEL_INDEX_CONNECTION].get_host(), playlist)
+ def on_mcg_load_playlist(self, playlist):
+ self._panels[self._PANEL_INDEX_PLAYLIST].set_playlist(self._panels[self._PANEL_INDEX_CONNECTION].get_host(), playlist)
- def on_mcg_load_albums(self, albums, error):
- self._panels[self._PANEL_INDEX_LIBRARY].set_albums(self._panels[self._PANEL_INDEX_CONNECTION].get_host(), albums)
+ def on_mcg_load_albums(self, albums):
+ self._panels[self._PANEL_INDEX_LIBRARY].set_albums(self._panels[self._PANEL_INDEX_CONNECTION].get_host(), albums)
- def on_mcg_error(self, error):
- GObject.idle_add(self._show_error, str(error))
+ def on_mcg_error(self, error):
+ GObject.idle_add(self._show_error, str(error))
- # Settings callbacks
+ # Settings callbacks
- def on_settings_panel_changed(self, settings, key):
- panel_index = settings.get_int(key)
- self._stack.set_visible_child(self._panels[panel_index])
+ def on_settings_panel_changed(self, settings, key):
+ panel_index = settings.get_int(key)
+ self._stack.set_visible_child(self._panels[panel_index])
- def on_settings_item_size_changed(self, settings, key):
- size = settings.get_int(key)
- self._panels[Window._PANEL_INDEX_PLAYLIST].set_item_size(size)
- self._panels[Window._PANEL_INDEX_LIBRARY].set_item_size(size)
+ def on_settings_item_size_changed(self, settings, key):
+ size = settings.get_int(key)
+ self._panels[Window._PANEL_INDEX_PLAYLIST].set_item_size(size)
+ self._panels[Window._PANEL_INDEX_LIBRARY].set_item_size(size)
- def on_settings_sort_order_changed(self, settings, key):
- sort_order = settings.get_string(key)
- self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_order(sort_order)
+ def on_settings_sort_order_changed(self, settings, key):
+ sort_order = settings.get_string(key)
+ self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_order(sort_order)
- def on_settings_sort_type_changed(self, settings, key):
- sort_type = settings.get_boolean(key)
- self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_type(sort_type)
+ def on_settings_sort_type_changed(self, settings, key):
+ sort_type = settings.get_boolean(key)
+ self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_type(sort_type)
- # Private methods
+ # Private methods
- def _connect(self):
- connection_panel = self._panels[Window._PANEL_INDEX_CONNECTION]
- connection_panel.set_sensitive(False)
- self._header_bar.set_sensitive(False, True)
- if self._mcg.is_connected():
- self._mcg.disconnect()
- else:
- host = connection_panel.get_host()
- port = connection_panel.get_port()
- password = connection_panel.get_password()
- image_dir = connection_panel.get_image_dir()
- self._mcg.connect(host, port, password, image_dir)
+ def _connect(self):
+ connection_panel = self._panels[Window._PANEL_INDEX_CONNECTION]
+ connection_panel.set_sensitive(False)
+ self._header_bar.set_sensitive(False, True)
+ if self._mcg.is_connected():
+ self._mcg.disconnect()
+ else:
+ host = connection_panel.get_host()
+ port = connection_panel.get_port()
+ password = connection_panel.get_password()
+ image_dir = connection_panel.get_image_dir()
+ self._mcg.connect(host, port, password, image_dir)
- def _connect_connected(self):
- self._header_bar.connected()
- self._header_bar.set_sensitive(True, False)
- self._stack.set_visible_child(self._panels[self._settings.get_int(Application.SETTING_PANEL)])
-
+ def _connect_connected(self):
+ self._header_bar.connected()
+ self._header_bar.set_sensitive(True, False)
+ self._stack.set_visible_child(self._panels[self._settings.get_int(Application.SETTING_PANEL)])
+
- def _connect_disconnected(self):
- self._panels[Window._PANEL_INDEX_PLAYLIST].stop_threads();
- self._panels[Window._PANEL_INDEX_LIBRARY].stop_threads();
- self._header_bar.disconnected()
- self._header_bar.set_sensitive(False, False)
- self._save_visible_panel()
- self._stack.set_visible_child(self._panels[Window._PANEL_INDEX_CONNECTION])
- self._panels[Window._PANEL_INDEX_CONNECTION].set_sensitive(True)
-
-
- def _fullscreen(self, fullscreened_new):
- if fullscreened_new != self._fullscreened:
- self._fullscreened = fullscreened_new
- if self._fullscreened:
- self._header_bar.hide()
- self._panels[Window._PANEL_INDEX_COVER].set_fullscreen(True)
- else:
- self._header_bar.show()
- self._panels[Window._PANEL_INDEX_COVER].set_fullscreen(False)
+ def _connect_disconnected(self):
+ self._panels[Window._PANEL_INDEX_PLAYLIST].stop_threads();
+ self._panels[Window._PANEL_INDEX_LIBRARY].stop_threads();
+ self._header_bar.disconnected()
+ self._header_bar.set_sensitive(False, False)
+ self._save_visible_panel()
+ self._stack.set_visible_child(self._panels[Window._PANEL_INDEX_CONNECTION])
+ self._panels[Window._PANEL_INDEX_CONNECTION].set_sensitive(True)
+
+
+ def _fullscreen(self, fullscreened_new):
+ if fullscreened_new != self._fullscreened:
+ self._fullscreened = fullscreened_new
+ if self._fullscreened:
+ self._header_bar.hide()
+ self._panels[Window._PANEL_INDEX_COVER].set_fullscreen(True)
+ else:
+ self._header_bar.show()
+ self._panels[Window._PANEL_INDEX_COVER].set_fullscreen(False)
- def _save_visible_panel(self):
- panel_index_selected = self._panels.index(self._stack.get_visible_child())
- if(panel_index_selected > 0):
- self._settings.set_int(Application.SETTING_PANEL, panel_index_selected)
+ def _save_visible_panel(self):
+ panel_index_selected = self._panels.index(self._stack.get_visible_child())
+ if(panel_index_selected > 0):
+ self._settings.set_int(Application.SETTING_PANEL, panel_index_selected)
- def _show_error(self, message):
- self._infobar.show_error(message)
- self._infobar.show()
+ def _show_error(self, message):
+ self._infobar.show_error(message)
+ self._infobar.show()
-class HeaderBar(mcg.MCGBase, Gtk.HeaderBar):
- SIGNAL_STACK_SWITCHED = 'stack-switched'
- SIGNAL_CONNECT = 'connect'
- SIGNAL_PLAYPAUSE = 'playpause'
- SIGNAL_SET_VOLUME = 'set-volume'
+class HeaderBar(mcg.Base, Gtk.HeaderBar):
+ SIGNAL_STACK_SWITCHED = 'stack-switched'
+ SIGNAL_CONNECT = 'connect'
+ SIGNAL_PLAYPAUSE = 'playpause'
+ SIGNAL_SET_VOLUME = 'set-volume'
- def __init__(self, stack):
- mcg.MCGBase.__init__(self)
- Gtk.HeaderBar.__init__(self)
- self._stack = stack
- self._buttons = {}
- self._button_handlers = {}
- self._changing_volume = False
- self._setting_volume = False
+ def __init__(self, stack):
+ mcg.Base.__init__(self)
+ Gtk.HeaderBar.__init__(self)
+ self._stack = stack
+ self._buttons = {}
+ self._button_handlers = {}
+ self._changing_volume = False
+ self._setting_volume = False
- # Widgets
- # StackSwitcher
- self._stack_switcher = StackSwitcher()
- self._stack_switcher.set_stack(self._stack)
- self.set_custom_title(self._stack_switcher)
- # Buttons left
- self._left_toolbar = Gtk.Toolbar()
- self._left_toolbar.set_show_arrow(False)
- self._left_toolbar.get_style_context().add_class(Window.STYLE_CLASS_NO_BG)
- self.pack_start(self._left_toolbar)
- # Buttons left: Connection
- self._buttons[HeaderBar.SIGNAL_CONNECT] = Gtk.ToggleToolButton.new_from_stock(Gtk.STOCK_DISCONNECT)
- self._left_toolbar.add(self._buttons[HeaderBar.SIGNAL_CONNECT])
- # Buttons left: Separator
- self._left_toolbar.add(Gtk.SeparatorToolItem())
- # Buttons left: Playback
- self._buttons[HeaderBar.SIGNAL_PLAYPAUSE] = Gtk.ToggleToolButton.new_from_stock(Gtk.STOCK_MEDIA_PAUSE)
- self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].set_sensitive(False)
- self._left_toolbar.add(self._buttons[HeaderBar.SIGNAL_PLAYPAUSE])
- # Buttons right
- self._right_toolbar = Gtk.Toolbar()
- self._right_toolbar.set_show_arrow(False)
- self._right_toolbar.get_style_context().add_class(Window.STYLE_CLASS_NO_BG)
- self.pack_end(self._right_toolbar)
- # Buttons right: Volume
- item = Gtk.ToolItem()
- self._buttons[HeaderBar.SIGNAL_SET_VOLUME] = Gtk.VolumeButton()
- self._buttons[HeaderBar.SIGNAL_SET_VOLUME].set_sensitive(False)
- item.add(self._buttons[HeaderBar.SIGNAL_SET_VOLUME])
- self._right_toolbar.add(item)
-
- # Properties
- self.set_show_close_button(True)
-
- # Signals
- self._stack_switcher.connect_signal(StackSwitcher.SIGNAL_STACK_SWITCHED, self.on_stack_switched)
- self._button_handlers[HeaderBar.SIGNAL_CONNECT] = self._buttons[HeaderBar.SIGNAL_CONNECT].connect('toggled', self._callback_from_widget, self.SIGNAL_CONNECT)
- self._button_handlers[HeaderBar.SIGNAL_PLAYPAUSE] = self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].connect('toggled', self._callback_from_widget, self.SIGNAL_PLAYPAUSE)
- self._buttons[HeaderBar.SIGNAL_SET_VOLUME].connect('value-changed', self.on_volume_changed)
- self._buttons[HeaderBar.SIGNAL_SET_VOLUME].connect('button-press-event', self.on_volume_set_active, True)
- self._buttons[HeaderBar.SIGNAL_SET_VOLUME].connect('button-release-event', self.on_volume_set_active, False)
+ # Widgets
+ # StackSwitcher
+ self._stack_switcher = StackSwitcher()
+ self._stack_switcher.set_stack(self._stack)
+ self.set_custom_title(self._stack_switcher)
+ # Buttons left
+ self._left_toolbar = Gtk.Toolbar()
+ self._left_toolbar.set_show_arrow(False)
+ self._left_toolbar.get_style_context().add_class(Window.STYLE_CLASS_NO_BG)
+ self.pack_start(self._left_toolbar)
+ # Buttons left: Connection
+ self._buttons[HeaderBar.SIGNAL_CONNECT] = Gtk.ToggleToolButton.new_from_stock(Gtk.STOCK_DISCONNECT)
+ self._left_toolbar.add(self._buttons[HeaderBar.SIGNAL_CONNECT])
+ # Buttons left: Separator
+ self._left_toolbar.add(Gtk.SeparatorToolItem())
+ # Buttons left: Playback
+ self._buttons[HeaderBar.SIGNAL_PLAYPAUSE] = Gtk.ToggleToolButton.new_from_stock(Gtk.STOCK_MEDIA_PLAY)
+ self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].set_sensitive(False)
+ self._left_toolbar.add(self._buttons[HeaderBar.SIGNAL_PLAYPAUSE])
+ # Buttons right
+ self._right_toolbar = Gtk.Toolbar()
+ self._right_toolbar.set_show_arrow(False)
+ self._right_toolbar.get_style_context().add_class(Window.STYLE_CLASS_NO_BG)
+ self.pack_end(self._right_toolbar)
+ # Buttons right: Volume
+ item = Gtk.ToolItem()
+ self._buttons[HeaderBar.SIGNAL_SET_VOLUME] = Gtk.VolumeButton()
+ self._buttons[HeaderBar.SIGNAL_SET_VOLUME].set_sensitive(False)
+ item.add(self._buttons[HeaderBar.SIGNAL_SET_VOLUME])
+ self._right_toolbar.add(item)
+
+ # Properties
+ self.set_show_close_button(True)
+
+ # Signals
+ self._stack_switcher.connect_signal(StackSwitcher.SIGNAL_STACK_SWITCHED, self.on_stack_switched)
+ self._button_handlers[HeaderBar.SIGNAL_CONNECT] = self._buttons[HeaderBar.SIGNAL_CONNECT].connect('toggled', self._callback_from_widget, self.SIGNAL_CONNECT)
+ self._button_handlers[HeaderBar.SIGNAL_PLAYPAUSE] = self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].connect('toggled', self._callback_from_widget, self.SIGNAL_PLAYPAUSE)
+ self._buttons[HeaderBar.SIGNAL_SET_VOLUME].connect('value-changed', self.on_volume_changed)
+ self._buttons[HeaderBar.SIGNAL_SET_VOLUME].connect('button-press-event', self.on_volume_set_active, True)
+ self._buttons[HeaderBar.SIGNAL_SET_VOLUME].connect('button-release-event', self.on_volume_set_active, False)
- def set_sensitive(self, sensitive, connecting):
- for button_signal in self._buttons:
- self._buttons[button_signal].set_sensitive(sensitive)
- self._stack_switcher.set_sensitive(sensitive)
- self._buttons[HeaderBar.SIGNAL_CONNECT].set_sensitive(not connecting)
+ def set_sensitive(self, sensitive, connecting):
+ for button_signal in self._buttons:
+ self._buttons[button_signal].set_sensitive(sensitive)
+ self._stack_switcher.set_sensitive(sensitive)
+ self._buttons[HeaderBar.SIGNAL_CONNECT].set_sensitive(not connecting)
- def on_stack_switched(self, widget):
- self._callback(HeaderBar.SIGNAL_STACK_SWITCHED, widget)
-
+ def on_stack_switched(self, widget):
+ self._callback(HeaderBar.SIGNAL_STACK_SWITCHED, widget)
+
- def on_volume_changed(self, widget, value):
- if not self._setting_volume:
- self._callback(self.SIGNAL_SET_VOLUME, int(value*100))
+ def on_volume_changed(self, widget, value):
+ if not self._setting_volume:
+ self._callback(self.SIGNAL_SET_VOLUME, int(value*100))
- def on_volume_set_active(self, widget, event, active):
- self._changing_volume = active
+ def on_volume_set_active(self, widget, event, active):
+ self._changing_volume = active
- def connected(self):
- self._buttons[HeaderBar.SIGNAL_CONNECT].set_stock_id(Gtk.STOCK_CONNECT)
- with self._buttons[HeaderBar.SIGNAL_CONNECT].handler_block(self._button_handlers[HeaderBar.SIGNAL_CONNECT]):
- self._buttons[HeaderBar.SIGNAL_CONNECT].set_active(True)
+ def connected(self):
+ self._buttons[HeaderBar.SIGNAL_CONNECT].set_stock_id(Gtk.STOCK_CONNECT)
+ with self._buttons[HeaderBar.SIGNAL_CONNECT].handler_block(self._button_handlers[HeaderBar.SIGNAL_CONNECT]):
+ self._buttons[HeaderBar.SIGNAL_CONNECT].set_active(True)
- def disconnected(self):
- self._buttons[HeaderBar.SIGNAL_CONNECT].set_stock_id(Gtk.STOCK_DISCONNECT)
- with self._buttons[HeaderBar.SIGNAL_CONNECT].handler_block(self._button_handlers[HeaderBar.SIGNAL_CONNECT]):
- self._buttons[HeaderBar.SIGNAL_CONNECT].set_active(False)
+ def disconnected(self):
+ self._buttons[HeaderBar.SIGNAL_CONNECT].set_stock_id(Gtk.STOCK_DISCONNECT)
+ with self._buttons[HeaderBar.SIGNAL_CONNECT].handler_block(self._button_handlers[HeaderBar.SIGNAL_CONNECT]):
+ self._buttons[HeaderBar.SIGNAL_CONNECT].set_active(False)
- def set_play(self):
- self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].set_stock_id(Gtk.STOCK_MEDIA_PLAY)
- with self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].handler_block(self._button_handlers[HeaderBar.SIGNAL_PLAYPAUSE]):
- self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].set_active(True)
+ def set_play(self):
+ #self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].set_stock_id(Gtk.STOCK_MEDIA_PLAY)
+ with self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].handler_block(self._button_handlers[HeaderBar.SIGNAL_PLAYPAUSE]):
+ self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].set_active(True)
- def set_pause(self):
- self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].set_stock_id(Gtk.STOCK_MEDIA_PAUSE)
- with self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].handler_block(self._button_handlers[HeaderBar.SIGNAL_PLAYPAUSE]):
- self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].set_active(False)
-
-
- def set_volume(self, volume):
- if not self._changing_volume:
- self._setting_volume = True
- self._buttons[HeaderBar.SIGNAL_SET_VOLUME].set_value(volume / 100)
- self._setting_volume = False
-
-
- def _callback_from_widget(self, widget, signal, *data):
- self._callback(signal, *data)
+ def set_pause(self):
+ #self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].set_stock_id(Gtk.STOCK_MEDIA_PAUSE)
+ with self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].handler_block(self._button_handlers[HeaderBar.SIGNAL_PLAYPAUSE]):
+ self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].set_active(False)
+
+
+ def set_volume(self, volume):
+ if not self._changing_volume:
+ self._setting_volume = True
+ self._buttons[HeaderBar.SIGNAL_SET_VOLUME].set_value(volume / 100)
+ self._setting_volume = False
+
+
+ def _callback_from_widget(self, widget, signal, *data):
+ self._callback(signal, *data)
class InfoBar(Gtk.InfoBar):
- _RESPONSE_CLOSE = 1
+ _RESPONSE_CLOSE = 1
- def __init__(self):
- Gtk.InfoBar.__init__(self)
-
- # Widgets
- self.add_button(Gtk.STOCK_CLOSE, InfoBar._RESPONSE_CLOSE)
- self._message_label = Gtk.Label()
- self._message_label.show()
- self.get_content_area().add(self._message_label)
+ def __init__(self):
+ Gtk.InfoBar.__init__(self)
+
+ # Widgets
+ self.add_button(Gtk.STOCK_CLOSE, InfoBar._RESPONSE_CLOSE)
+ self._message_label = Gtk.Label()
+ self._message_label.show()
+ self.get_content_area().add(self._message_label)
- # Signals
- self.connect('close', self.on_response, InfoBar._RESPONSE_CLOSE)
- self.connect('response', self.on_response)
+ # Signals
+ self.connect('close', self.on_response, InfoBar._RESPONSE_CLOSE)
+ self.connect('response', self.on_response)
- def on_response(self, widget, response):
- if response == InfoBar._RESPONSE_CLOSE:
- self.hide()
+ def on_response(self, widget, response):
+ if response == InfoBar._RESPONSE_CLOSE:
+ self.hide()
- def show_error(self, message):
- self.set_message_type(Gtk.MessageType.ERROR)
- self._message_label.set_text(message)
+ def show_error(self, message):
+ self.set_message_type(Gtk.MessageType.ERROR)
+ self._message_label.set_text(message)
-class Panel(mcg.MCGBase):
+class Panel(mcg.Base):
- def __init__(self):
- mcg.MCGBase.__init__(self)
+ def __init__(self):
+ mcg.Base.__init__(self)
- def get_name(self):
- raise NotImplementedError()
+ def get_name(self):
+ raise NotImplementedError()
- def get_title(self):
- raise NotImplementedError()
+ def get_title(self):
+ raise NotImplementedError()
class ConnectionPanel(Panel, Gtk.HBox):
- SIGNAL_PROFILE_CHANGED = 'profile-changed'
- TAG_AUTOCONNECT = 'autoconnect'
+ SIGNAL_PROFILE_CHANGED = 'profile-changed'
+ TAG_AUTOCONNECT = 'autoconnect'
- def __init__(self):
- Panel.__init__(self)
- Gtk.HBox.__init__(self)
- self._profile_config = mcg.MCGProfileConfig()
- self._profiles = Gtk.ListStore(str)
- self._profile = None
+ def __init__(self):
+ Panel.__init__(self)
+ Gtk.HBox.__init__(self)
+ self._profile_config = mcg.MCGProfileConfig()
+ self._profiles = Gtk.ListStore(str)
+ self._profile = None
- # Widgets
- vbox = Gtk.VBox()
- self.pack_start(vbox, True, False, 0)
- self._table = Gtk.Table(6, 2, False)
- vbox.pack_start(self._table, True, False, 0)
- # Profile
- profile_box = Gtk.HBox()
- self._table.attach(profile_box, 0, 2, 0, 1)
- # Profile Selection
- self._profile_combo = Gtk.ComboBox.new_with_model(self._profiles)
- self._profile_combo.set_entry_text_column(0)
- renderer = Gtk.CellRendererText()
- self._profile_combo.pack_start(renderer, True)
- self._profile_combo.add_attribute(renderer, "text", 0)
- profile_box.pack_start(self._profile_combo, True, True, 0)
- # Profile Management
- profile_button_box = Gtk.HBox()
- profile_box.pack_end(profile_button_box, False, True, 0)
- # New Profile
- self._profile_new_button = Gtk.Button()
- self._profile_new_button.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.BUTTON))
- profile_button_box.add(self._profile_new_button)
- # Delete Profile
- self._profile_delete_button = Gtk.Button()
- self._profile_delete_button.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_DELETE, Gtk.IconSize.BUTTON))
- profile_button_box.add(self._profile_delete_button)
- # Host
- host_label = Gtk.Label("Host:")
- host_label.set_alignment(0, 0.5)
- self._table.attach(host_label, 0, 1, 1, 2)
- self._host_entry = Gtk.Entry()
- self._host_entry.set_text("localhost")
- self._table.attach(self._host_entry, 1, 2, 1, 2)
- # Port
- port_label = Gtk.Label("Port:")
- port_label.set_alignment(0, 0.5)
- self._table.attach(port_label, 0, 1, 2, 3)
- adjustment = Gtk.Adjustment(6600, 1024, 9999, 1, 10, 10)
- self._port_spinner = Gtk.SpinButton()
- self._port_spinner.set_adjustment(adjustment)
- self._table.attach(self._port_spinner, 1, 2, 2, 3)
- # Passwort
- password_label = Gtk.Label("Password:")
- password_label.set_alignment(0, 0.5)
- self._table.attach(password_label, 0, 1, 3, 4)
- self._password_entry = Gtk.Entry()
- self._table.attach(self._password_entry, 1, 2, 3, 4)
- # Image dir
- image_dir_label = Gtk.Label("Image Dir:")
- image_dir_label.set_alignment(0, 0.5)
- self._table.attach(image_dir_label, 0, 1, 4, 5)
- self._image_dir_entry = Gtk.Entry()
- self._table.attach(self._image_dir_entry, 1, 2, 4, 5)
- # Autoconnect
- self._autoconnect_button = Gtk.CheckButton("Autoconnect")
- self._table.attach(self._autoconnect_button, 1, 2, 5, 6)
+ # Widgets
+ vbox = Gtk.VBox()
+ self.pack_start(vbox, True, False, 0)
+ self._table = Gtk.Table(6, 2, False)
+ vbox.pack_start(self._table, True, False, 0)
+ # Profile
+ profile_box = Gtk.HBox()
+ self._table.attach(profile_box, 0, 2, 0, 1)
+ # Profile Selection
+ self._profile_combo = Gtk.ComboBox.new_with_model(self._profiles)
+ self._profile_combo.set_entry_text_column(0)
+ renderer = Gtk.CellRendererText()
+ self._profile_combo.pack_start(renderer, True)
+ self._profile_combo.add_attribute(renderer, "text", 0)
+ profile_box.pack_start(self._profile_combo, True, True, 0)
+ # Profile Management
+ profile_button_box = Gtk.HBox()
+ profile_box.pack_end(profile_button_box, False, True, 0)
+ # New Profile
+ self._profile_new_button = Gtk.Button()
+ self._profile_new_button.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.BUTTON))
+ profile_button_box.add(self._profile_new_button)
+ # Delete Profile
+ self._profile_delete_button = Gtk.Button()
+ self._profile_delete_button.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_DELETE, Gtk.IconSize.BUTTON))
+ profile_button_box.add(self._profile_delete_button)
+ # Host
+ host_label = Gtk.Label("Host:")
+ host_label.set_alignment(0, 0.5)
+ self._table.attach(host_label, 0, 1, 1, 2)
+ self._host_entry = Gtk.Entry()
+ self._host_entry.set_text("localhost")
+ self._table.attach(self._host_entry, 1, 2, 1, 2)
+ # Port
+ port_label = Gtk.Label("Port:")
+ port_label.set_alignment(0, 0.5)
+ self._table.attach(port_label, 0, 1, 2, 3)
+ adjustment = Gtk.Adjustment(6600, 1024, 9999, 1, 10, 10)
+ self._port_spinner = Gtk.SpinButton()
+ self._port_spinner.set_adjustment(adjustment)
+ self._table.attach(self._port_spinner, 1, 2, 2, 3)
+ # Passwort
+ password_label = Gtk.Label("Password:")
+ password_label.set_alignment(0, 0.5)
+ self._table.attach(password_label, 0, 1, 3, 4)
+ self._password_entry = Gtk.Entry()
+ self._table.attach(self._password_entry, 1, 2, 3, 4)
+ # Image dir
+ image_dir_label = Gtk.Label("Image Dir:")
+ image_dir_label.set_alignment(0, 0.5)
+ self._table.attach(image_dir_label, 0, 1, 4, 5)
+ self._image_dir_entry = Gtk.Entry()
+ self._table.attach(self._image_dir_entry, 1, 2, 4, 5)
+ # Autoconnect
+ self._autoconnect_button = Gtk.CheckButton("Autoconnect")
+ self._table.attach(self._autoconnect_button, 1, 2, 5, 6)
- # Signals
- self._profiles.connect('row-changed', self.on_profiles_changed)
- self._profiles.connect('row-inserted', self.on_profiles_changed)
- self._profiles.connect('row-deleted', self.on_profiles_changed)
- self._profile_combo.connect("changed", self.on_profile_combo_changed)
- self._profile_new_button.connect('clicked', self.on_profile_new_clicked)
- self._profile_delete_button.connect('clicked', self.on_profile_delete_clicked)
- self._host_entry.connect('focus-out-event', self.on_host_entry_outfocused)
- self._port_spinner.connect('value-changed', self.on_port_spinner_value_changed)
- self._password_entry.connect('focus-out-event', self.on_password_entry_outfocused)
- self._image_dir_entry.connect('focus-out-event', self.on_image_dir_entry_outfocused)
- self._autoconnect_button.connect('toggled', self.on_autoconnect_button_toggled)
+ # Signals
+ self._profiles.connect('row-changed', self.on_profiles_changed)
+ self._profiles.connect('row-inserted', self.on_profiles_changed)
+ self._profiles.connect('row-deleted', self.on_profiles_changed)
+ self._profile_combo.connect("changed", self.on_profile_combo_changed)
+ self._profile_new_button.connect('clicked', self.on_profile_new_clicked)
+ self._profile_delete_button.connect('clicked', self.on_profile_delete_clicked)
+ self._host_entry.connect('focus-out-event', self.on_host_entry_outfocused)
+ self._port_spinner.connect('value-changed', self.on_port_spinner_value_changed)
+ self._password_entry.connect('focus-out-event', self.on_password_entry_outfocused)
+ self._image_dir_entry.connect('focus-out-event', self.on_image_dir_entry_outfocused)
+ self._autoconnect_button.connect('toggled', self.on_autoconnect_button_toggled)
- # Actions
- self._load_profiles()
+ # Actions
+ self._load_profiles()
- def get_name(self):
- return 'connection'
+ def get_name(self):
+ return 'connection'
- def get_title(self):
- return "Server"
+ def get_title(self):
+ return "Server"
- def on_profiles_changed(self, *data):
- self._profile_config.save()
+ def on_profiles_changed(self, *data):
+ self._profile_config.save()
- def on_profile_combo_changed(self, combo):
- (index, profile) = self._get_selected_profile()
- if profile is not None:
- self._profile = profile
- self.set_host(self._profile.get('host'))
- self.set_port(int(self._profile.get('port')))
- self.set_password(self._profile.get('password'))
- self.set_image_dir(self._profile.get('image_dir'))
- self._autoconnect_button.set_active(ConnectionPanel.TAG_AUTOCONNECT in self._profile.get_tags())
- self._callback(ConnectionPanel.SIGNAL_PROFILE_CHANGED, index, profile)
+ def on_profile_combo_changed(self, combo):
+ (index, profile) = self._get_selected_profile()
+ if profile is not None:
+ self._profile = profile
+ self.set_host(self._profile.get('host'))
+ self.set_port(int(self._profile.get('port')))
+ self.set_password(self._profile.get('password'))
+ self.set_image_dir(self._profile.get('image_dir'))
+ self._autoconnect_button.set_active(ConnectionPanel.TAG_AUTOCONNECT in self._profile.get_tags())
+ self._callback(ConnectionPanel.SIGNAL_PROFILE_CHANGED, index, profile)
- def on_profile_new_clicked(self, widget):
- profile = mcg.MCGProfile()
- self._profile_config.add_profile(profile)
- self._reload_profiles()
- self._profile_combo.set_active(len(self._profiles)-1)
+ def on_profile_new_clicked(self, widget):
+ profile = mcg.MCGProfile()
+ self._profile_config.add_profile(profile)
+ self._reload_profiles()
+ self._profile_combo.set_active(len(self._profiles)-1)
- def on_profile_delete_clicked(self, widget):
- (index, profile) = self._get_selected_profile()
- if profile is not None:
- self._profile_config.delete_profile(profile)
- self._reload_profiles()
- self._profile_combo.set_active(0)
+ def on_profile_delete_clicked(self, widget):
+ (index, profile) = self._get_selected_profile()
+ if profile is not None:
+ self._profile_config.delete_profile(profile)
+ self._reload_profiles()
+ self._profile_combo.set_active(0)
- def on_host_entry_outfocused(self, widget, event):
- self._profile.set('host', widget.get_text())
- self._profiles.set(self._profile_combo.get_active_iter(), 0, widget.get_text())
+ def on_host_entry_outfocused(self, widget, event):
+ self._profile.set('host', widget.get_text())
+ self._profiles.set(self._profile_combo.get_active_iter(), 0, widget.get_text())
- def on_port_spinner_value_changed(self, widget):
- self._profile.set('port', self.get_port())
+ def on_port_spinner_value_changed(self, widget):
+ self._profile.set('port', self.get_port())
- def on_password_entry_outfocused(self, widget, event):
- self._profile.set('password', widget.get_text())
+ def on_password_entry_outfocused(self, widget, event):
+ self._profile.set('password', widget.get_text())
- def on_image_dir_entry_outfocused(self, widget, event):
- self._profile.set('image_dir', widget.get_text())
+ def on_image_dir_entry_outfocused(self, widget, event):
+ self._profile.set('image_dir', widget.get_text())
- def on_autoconnect_button_toggled(self, widget):
- tags = self._profile.get_tags()
- if widget.get_active():
- if ConnectionPanel.TAG_AUTOCONNECT not in tags:
- tags.append(ConnectionPanel.TAG_AUTOCONNECT)
- else:
- if ConnectionPanel.TAG_AUTOCONNECT in tags:
- tags.remove(ConnectionPanel.TAG_AUTOCONNECT)
- self._profile.set_tags(tags)
+ def on_autoconnect_button_toggled(self, widget):
+ tags = self._profile.get_tags()
+ if widget.get_active():
+ if ConnectionPanel.TAG_AUTOCONNECT not in tags:
+ tags.append(ConnectionPanel.TAG_AUTOCONNECT)
+ else:
+ if ConnectionPanel.TAG_AUTOCONNECT in tags:
+ tags.remove(ConnectionPanel.TAG_AUTOCONNECT)
+ self._profile.set_tags(tags)
- def set_host(self, host):
- self._host_entry.set_text(host)
+ def set_host(self, host):
+ self._host_entry.set_text(host)
- def get_host(self):
- return self._host_entry.get_text()
+ def get_host(self):
+ return self._host_entry.get_text()
- def set_port(self, port):
- self._port_spinner.set_value(port)
+ def set_port(self, port):
+ self._port_spinner.set_value(port)
- def get_port(self):
- return self._port_spinner.get_value_as_int()
+ def get_port(self):
+ return self._port_spinner.get_value_as_int()
- def set_password(self, password):
- if password is None:
- password = ""
- self._password_entry.set_text(password)
+ def set_password(self, password):
+ if password is None:
+ password = ""
+ self._password_entry.set_text(password)
- def get_password(self):
- if self._password_entry.get_text() == "":
- return None
- else:
- return self._password_entry.get_text()
+ def get_password(self):
+ if self._password_entry.get_text() == "":
+ return None
+ else:
+ return self._password_entry.get_text()
- def set_image_dir(self, image_dir):
- self._image_dir_entry.set_text(image_dir)
+ 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 get_image_dir(self):
+ return self._image_dir_entry.get_text()
- def select_profile(self, index):
- if len(self._profiles) <= index:
- index = 0
- self._profile_combo.set_active(index)
+ def select_profile(self, index):
+ if len(self._profiles) <= index:
+ index = 0
+ self._profile_combo.set_active(index)
- def _load_profiles(self):
- self._profile_config.load()
- for profile in self._profile_config.get_profiles():
- self._profiles.append([str(profile)])
+ def _load_profiles(self):
+ self._profile_config.load()
+ for profile in self._profile_config.get_profiles():
+ self._profiles.append([str(profile)])
- def _reload_profiles(self):
- self._profiles.clear()
- for profile in self._profile_config.get_profiles():
- self._profiles.append([str(profile)])
+ def _reload_profiles(self):
+ self._profiles.clear()
+ for profile in self._profile_config.get_profiles():
+ self._profiles.append([str(profile)])
- def _get_selected_profile(self):
- index = self._profile_combo.get_active()
- if index >= 0:
- profiles = self._profile_config.get_profiles()
- if index < len(profiles):
- return (index, profiles[index])
- return (-1, None)
+ def _get_selected_profile(self):
+ index = self._profile_combo.get_active()
+ if index >= 0:
+ profiles = self._profile_config.get_profiles()
+ if index < len(profiles):
+ return (index, profiles[index])
+ return (-1, None)
class CoverPanel(Panel, Gtk.VBox):
- SIGNAL_TOGGLE_FULLSCREEN = 'toggle-fullscreen'
- SIGNAL_SET_SONG = 'set-song'
+ SIGNAL_TOGGLE_FULLSCREEN = 'toggle-fullscreen'
+ SIGNAL_SET_SONG = 'set-song'
- def __init__(self):
- Panel.__init__(self)
- Gtk.VBox.__init__(self)
- self._current_album = None
- self._cover_pixbuf = None
- self._timer = None
- self._properties = {}
+ def __init__(self):
+ Panel.__init__(self)
+ Gtk.VBox.__init__(self)
+ self._current_album = None
+ self._cover_pixbuf = None
+ self._timer = None
+ self._properties = {}
- # Widgets
- self._current_box = Gtk.Box(Gtk.Orientation.HORIZONTAL)
- self._current_box.set_halign(Gtk.Align.FILL)
- self._current_box.set_homogeneous(True)
- self.pack_start(self._current_box, True, True, 10)
- # Cover
- self._cover_image = Gtk.Image()
- self._cover_box = Gtk.EventBox()
- self._cover_box.add(self._cover_image)
- self._cover_scroll = Gtk.ScrolledWindow()
- self._cover_scroll.add(self._cover_box)
- self._current_box.pack_start(self._cover_scroll, True, True, 10)
- # Songs
- self._songs_scale = Gtk.VScale()
- self._songs_scale.set_halign(Gtk.Align.START)
- self._songs_scale.set_vexpand(True)
- self._songs_scale.set_digits(0)
- self._songs_scale.set_draw_value(False)
- self._songs_scale.override_color(Gtk.StateFlags.NORMAL, Gdk.RGBA(0, 0, 0, 1))
- self._current_box.pack_end(self._songs_scale, True, True, 10)
- # Album Infos
- self._info_grid = Gtk.Grid()
- self._info_grid.set_halign(Gtk.Align.CENTER)
- self._info_grid.set_row_spacing(5)
- self._album_title_label = Gtk.Label()
- self._info_grid.add(self._album_title_label)
- self._album_date_label = Gtk.Label()
- self._info_grid.attach_next_to(self._album_date_label, self._album_title_label, Gtk.PositionType.BOTTOM, 1, 1)
- self._album_artist_label = Gtk.Label()
- self._info_grid.attach_next_to(self._album_artist_label, self._album_date_label, Gtk.PositionType.BOTTOM, 1, 1)
- self.pack_end(self._info_grid, False, True, 10)
+ # Widgets
+ self._current_box = Gtk.Box(Gtk.Orientation.HORIZONTAL)
+ self._current_box.set_halign(Gtk.Align.FILL)
+ self._current_box.set_homogeneous(True)
+ self.pack_start(self._current_box, True, True, 10)
+ # Cover
+ self._cover_image = Gtk.Image()
+ self._cover_box = Gtk.EventBox()
+ self._cover_box.add(self._cover_image)
+ self._cover_scroll = Gtk.ScrolledWindow()
+ self._cover_scroll.add(self._cover_box)
+ self._current_box.pack_start(self._cover_scroll, True, True, 10)
+ # Songs
+ self._songs_scale = Gtk.VScale()
+ self._songs_scale.set_halign(Gtk.Align.START)
+ self._songs_scale.set_vexpand(True)
+ self._songs_scale.set_digits(0)
+ self._songs_scale.set_draw_value(False)
+ self._songs_scale.override_color(Gtk.StateFlags.NORMAL, Gdk.RGBA(0, 0, 0, 1))
+ self._current_box.pack_end(self._songs_scale, True, True, 10)
+ # Album Infos
+ self._info_grid = Gtk.Grid()
+ self._info_grid.set_halign(Gtk.Align.CENTER)
+ self._info_grid.set_row_spacing(5)
+ self._album_title_label = Gtk.Label()
+ self._info_grid.add(self._album_title_label)
+ self._album_date_label = Gtk.Label()
+ self._info_grid.attach_next_to(self._album_date_label, self._album_title_label, Gtk.PositionType.BOTTOM, 1, 1)
+ self._album_artist_label = Gtk.Label()
+ self._info_grid.attach_next_to(self._album_artist_label, self._album_date_label, Gtk.PositionType.BOTTOM, 1, 1)
+ self.pack_end(self._info_grid, False, True, 10)
- # Signals
- self._cover_box.connect('button-press-event', self.on_cover_box_pressed)
- self._cover_scroll.connect('size-allocate', self.on_cover_size_allocate)
- self._songs_scale.connect('button-press-event', self.on_songs_start_change)
- self._songs_scale.connect('button-release-event', self.on_songs_change)
+ # Signals
+ self._cover_box.connect('button-press-event', self.on_cover_box_pressed)
+ self._cover_scroll.connect('size-allocate', self.on_cover_size_allocate)
+ self._songs_scale.connect('button-press-event', self.on_songs_start_change)
+ self._songs_scale.connect('button-release-event', self.on_songs_change)
- def get_name(self):
- return 'cover'
+ def get_name(self):
+ return 'cover'
- def get_title(self):
- return "Cover"
+ def get_title(self):
+ return "Cover"
- def on_cover_box_pressed(self, widget, event):
- if event.type == Gdk.EventType._2BUTTON_PRESS:
- self._callback(self.SIGNAL_TOGGLE_FULLSCREEN)
+ def on_cover_box_pressed(self, widget, event):
+ if event.type == Gdk.EventType._2BUTTON_PRESS:
+ self._callback(self.SIGNAL_TOGGLE_FULLSCREEN)
- def on_cover_size_allocate(self, widget, allocation):
- self._resize_image()
+ def on_cover_size_allocate(self, widget, allocation):
+ self._resize_image()
- def on_songs_start_change(self, widget, event):
- if self._timer:
- GObject.source_remove(self._timer)
+ def on_songs_start_change(self, widget, event):
+ if self._timer:
+ GObject.source_remove(self._timer)
- def on_songs_change(self, widget, event):
- value = int(self._songs_scale.get_value())
- time = self._current_album.get_length()
- tracks = self._current_album.get_tracks()
- pos = len(tracks)
- for index in range(pos-1, -1, -1):
- time = time - tracks[index].get_length()
- pos = pos - 1
- if time < value:
- break
- time = max(value - time - 1, 0)
- self._callback(self.SIGNAL_SET_SONG, pos, time)
+ def on_songs_change(self, widget, event):
+ value = int(self._songs_scale.get_value())
+ time = self._current_album.get_length()
+ tracks = self._current_album.get_tracks()
+ pos = len(tracks)
+ for index in range(pos-1, -1, -1):
+ time = time - tracks[index].get_length()
+ pos = pos - 1
+ if time < value:
+ break
+ time = max(value - time - 1, 0)
+ self._callback(self.SIGNAL_SET_SONG, pos, time)
- def set_album(self, album):
- self._album_title_label.set_markup("{}".format(album.get_title()))
- self._album_date_label.set_markup("{}".format(', '.join(album.get_dates())))
- self._album_artist_label.set_markup("{}".format(', '.join(album.get_artists())))
- self._set_cover(album)
- self._set_tracks(album)
+ def set_album(self, album):
+ self._album_title_label.set_markup("{}".format(album.get_title()))
+ self._album_date_label.set_markup("{}".format(', '.join(album.get_dates())))
+ self._album_artist_label.set_markup("{}".format(', '.join(album.get_artists())))
+ self._set_cover(album)
+ self._set_tracks(album)
- def set_play(self, pos, time):
- if self._timer is not None:
- GObject.source_remove(self._timer)
- tracks = self._current_album.get_tracks()
- for index in range(0, pos):
- time = time + tracks[index].get_length()
+ def set_play(self, pos, time):
+ if self._timer is not None:
+ GObject.source_remove(self._timer)
+ tracks = self._current_album.get_tracks()
+ for index in range(0, pos):
+ time = time + tracks[index].get_length()
- self._songs_scale.set_value(time+1)
- self._timer = GObject.timeout_add(1000, self._playing)
+ self._songs_scale.set_value(time+1)
+ self._timer = GObject.timeout_add(1000, self._playing)
- def set_pause(self):
- if self._timer is not None:
- GObject.source_remove(self._timer)
- self._timer = None
+ def set_pause(self):
+ if self._timer is not None:
+ GObject.source_remove(self._timer)
+ self._timer = None
- def set_fullscreen(self, active):
- if active:
- self._songs_scale.hide()
- self._info_grid.hide()
- self.child_set_property(self._current_box, 'padding', 0)
- self._current_box.child_set_property(self._cover_scroll, 'padding', 0)
- self._cover_box.override_background_color(Gtk.StateFlags.NORMAL, Gdk.RGBA(0, 0, 0, 1))
- else:
- self._songs_scale.show()
- self._info_grid.show()
- self.child_set_property(self._current_box, 'padding', 10)
- self._current_box.child_set_property(self._cover_scroll, 'padding', 10)
- self._cover_box.override_background_color(Gtk.StateFlags.NORMAL, Gdk.RGBA(0, 0, 0, 0))
- GObject.idle_add(self._resize_image)
+ def set_fullscreen(self, active):
+ if active:
+ self._songs_scale.hide()
+ self._info_grid.hide()
+ self.child_set_property(self._current_box, 'padding', 0)
+ self._current_box.child_set_property(self._cover_scroll, 'padding', 0)
+ self._cover_box.override_background_color(Gtk.StateFlags.NORMAL, Gdk.RGBA(0, 0, 0, 1))
+ else:
+ self._songs_scale.show()
+ self._info_grid.show()
+ self.child_set_property(self._current_box, 'padding', 10)
+ self._current_box.child_set_property(self._cover_scroll, 'padding', 10)
+ self._cover_box.override_background_color(Gtk.StateFlags.NORMAL, Gdk.RGBA(0, 0, 0, 0))
+ GObject.idle_add(self._resize_image)
- def _set_cover(self, album):
- if self._current_album is not None and album.get_hash() == self._current_album.get_hash():
- return
- self._current_album = album
- url = album.get_cover()
- if url is not None and url is not "":
- # Load image and draw it
- self._cover_pixbuf = self._load_cover(url)
- self._resize_image()
- else:
- # Reset image
- self._cover_pixbuf = None
- self._cover_image.clear()
+ def _set_cover(self, album):
+ if self._current_album is not None and album.get_hash() == self._current_album.get_hash():
+ return
+ self._current_album = album
+ url = album.get_cover()
+ if url is not None and url is not "":
+ # Load image and draw it
+ self._cover_pixbuf = self._load_cover(url)
+ self._resize_image()
+ else:
+ # Reset image
+ self._cover_pixbuf = None
+ self._cover_image.clear()
- def _set_tracks(self, album):
- self._songs_scale.clear_marks()
- self._songs_scale.set_range(0, album.get_length())
- length = 0
- for track in album.get_tracks():
- cur_length = length
- if length > 0 and length < album.get_length():
- cur_length = cur_length + 1
- self._songs_scale.add_mark(cur_length, Gtk.PositionType.RIGHT, track.get_title())
- length = length + track.get_length()
- self._songs_scale.add_mark(length, Gtk.PositionType.RIGHT, "{0[0]:02d}:{0[1]:02d} minutes".format(divmod(length, 60)))
+ def _set_tracks(self, album):
+ self._songs_scale.clear_marks()
+ self._songs_scale.set_range(0, album.get_length())
+ length = 0
+ for track in album.get_tracks():
+ cur_length = length
+ if length > 0 and length < album.get_length():
+ cur_length = cur_length + 1
+ self._songs_scale.add_mark(cur_length, Gtk.PositionType.RIGHT, track.get_title())
+ length = length + track.get_length()
+ self._songs_scale.add_mark(length, Gtk.PositionType.RIGHT, "{0[0]:02d}:{0[1]:02d} minutes".format(divmod(length, 60)))
- def _playing(self):
- value = self._songs_scale.get_value() + 1
- self._songs_scale.set_value(value)
+ def _playing(self):
+ value = self._songs_scale.get_value() + 1
+ self._songs_scale.set_value(value)
- return True
+ return True
- def _load_cover(self, url):
- 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_cover(self, url):
+ 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 _resize_image(self):
- """Diese Methode skaliert das geladene Bild aus dem Pixelpuffer
- auf die Größe des Fensters unter Beibehalt der Seitenverhältnisse
- """
- pixbuf = self._cover_pixbuf
- size = self._cover_scroll.get_allocation()
- ## Check pixelbuffer
- if pixbuf is None:
- return
+ def _resize_image(self):
+ """Diese Methode skaliert das geladene Bild aus dem Pixelpuffer
+ auf die Größe des Fensters unter Beibehalt der Seitenverhältnisse
+ """
+ pixbuf = self._cover_pixbuf
+ size = self._cover_scroll.get_allocation()
+ ## Check pixelbuffer
+ if pixbuf is None:
+ return
- # Skalierungswert für Breite und Höhe ermitteln
- ratioW = float(size.width) / float(pixbuf.get_width())
- ratioH = float(size.height) / float(pixbuf.get_height())
- # Kleineren beider Skalierungswerte nehmen, nicht Hochskalieren
- ratio = min(ratioW, ratioH)
- ratio = min(ratio, 1)
- # Neue Breite und Höhe berechnen
- width = int(math.floor(pixbuf.get_width()*ratio))
- height = int(math.floor(pixbuf.get_height()*ratio))
- if width <= 0 or height <= 0:
- return
- # Pixelpuffer auf Oberfläche zeichnen
- self._cover_image.set_allocation(self._cover_scroll.get_allocation())
- self._cover_image.set_from_pixbuf(pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.HYPER))
- self._cover_image.show()
+ # Skalierungswert für Breite und Höhe ermitteln
+ ratioW = float(size.width) / float(pixbuf.get_width())
+ ratioH = float(size.height) / float(pixbuf.get_height())
+ # Kleineren beider Skalierungswerte nehmen, nicht Hochskalieren
+ ratio = min(ratioW, ratioH)
+ ratio = min(ratio, 1)
+ # Neue Breite und Höhe berechnen
+ width = int(math.floor(pixbuf.get_width()*ratio))
+ height = int(math.floor(pixbuf.get_height()*ratio))
+ if width <= 0 or height <= 0:
+ return
+ # Pixelpuffer auf Oberfläche zeichnen
+ self._cover_image.set_allocation(self._cover_scroll.get_allocation())
+ self._cover_image.set_from_pixbuf(pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.HYPER))
+ self._cover_image.show()
class PlaylistPanel(Panel, Gtk.VBox):
- SIGNAL_CLEAR_PLAYLIST = 'clear-playlist'
+ SIGNAL_CLEAR_PLAYLIST = 'clear-playlist'
- def __init__(self):
- Panel.__init__(self)
- Gtk.VBox.__init__(self)
- self._host = None
- self._item_size = 150
- self._playlist = None
- self._playlist_lock = threading.Lock()
- self._playlist_stop = threading.Event()
+ def __init__(self):
+ Panel.__init__(self)
+ Gtk.VBox.__init__(self)
+ self._host = None
+ self._item_size = 150
+ self._playlist = None
+ self._playlist_lock = threading.Lock()
+ self._playlist_stop = threading.Event()
- # Toolbar
- self._playlist_toolbar = Gtk.Toolbar()
- self.pack_start(self._playlist_toolbar, False, True, 0)
- # Toolbar: Clear Button
- self._clear_playlist_button = Gtk.ToolButton(Gtk.STOCK_CLEAR)
- self._playlist_toolbar.add(self._clear_playlist_button)
- # Playlist Grid: Model
- self._playlist_grid_model = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str)
- # Playlist Grid
- self._playlist_grid = Gtk.IconView(self._playlist_grid_model)
- self._playlist_grid.set_pixbuf_column(0)
- self._playlist_grid.set_text_column(-1)
- self._playlist_grid.set_tooltip_column(1)
- self._playlist_grid.set_margin(0)
- self._playlist_grid.set_spacing(0)
- self._playlist_grid.set_row_spacing(0)
- self._playlist_grid.set_column_spacing(0)
- self._playlist_grid.set_item_padding(10)
- self._playlist_grid.set_reorderable(False)
- self._playlist_grid.set_item_width(-1)
- self._playlist_grid.set_selection_mode(Gtk.SelectionMode.SINGLE)
- self._playlist_scroll = Gtk.ScrolledWindow()
- self._playlist_scroll.add(self._playlist_grid)
- self.pack_end(self._playlist_scroll, True, True, 0)
- self.show_all();
+ # Toolbar
+ self._playlist_toolbar = Gtk.Toolbar()
+ self.pack_start(self._playlist_toolbar, False, True, 0)
+ # Toolbar: Clear Button
+ self._clear_playlist_button = Gtk.ToolButton(Gtk.STOCK_CLEAR)
+ self._playlist_toolbar.add(self._clear_playlist_button)
+ # Playlist Grid: Model
+ self._playlist_grid_model = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str)
+ # Playlist Grid
+ self._playlist_grid = Gtk.IconView(self._playlist_grid_model)
+ self._playlist_grid.set_pixbuf_column(0)
+ self._playlist_grid.set_text_column(-1)
+ self._playlist_grid.set_tooltip_column(1)
+ self._playlist_grid.set_margin(0)
+ self._playlist_grid.set_spacing(0)
+ self._playlist_grid.set_row_spacing(0)
+ self._playlist_grid.set_column_spacing(0)
+ self._playlist_grid.set_item_padding(10)
+ self._playlist_grid.set_reorderable(False)
+ self._playlist_grid.set_item_width(-1)
+ self._playlist_grid.set_selection_mode(Gtk.SelectionMode.SINGLE)
+ self._playlist_scroll = Gtk.ScrolledWindow()
+ self._playlist_scroll.add(self._playlist_grid)
+ self.pack_end(self._playlist_scroll, True, True, 0)
+ self.show_all();
- # Properties
- self._playlist_toolbar.get_style_context().add_class(Window.STYLE_CLASS_NO_BG)
- self._playlist_grid.get_style_context().add_class(Window.STYLE_CLASS_NO_BG)
+ # Properties
+ self._playlist_toolbar.get_style_context().add_class(Window.STYLE_CLASS_NO_BG)
+ self._playlist_grid.get_style_context().add_class(Window.STYLE_CLASS_NO_BG)
- # Signals
- self._clear_playlist_button.connect('clicked' ,self._callback_from_widget, PlaylistPanel.SIGNAL_CLEAR_PLAYLIST)
+ # Signals
+ self._clear_playlist_button.connect('clicked' ,self._callback_from_widget, PlaylistPanel.SIGNAL_CLEAR_PLAYLIST)
- def get_name(self):
- return "playlist"
+ def get_name(self):
+ return "playlist"
- def get_title(self):
- return "Playlist"
+ def get_title(self):
+ return "Playlist"
- def set_item_size(self, item_size):
- if self._item_size != item_size:
- self._item_size = item_size
- self._redraw()
+ def set_item_size(self, item_size):
+ if self._item_size != item_size:
+ self._item_size = item_size
+ self._redraw()
- def get_item_size(self):
- return self._item_size
+ def get_item_size(self):
+ return self._item_size
- def set_playlist(self, host, playlist):
- self._host = host
- self._playlist_stop.set()
- threading.Thread(target=self._set_playlist, args=(host, playlist, self._item_size,)).start()
+ def set_playlist(self, host, playlist):
+ self._host = host
+ self._playlist_stop.set()
+ threading.Thread(target=self._set_playlist, args=(host, playlist, self._item_size,)).start()
- def stop_threads(self):
- self._playlist_stop.set()
+ def stop_threads(self):
+ self._playlist_stop.set()
- def _set_playlist(self, host, playlist, size):
- self._playlist_lock.acquire()
- self._playlist_stop.clear()
- self._playlist = playlist
- self._playlist_grid.set_model(None)
- self._playlist_grid.freeze_child_notify()
- self._playlist_grid_model.clear()
+ def _set_playlist(self, host, playlist, size):
+ self._playlist_lock.acquire()
+ self._playlist_stop.clear()
+ self._playlist = playlist
+ self._playlist_grid.set_model(None)
+ self._playlist_grid.freeze_child_notify()
+ self._playlist_grid_model.clear()
- cache = mcg.MCGCache(host, size)
- for album in playlist:
- pixbuf = None
- if album.get_cover() is not None:
- try:
- pixbuf = Application.load_thumbnail(cache, album, size)
- except Exception as e:
- print(e)
- if pixbuf is None:
- pixbuf = self._playlist_grid.render_icon_pixbuf(Gtk.STOCK_MISSING_IMAGE, Gtk.IconSize.DIALOG)
- if pixbuf is not None:
- self._playlist_grid_model.append([
- pixbuf,
- GObject.markup_escape_text("\n".join([
- album.get_title(),
- ', '.join(album.get_dates()),
- ', '.join(album.get_artists())
- ])),
- album.get_hash()
- ])
+ cache = mcg.MCGCache(host, size)
+ for album in playlist:
+ pixbuf = None
+ if album.get_cover() is not None:
+ try:
+ pixbuf = Application.load_thumbnail(cache, album, size)
+ except Exception as e:
+ print(e)
+ if pixbuf is None:
+ pixbuf = self._playlist_grid.render_icon_pixbuf(Gtk.STOCK_MISSING_IMAGE, Gtk.IconSize.DIALOG)
+ if pixbuf is not None:
+ self._playlist_grid_model.append([
+ pixbuf,
+ GObject.markup_escape_text("\n".join([
+ album.get_title(),
+ ', '.join(album.get_dates()),
+ ', '.join(album.get_artists())
+ ])),
+ album.get_hash()
+ ])
- if self._playlist_stop.is_set():
- self._playlist_lock.release()
- return
+ if self._playlist_stop.is_set():
+ self._playlist_lock.release()
+ return
- self._playlist_grid.set_model(self._playlist_grid_model)
- self._playlist_grid.thaw_child_notify()
- self._playlist_grid.set_columns(len(playlist))
- self._playlist_lock.release()
+ self._playlist_grid.set_model(self._playlist_grid_model)
+ self._playlist_grid.thaw_child_notify()
+ self._playlist_grid.set_columns(len(playlist))
+ self._playlist_lock.release()
- def _redraw(self):
- if self._playlist is not None:
- self.set_playlist(self._host, self._playlist)
+ def _redraw(self):
+ if self._playlist is not None:
+ self.set_playlist(self._host, self._playlist)
- def _callback_from_widget(self, widget, signal, *data):
- self._callback(signal, *data)
+ def _callback_from_widget(self, widget, signal, *data):
+ self._callback(signal, *data)
class LibraryPanel(Panel, Gtk.VBox):
- SIGNAL_UPDATE = 'update'
- SIGNAL_PLAY = 'play'
- SIGNAL_ITEM_SIZE_CHANGED = 'item-size-changed'
- SIGNAL_SORT_ORDER_CHANGED = 'sort-order-changed'
- SIGNAL_SORT_TYPE_CHANGED = 'sort-type-changed'
-
-
- def __init__(self):
- Panel.__init__(self)
- Gtk.VBox.__init__(self)
- self._buttons = {}
- self._albums = None
- self._host = "localhost"
- self._filter_string = ""
- self._item_size = 150
- self._sort_order = mcg.MCGAlbum.SORT_BY_YEAR
- self._sort_type = Gtk.SortType.DESCENDING
- self._grid_pixbufs = {}
- self._old_ranges = {}
- self._library_lock = threading.Lock()
- self._library_stop = threading.Event()
-
- # Widgets
- # Progress Bar
- self._progress_bar = Gtk.ProgressBar()
- # Toolbar
- self._library_toolbar = Gtk.HeaderBar()
- self.pack_start(self._library_toolbar, False, True, 0)
- # Toolbar: buttons left
- # Toolbar: buttons left: Update Button
- self._buttons[LibraryPanel.SIGNAL_UPDATE] = Gtk.ToolButton(Gtk.STOCK_REFRESH)
- self._library_toolbar.pack_start(self._buttons[LibraryPanel.SIGNAL_UPDATE])
- # Toolbar: Filter Entry
- self._filter_entry = Gtk.SearchEntry()
- self._filter_entry.set_placeholder_text("Bibliothek durchsuchen")
- self._library_toolbar.set_custom_title(self._filter_entry)
- # Toolbar: buttons right
- self._right_toolbar = Gtk.Toolbar()
- self._right_toolbar.set_show_arrow(False)
- self._right_toolbar.get_style_context().add_class(Window.STYLE_CLASS_NO_BG)
- self._library_toolbar.pack_end(self._right_toolbar)
- # Toolbar: buttons right: Grid Scale
- self._grid_scale = Gtk.HScale()
- self._grid_scale.set_range(100, 1000)
- self._grid_scale.set_round_digits(0)
- self._grid_scale.set_value(self._item_size)
- self._grid_scale.set_size_request(100, -1)
- self._grid_scale.set_draw_value(False)
- item = Gtk.ToolItem()
- item.add(self._grid_scale)
- self._right_toolbar.add(item)
- # Toolbar: buttons right: Library Sort Menu
- library_sort_store = Gtk.ListStore(str, str)
- library_sort_store.append([mcg.MCGAlbum.SORT_BY_ARTIST, "sort by artist"])
- library_sort_store.append([mcg.MCGAlbum.SORT_BY_TITLE, "sort by title"])
- library_sort_store.append([mcg.MCGAlbum.SORT_BY_YEAR, "sort by year"])
- self._library_sort_combo = Gtk.ComboBox.new_with_model(library_sort_store)
- renderer_text = Gtk.CellRendererText()
- self._library_sort_combo.pack_start(renderer_text, True)
- self._library_sort_combo.add_attribute(renderer_text, "text", 1)
- self._library_sort_combo.set_id_column(0)
- self._library_sort_combo.set_active_id(self._sort_order)
- item = Gtk.ToolItem()
- item.add(self._library_sort_combo)
- self._right_toolbar.add(item)
- # Toolbar: buttons right: Library Sort Type
- self._library_sort_type_button = Gtk.ToggleToolButton.new_from_stock(Gtk.STOCK_SORT_ASCENDING)
- self._library_sort_type_button.set_active(True)
- self._library_sort_type_button.set_stock_id(Gtk.STOCK_SORT_DESCENDING)
- self._right_toolbar.add(self._library_sort_type_button)
- # Library Grid: Model
- self._library_grid_model = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str)
- self._library_grid_model.set_sort_func(2, self.compare_albums, self._sort_order)
- self._library_grid_model.set_sort_column_id(2, self._sort_type)
- self._library_grid_filter = self._library_grid_model.filter_new()
- self._library_grid_filter.set_visible_func(self.on_filter_visible)
- # Library Grid
- self._library_grid = Gtk.IconView(self._library_grid_filter)
-# self._library_grid.pack_end(text_renderer, False)
-# self._library_grid.add_attribute(text_renderer, "markup", 0)
- self._library_grid.set_pixbuf_column(0)
- self._library_grid.set_text_column(-1)
- self._library_grid.set_tooltip_column(1)
- self._library_grid.set_margin(0)
- self._library_grid.set_spacing(0)
- self._library_grid.set_row_spacing(0)
- self._library_grid.set_column_spacing(0)
- self._library_grid.set_item_padding(5)
- self._library_grid.set_reorderable(False)
- self._library_grid.set_item_width(-1)
- self._library_grid.set_selection_mode(Gtk.SelectionMode.SINGLE)
- self._library_scroll = Gtk.ScrolledWindow()
- self._library_scroll.add(self._library_grid)
- self.pack_end(self._library_scroll, True, True, 0)
-
- # Properties
- self._library_toolbar.get_style_context().add_class(Window.STYLE_CLASS_NO_BG)
- self._library_toolbar.get_style_context().add_class(Window.STYLE_CLASS_NO_BORDER)
- self._library_grid.get_style_context().add_class(Window.STYLE_CLASS_NO_BG)
- self._library_grid.get_style_context().add_class(Window.STYLE_CLASS_NO_BORDER)
-
- # Signals
- self._buttons[LibraryPanel.SIGNAL_UPDATE].connect('clicked', self._callback_from_widget, self.SIGNAL_UPDATE)
- self._grid_scale.connect('change-value', self.on_grid_scale_change)
- self._grid_scale.connect('button-release-event', self.on_grid_scale_changed)
- self._library_sort_combo.connect("changed", self.on_library_sort_combo_changed)
- self._library_sort_type_button.connect('clicked', self.on_library_sort_type_button_activated)
- self._filter_entry.connect('changed', self.on_filter_entry_changed)
- self._library_grid.connect('item-activated', self.on_library_grid_clicked)
-
-
- def get_name(self):
- return "library"
-
-
- def get_title(self):
- return "Library"
-
-
- def on_filter_visible(self, model, iter, data):
- hash = model.get_value(iter, 2)
- if not hash in self._albums.keys():
- return
- album = self._albums[hash]
- return album.filter(self._filter_string)
-
-
- def on_filter_entry_changed(self, widget):
- self._filter_string = self._filter_entry.get_text()
- GObject.idle_add(self._library_grid_filter.refilter)
-
-
- def on_grid_scale_change(self, widget, scroll, value):
- size = round(value)
- range = self._grid_scale.get_adjustment()
- if size < range.get_lower() or size > range.get_upper():
- return
- self._item_size = size
- GObject.idle_add(self._set_widget_grid_size, self._library_grid, size, True)
-
-
- def on_grid_scale_changed(self, widget, event):
- size = round(self._grid_scale.get_value())
- range = self._grid_scale.get_adjustment()
- if size < range.get_lower() or size > range.get_upper():
- return
- self._callback(LibraryPanel.SIGNAL_ITEM_SIZE_CHANGED, size)
- self._redraw()
-
-
- def on_library_sort_combo_changed(self, combo):
- sort_order = combo.get_active_id()
- self._sort_order = sort_order
- self._library_grid_model.set_sort_func(2, self.compare_albums, sort_order)
- self._callback(LibraryPanel.SIGNAL_SORT_ORDER_CHANGED, sort_order)
-
-
- def on_library_sort_type_button_activated(self, button):
- if button.get_active():
- sort_type = Gtk.SortType.DESCENDING
- button.set_stock_id(Gtk.STOCK_SORT_DESCENDING)
- else:
- sort_type = Gtk.SortType.ASCENDING
- button.set_stock_id(Gtk.STOCK_SORT_ASCENDING)
- self._sort_type = sort_type
- self._library_grid_model.set_sort_column_id(2, sort_type)
- self._callback(LibraryPanel.SIGNAL_SORT_TYPE_CHANGED, sort_type)
-
-
- def on_library_grid_clicked(self, widget, path):
- path = self._library_grid_filter.convert_path_to_child_path(path)
- iter = self._library_grid_model.get_iter(path)
- self._callback(LibraryPanel.SIGNAL_PLAY, self._library_grid_model.get_value(iter, 2))
-
-
- def set_item_size(self, item_size):
- if self._item_size != item_size:
- self._item_size = item_size
- self._grid_scale.set_value(item_size)
- self._redraw()
-
-
- def get_item_size(self):
- return self._item_size
-
-
- def set_sort_order(self, sort_order):
- if self._sort_order != sort_order:
- self._sort_order = sort_order
- self._library_sort_combo.set_active_id(sort_order)
-
-
- def get_sort_order(self):
- return self._sort_order
-
-
- def set_sort_type(self, sort_type):
- if self._sort_type != sort_type:
- if sort_type:
- self._sort_type = Gtk.SortType.DESCENDING
- self._library_sort_type_button.set_active(True)
- else:
- self._sort_type = Gtk.SortType.ASCENDING
- self._library_sort_type_button.set_active(False)
-
-
- def get_sort_type(self):
- return (self._sort_type != Gtk.SortType.ASCENDING)
-
-
- def set_albums(self, host, albums):
- self._host = host
- self._library_stop.set()
- threading.Thread(target=self._set_albums, args=(host, albums, self._item_size,)).start()
-
-
- def compare_albums(self, model, row1, row2, criterion):
- hash1 = model.get_value(row1, 2)
- hash2 = model.get_value(row2, 2)
-
- if hash1 == "" or hash2 == "":
- return
- return mcg.MCGAlbum.compare(self._albums[hash1], self._albums[hash2], criterion)
-
-
- def stop_threads(self):
- self._library_stop.set()
-
-
- def _set_albums(self, host, albums, size):
- self._library_lock.acquire()
- self._library_stop.clear()
- self._albums = albums
- if len(self.get_children()) > 1:
- GObject.idle_add(self.remove, self.get_children()[0])
- GObject.idle_add(self._progress_bar.set_fraction, 0.0)
- GObject.idle_add(self.pack_start, self._progress_bar, False, True, 0)
- GObject.idle_add(self.show_all)
- self._library_grid.set_model(None)
- self._library_grid.freeze_child_notify()
- self._library_grid_model.clear()
-
- i = 0
- n = len(albums)
- cache = mcg.MCGCache(host, size)
- self._grid_pixbufs.clear()
- for hash in albums.keys():
- album = albums[hash]
- pixbuf = None
- try:
- pixbuf = Application.load_thumbnail(cache, album, size)
- except Exception as e:
- print(e)
- if pixbuf is None:
- pixbuf = self._library_grid.render_icon_pixbuf(Gtk.STOCK_MISSING_IMAGE, Gtk.IconSize.DIALOG)
- if pixbuf is not None:
- self._grid_pixbufs[album.get_hash()] = pixbuf
- self._library_grid_model.append([
- pixbuf,
- GObject.markup_escape_text("\n".join([
- album.get_title(),
- ', '.join(album.get_dates()),
- ', '.join(album.get_artists())
- ])),
- hash
- ])
-
- i += 1
- GObject.idle_add(self._progress_bar.set_fraction, i/n)
- if self._library_stop.is_set():
- self._library_lock.release()
- return
-
- self._library_grid.set_model(self._library_grid_filter)
- self._library_grid.thaw_child_notify()
- self._library_grid.set_item_width(-1)
- self._library_lock.release()
- if len(self.get_children()) > 1:
- GObject.idle_add(self.remove, self.get_children()[0])
- GObject.idle_add(self.pack_start, self._library_toolbar, False, True, 0)
- GObject.idle_add(self.show_all)
-
-
- def _set_widget_grid_size(self, grid_widget, size, vertical):
- self._library_stop.set()
- threading.Thread(target=self._set_widget_grid_size_thread, args=(grid_widget, size, vertical,)).start()
-
-
- def _set_widget_grid_size_thread(self, grid_widget, size, vertical):
- self._library_lock.acquire()
- self._library_stop.clear()
- grid_filter = grid_widget.get_model()
- grid_model = grid_filter.get_model()
-
- # get old_range
- grid_widget_id = id(grid_widget)
- if grid_widget_id not in self._old_ranges or self._old_ranges[grid_widget_id] is None:
- self._old_ranges[grid_widget_id] = range(0, len(grid_filter))
- old_range = self._old_ranges[grid_widget_id]
- old_start = len(old_range) > 0 and old_range[0] or 0
- old_end = len(old_range) > 0 and old_range[len(old_range)-1] + 1 or 0
-
- # calculate visible range
- w = (grid_widget.get_allocation().width // size) + (vertical and 0 or 1)
- h = (grid_widget.get_allocation().height // size) + (vertical and 1 or 0)
- c = w * h
- vis_range = grid_widget.get_visible_range()
- if vis_range is None:
- self._library_lock.release()
- return
- (vis_start,), (vis_end,) = vis_range
- vis_end = min(vis_start + c, len(grid_filter))
- vis_range = range(vis_start, vis_end)
-
- # set pixbuf
- cur_start = min(old_start, vis_start)
- cur_end = max(old_end, vis_end)
- cur_range = range(cur_start, cur_end)
- for index in cur_range:
- iter = grid_filter.convert_iter_to_child_iter(grid_filter[index].iter)
- if index in vis_range:
- hash = grid_model.get_value(iter, 2)
- pixbuf = self._grid_pixbufs[hash]
- pixbuf = pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.NEAREST)
- else:
- pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, False, 8, 1, 1)
- grid_model.set_value(iter, 0, pixbuf)
-
- if self._library_stop.is_set():
- self._library_lock.release()
- return
-
- self._old_ranges[grid_widget_id] = vis_range
- grid_widget.set_item_width(size)
-
- self._library_lock.release()
-
-
- def _redraw(self):
- if self._albums is not None:
- self.set_albums(self._host, self._albums)
-
-
- def _callback_from_widget(self, widget, signal, *data):
- self._callback(signal, *data)
-
-
-
-
-class StackSwitcher(mcg.MCGBase, Gtk.StackSwitcher):
- SIGNAL_STACK_SWITCHED = 'stack-switched'
-
-
- def __init__(self):
- mcg.MCGBase.__init__(self)
- Gtk.StackSwitcher.__init__(self)
- self._temp_button = None
-
-
- def set_stack(self, stack):
- super().set_stack(stack)
- for child in self.get_children():
- if type(child) is Gtk.RadioButton:
- child.connect('clicked', self.on_clicked)
-
-
- def on_clicked(self, widget):
- if not self._temp_button:
- self._temp_button = widget
- else:
- self._temp_button = None
- self._callback(StackSwitcher.SIGNAL_STACK_SWITCHED, self)
-
-
-
-
+ SIGNAL_UPDATE = 'update'
+ SIGNAL_PLAY = 'play'
+ SIGNAL_ITEM_SIZE_CHANGED = 'item-size-changed'
+ SIGNAL_SORT_ORDER_CHANGED = 'sort-order-changed'
+ SIGNAL_SORT_TYPE_CHANGED = 'sort-type-changed'
+
+
+ def __init__(self):
+ Panel.__init__(self)
+ Gtk.VBox.__init__(self)
+ self._buttons = {}
+ self._albums = None
+ self._host = "localhost"
+ self._filter_string = ""
+ self._item_size = 150
+ self._sort_order = mcg.MCGAlbum.SORT_BY_YEAR
+ self._sort_type = Gtk.SortType.DESCENDING
+ self._grid_pixbufs = {}
+ self._old_ranges = {}
+ self._library_lock = threading.Lock()
+ self._library_stop = threading.Event()
+
+ # Widgets
+ # Progress Bar
+ self._progress_bar = Gtk.ProgressBar()
+ # Toolbar
+ self._library_toolbar = Gtk.HeaderBar()
+ self.pack_start(self._library_toolbar, False, True, 0)
+ # Toolbar: buttons left
+ # Toolbar: buttons left: Update Button
+ self._buttons[LibraryPanel.SIGNAL_UPDATE] = Gtk.ToolButton(Gtk.STOCK_REFRESH)
+ self._library_toolbar.pack_start(self._buttons[LibraryPanel.SIGNAL_UPDATE])
+ # Toolbar: Filter Entry
+ self._filter_entry = Gtk.SearchEntry()
+ self._filter_entry.set_placeholder_text("Bibliothek durchsuchen")
+ self._library_toolbar.set_custom_title(self._filter_entry)
+ # Toolbar: buttons right
+ self._right_toolbar = Gtk.Toolbar()
+ self._right_toolbar.set_show_arrow(False)
+ self._right_toolbar.get_style_context().add_class(Window.STYLE_CLASS_NO_BG)
+ self._library_toolbar.pack_end(self._right_toolbar)
+ # Toolbar: buttons right: Grid Scale
+ self._grid_scale = Gtk.HScale()
+ self._grid_scale.set_range(100, 1000)
+ self._grid_scale.set_round_digits(0)
+ self._grid_scale.set_value(self._item_size)
+ self._grid_scale.set_size_request(100, -1)
+ self._grid_scale.set_draw_value(False)
+ item = Gtk.ToolItem()
+ item.add(self._grid_scale)
+ self._right_toolbar.add(item)
+ # Toolbar: buttons right: Library Sort Menu
+ library_sort_store = Gtk.ListStore(str, str)
+ library_sort_store.append([mcg.MCGAlbum.SORT_BY_ARTIST, "sort by artist"])
+ library_sort_store.append([mcg.MCGAlbum.SORT_BY_TITLE, "sort by title"])
+ library_sort_store.append([mcg.MCGAlbum.SORT_BY_YEAR, "sort by year"])
+ self._library_sort_combo = Gtk.ComboBox.new_with_model(library_sort_store)
+ renderer_text = Gtk.CellRendererText()
+ self._library_sort_combo.pack_start(renderer_text, True)
+ self._library_sort_combo.add_attribute(renderer_text, "text", 1)
+ self._library_sort_combo.set_id_column(0)
+ self._library_sort_combo.set_active_id(self._sort_order)
+ item = Gtk.ToolItem()
+ item.add(self._library_sort_combo)
+ self._right_toolbar.add(item)
+ # Toolbar: buttons right: Library Sort Type
+ self._library_sort_type_button = Gtk.ToggleToolButton.new_from_stock(Gtk.STOCK_SORT_ASCENDING)
+ self._library_sort_type_button.set_active(True)
+ self._library_sort_type_button.set_stock_id(Gtk.STOCK_SORT_DESCENDING)
+ self._right_toolbar.add(self._library_sort_type_button)
+ # Library Grid: Model
+ self._library_grid_model = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str)
+ self._library_grid_model.set_sort_func(2, self.compare_albums, self._sort_order)
+ self._library_grid_model.set_sort_column_id(2, self._sort_type)
+ self._library_grid_filter = self._library_grid_model.filter_new()
+ self._library_grid_filter.set_visible_func(self.on_filter_visible)
+ # Library Grid
+ self._library_grid = Gtk.IconView(self._library_grid_filter)
+# self._library_grid.pack_end(text_renderer, False)
+# self._library_grid.add_attribute(text_renderer, "markup", 0)
+ self._library_grid.set_pixbuf_column(0)
+ self._library_grid.set_text_column(-1)
+ self._library_grid.set_tooltip_column(1)
+ self._library_grid.set_margin(0)
+ self._library_grid.set_spacing(0)
+ self._library_grid.set_row_spacing(0)
+ self._library_grid.set_column_spacing(0)
+ self._library_grid.set_item_padding(5)
+ self._library_grid.set_reorderable(False)
+ self._library_grid.set_item_width(-1)
+ self._library_grid.set_selection_mode(Gtk.SelectionMode.SINGLE)
+ self._library_scroll = Gtk.ScrolledWindow()
+ self._library_scroll.add(self._library_grid)
+ self.pack_end(self._library_scroll, True, True, 0)
+
+ # Properties
+ self._library_toolbar.get_style_context().add_class(Window.STYLE_CLASS_NO_BG)
+ self._library_toolbar.get_style_context().add_class(Window.STYLE_CLASS_NO_BORDER)
+ self._library_grid.get_style_context().add_class(Window.STYLE_CLASS_NO_BG)
+ self._library_grid.get_style_context().add_class(Window.STYLE_CLASS_NO_BORDER)
+
+ # Signals
+ self._buttons[LibraryPanel.SIGNAL_UPDATE].connect('clicked', self._callback_from_widget, self.SIGNAL_UPDATE)
+ self._grid_scale.connect('change-value', self.on_grid_scale_change)
+ self._grid_scale.connect('button-release-event', self.on_grid_scale_changed)
+ self._library_sort_combo.connect("changed", self.on_library_sort_combo_changed)
+ self._library_sort_type_button.connect('clicked', self.on_library_sort_type_button_activated)
+ self._filter_entry.connect('changed', self.on_filter_entry_changed)
+ self._library_grid.connect('item-activated', self.on_library_grid_clicked)
+
+
+ def get_name(self):
+ return "library"
+
+
+ def get_title(self):
+ return "Library"
+
+
+ def on_filter_visible(self, model, iter, data):
+ hash = model.get_value(iter, 2)
+ if not hash in self._albums.keys():
+ return
+ album = self._albums[hash]
+ return album.filter(self._filter_string)
+
+
+ def on_filter_entry_changed(self, widget):
+ self._filter_string = self._filter_entry.get_text()
+ GObject.idle_add(self._library_grid_filter.refilter)
+
+
+ def on_grid_scale_change(self, widget, scroll, value):
+ size = round(value)
+ range = self._grid_scale.get_adjustment()
+ if size < range.get_lower() or size > range.get_upper():
+ return
+ self._item_size = size
+ GObject.idle_add(self._set_widget_grid_size, self._library_grid, size, True)
+
+
+ def on_grid_scale_changed(self, widget, event):
+ size = round(self._grid_scale.get_value())
+ range = self._grid_scale.get_adjustment()
+ if size < range.get_lower() or size > range.get_upper():
+ return
+ self._callback(LibraryPanel.SIGNAL_ITEM_SIZE_CHANGED, size)
+ self._redraw()
+
+
+ def on_library_sort_combo_changed(self, combo):
+ sort_order = combo.get_active_id()
+ self._sort_order = sort_order
+ self._library_grid_model.set_sort_func(2, self.compare_albums, sort_order)
+ self._callback(LibraryPanel.SIGNAL_SORT_ORDER_CHANGED, sort_order)
+
+
+ def on_library_sort_type_button_activated(self, button):
+ if button.get_active():
+ sort_type = Gtk.SortType.DESCENDING
+ button.set_stock_id(Gtk.STOCK_SORT_DESCENDING)
+ else:
+ sort_type = Gtk.SortType.ASCENDING
+ button.set_stock_id(Gtk.STOCK_SORT_ASCENDING)
+ self._sort_type = sort_type
+ self._library_grid_model.set_sort_column_id(2, sort_type)
+ self._callback(LibraryPanel.SIGNAL_SORT_TYPE_CHANGED, sort_type)
+
+
+ def on_library_grid_clicked(self, widget, path):
+ path = self._library_grid_filter.convert_path_to_child_path(path)
+ iter = self._library_grid_model.get_iter(path)
+ self._callback(LibraryPanel.SIGNAL_PLAY, self._library_grid_model.get_value(iter, 2))
+
+
+ def set_item_size(self, item_size):
+ if self._item_size != item_size:
+ self._item_size = item_size
+ self._grid_scale.set_value(item_size)
+ self._redraw()
+
+
+ def get_item_size(self):
+ return self._item_size
+
+
+ def set_sort_order(self, sort_order):
+ if self._sort_order != sort_order:
+ self._sort_order = sort_order
+ self._library_sort_combo.set_active_id(sort_order)
+
+
+ def get_sort_order(self):
+ return self._sort_order
+
+
+ def set_sort_type(self, sort_type):
+ if self._sort_type != sort_type:
+ if sort_type:
+ self._sort_type = Gtk.SortType.DESCENDING
+ self._library_sort_type_button.set_active(True)
+ else:
+ self._sort_type = Gtk.SortType.ASCENDING
+ self._library_sort_type_button.set_active(False)
+
+
+ def get_sort_type(self):
+ return (self._sort_type != Gtk.SortType.ASCENDING)
+
+
+ def set_albums(self, host, albums):
+ self._host = host
+ self._library_stop.set()
+ threading.Thread(target=self._set_albums, args=(host, albums, self._item_size,)).start()
+
+
+ def compare_albums(self, model, row1, row2, criterion):
+ hash1 = model.get_value(row1, 2)
+ hash2 = model.get_value(row2, 2)
+
+ if hash1 == "" or hash2 == "":
+ return
+ return mcg.MCGAlbum.compare(self._albums[hash1], self._albums[hash2], criterion)
+
+
+ def stop_threads(self):
+ self._library_stop.set()
+
+
+ def _set_albums(self, host, albums, size):
+ self._library_lock.acquire()
+ self._library_stop.clear()
+ self._albums = albums
+ if len(self.get_children()) > 1:
+ GObject.idle_add(self.remove, self.get_children()[0])
+ GObject.idle_add(self._progress_bar.set_fraction, 0.0)
+ GObject.idle_add(self.pack_start, self._progress_bar, False, True, 0)
+ GObject.idle_add(self.show_all)
+ self._library_grid.set_model(None)
+ self._library_grid.freeze_child_notify()
+ self._library_grid_model.clear()
+
+ i = 0
+ n = len(albums)
+ cache = mcg.MCGCache(host, size)
+ self._grid_pixbufs.clear()
+ for hash in albums.keys():
+ album = albums[hash]
+ pixbuf = None
+ try:
+ pixbuf = Application.load_thumbnail(cache, album, size)
+ except Exception as e:
+ print(e)
+ if pixbuf is None:
+ pixbuf = self._library_grid.render_icon_pixbuf(Gtk.STOCK_MISSING_IMAGE, Gtk.IconSize.DIALOG)
+ if pixbuf is not None:
+ self._grid_pixbufs[album.get_hash()] = pixbuf
+ self._library_grid_model.append([
+ pixbuf,
+ GObject.markup_escape_text("\n".join([
+ album.get_title(),
+ ', '.join(album.get_dates()),
+ ', '.join(album.get_artists())
+ ])),
+ hash
+ ])
+
+ i += 1
+ GObject.idle_add(self._progress_bar.set_fraction, i/n)
+ if self._library_stop.is_set():
+ self._library_lock.release()
+ return
+
+ self._library_grid.set_model(self._library_grid_filter)
+ self._library_grid.thaw_child_notify()
+ self._library_grid.set_item_width(-1)
+ self._library_lock.release()
+ if len(self.get_children()) > 1:
+ GObject.idle_add(self.remove, self.get_children()[0])
+ GObject.idle_add(self.pack_start, self._library_toolbar, False, True, 0)
+ GObject.idle_add(self.show_all)
+
+
+ def _set_widget_grid_size(self, grid_widget, size, vertical):
+ self._library_stop.set()
+ threading.Thread(target=self._set_widget_grid_size_thread, args=(grid_widget, size, vertical,)).start()
+
+
+ def _set_widget_grid_size_thread(self, grid_widget, size, vertical):
+ self._library_lock.acquire()
+ self._library_stop.clear()
+ grid_filter = grid_widget.get_model()
+ grid_model = grid_filter.get_model()
+
+ # get old_range
+ grid_widget_id = id(grid_widget)
+ if grid_widget_id not in self._old_ranges or self._old_ranges[grid_widget_id] is None:
+ self._old_ranges[grid_widget_id] = range(0, len(grid_filter))
+ old_range = self._old_ranges[grid_widget_id]
+ old_start = len(old_range) > 0 and old_range[0] or 0
+ old_end = len(old_range) > 0 and old_range[len(old_range)-1] + 1 or 0
+
+ # calculate visible range
+ w = (grid_widget.get_allocation().width // size) + (vertical and 0 or 1)
+ h = (grid_widget.get_allocation().height // size) + (vertical and 1 or 0)
+ c = w * h
+ vis_range = grid_widget.get_visible_range()
+ if vis_range is None:
+ self._library_lock.release()
+ return
+ (vis_start,), (vis_end,) = vis_range
+ vis_end = min(vis_start + c, len(grid_filter))
+ vis_range = range(vis_start, vis_end)
+
+ # set pixbuf
+ cur_start = min(old_start, vis_start)
+ cur_end = max(old_end, vis_end)
+ cur_range = range(cur_start, cur_end)
+ for index in cur_range:
+ iter = grid_filter.convert_iter_to_child_iter(grid_filter[index].iter)
+ if index in vis_range:
+ hash = grid_model.get_value(iter, 2)
+ pixbuf = self._grid_pixbufs[hash]
+ pixbuf = pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.NEAREST)
+ else:
+ pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, False, 8, 1, 1)
+ grid_model.set_value(iter, 0, pixbuf)
+
+ if self._library_stop.is_set():
+ self._library_lock.release()
+ return
+
+ self._old_ranges[grid_widget_id] = vis_range
+ grid_widget.set_item_width(size)
+
+ self._library_lock.release()
+
+
+ def _redraw(self):
+ if self._albums is not None:
+ self.set_albums(self._host, self._albums)
+
+
+ def _callback_from_widget(self, widget, signal, *data):
+ self._callback(signal, *data)
+
+
+
+
+class StackSwitcher(mcg.Base, Gtk.StackSwitcher):
+ SIGNAL_STACK_SWITCHED = 'stack-switched'
+
+
+ def __init__(self):
+ mcg.Base.__init__(self)
+ Gtk.StackSwitcher.__init__(self)
+ self._temp_button = None
+
+
+ def set_stack(self, stack):
+ super().set_stack(stack)
+ for child in self.get_children():
+ if type(child) is Gtk.RadioButton:
+ child.connect('clicked', self.on_clicked)
+
+
+ def on_clicked(self, widget):
+ if not self._temp_button:
+ self._temp_button = widget
+ else:
+ self._temp_button = None
+ self._callback(StackSwitcher.SIGNAL_STACK_SWITCHED, self)
diff --git a/mcg.py b/mcg.py
index e8f95bc..9e6e7d5 100644
--- a/mcg.py
+++ b/mcg.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""MPDCoverGrid is a client for the Music Player Daemon, focused on albums instead of single tracks."""
@@ -6,877 +6,984 @@
__author__ = "coderkun"
__email__ = ""
__license__ = "GPL"
-__version__ = "0.2"
+__version__ = "0.3"
__status__ = "Development"
import configparser
import glob
+import logging
import os
import queue
+import socket
+import sys
import threading
import urllib.request
from hashlib import md5
-import mpd
+class MPDException(Exception):
+ pass
-class MCGBase():
- def __init__(self):
- self._callbacks = {}
+class ConnectionException(MPDException):
+ pass
- def connect_signal(self, signal, callback):
- """Connect a callback function to a signal (event).
- """
- self._callbacks[signal] = callback
+class ProtocolException(MPDException):
+ pass
- def disconnect_signal(self, signal):
- """Disconnect a callback function from a signal (event).
- """
- if self._has_callback(signal):
- del self._callbacks[signal]
+class CommandException(MPDException):
+ pass
- def _has_callback(self, signal):
- """Check if there is a registered callback function for a
- signal.
- """
- return signal in self._callbacks
- def _callback(self, signal, *data):
- if signal in self._callbacks:
- callback = self._callbacks[signal]
- callback(*data)
+class Base():
+ def __init__(self):
+ self._callbacks = {}
+ def connect_signal(self, signal, callback):
+ """Connect a callback function to a signal (event)."""
+ self._callbacks[signal] = callback
-class MCGClient(MCGBase, mpd.MPDClient):
- """Client library for handling the connection to the Music Player Daemon.
- This class implements an album-based MPD client.
- It offers a non-blocking threaded worker model for use in graphical
- environments and is based on python-mpd2.
- """
- # Signal: connect/disconnect event
- SIGNAL_CONNECT = 'connect'
- # Signal: status event
- SIGNAL_STATUS = 'status'
- # Signal: load albums
- SIGNAL_LOAD_ALBUMS = 'load-albums'
- # Signal: load playlist
- SIGNAL_LOAD_PLAYLIST = 'load-playlist'
- # Signal: error
- SIGNAL_ERROR = 'error'
+ def disconnect_signal(self, signal):
+ """Disconnect a callback function from a signal (event)."""
+ if self._has_callback(signal):
+ del self._callbacks[signal]
- def __init__(self):
- """Set class variables and instantiates the MPDClient."""
- MCGBase.__init__(self)
- mpd.MPDClient.__init__(self)
- self._connected = False
- self._state = None
- self._client_lock = threading.Lock()
- self._client_stop = threading.Event()
- self._actions = queue.Queue()
- self._worker = None
- self._albums = {}
- self._playlist = []
- self._host = None
- self._image_dir = ""
+ def _has_callback(self, signal):
+ """Check if there is a registered callback function for a signal."""
+ return signal in self._callbacks
- # Connection commands
+ def _callback(self, signal, *data):
+ if signal in self._callbacks:
+ callback = self._callbacks[signal]
+ callback(*data)
- def connect(self, host="localhost", port="6600", password=None, image_dir=""):
- """Connect to MPD with the given host, port and password or
- with standard values.
- """
- self._host = host
- self._image_dir = image_dir
- self._add_action(self._connect, host, port, password)
- def is_connected(self):
- """Return the connection status.
- """
- return self._connected
+class Client(Base):
+ """Client library for handling the connection to the Music Player Daemon.
- def disconnect(self):
- """Disconnect from the connected MPD."""
- self._client_stop.set()
- self._add_action(self._disconnect)
+ This class implements an album-based MPD client. It offers a non-blocking
+ threaded worker model for use in graphical environments.
+ """
+ # Protocol: greeting mark
+ PROTOCOL_GREETING = 'OK MPD '
+ # Protocol: completion mark
+ PROTOCOL_COMPLETION = 'OK'
+ # Protocol: error mark
+ PROTOCOL_ERROR = 'ACK '
+ # Signal: connection status
+ SIGNAL_CONNECTION = 'connection'
+ # Signal: status
+ SIGNAL_STATUS = 'status'
+ # Signal: load albums
+ SIGNAL_LOAD_ALBUMS = 'load-albums'
+ # Signal: load playlist
+ SIGNAL_LOAD_PLAYLIST = 'load-playlist'
+ # Signal: error
+ SIGNAL_ERROR = 'error'
- def join(self):
- self._actions.join()
+ def __init__(self):
+ """Set class variables and instantiates the Client."""
+ Base.__init__(self)
+ self._logger = logging.getLogger(__name__)
+ self._sock = None
+ self._sock_read = None
+ self._sock_write = None
+ self._stop = threading.Event()
+ self._actions = queue.Queue()
+ self._worker = None
+ self._idling = False
+ self._host = None
+ self._albums = {}
+ self._playlist = []
+ self._image_dir = ""
+ self._state = None
- # Status commands
+ def get_logger(self):
+ return self._logger
- def get_status(self):
- """Determine the current status."""
- self._add_action(self._get_status)
+ # Client commands
- # Playback option commands
+ def connect(self, host, port, password=None, image_dir=""):
+ """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()
- def set_volume(self, volume):
- self._add_action(self._set_volume, volume)
+ def is_connected(self):
+ """Return the connection status."""
+ return self._worker is not None
- # Playback control commands
- def playpause(self):
- """Play or pauses the current state."""
- self._add_action(self._playpause)
+ def disconnect(self):
+ """Disconnect from the connected MPD."""
+ self._logger.info("disconnect")
+ self._stop.set()
+ self._add_action(self._disconnect)
- def play_album(self, album):
- """Play the given album.
- """
- self._add_action(self._play_album, album)
+ def join(self):
+ self._actions.join()
- def seek(self, pos, time):
- """Seeks to a song at a position
- """
- self._add_action(self._seek, pos, time)
+ def get_status(self):
+ """Determine the current status."""
+ self._logger.info("get status")
+ self._add_action(self._get_status)
- def stop(self):
- self._add_action(self._stop)
+ def load_albums(self):
+ self._logger.info("load albums")
+ self._add_action(self._load_albums)
+
+ def update(self):
+ self._logger.info("update")
+ self._add_action(self._update)
- # Playlist commands
- def load_playlist(self):
- self._add_action(self._load_playlist)
-
-
- def clear_playlist(self):
- """Clear the current playlist"""
- self._add_action(self._clear_playlist)
-
-
- # Database commands
-
- def load_albums(self):
- self._add_action(self._load_albums)
-
-
- def update(self):
- self._add_action(self._update)
-
-
- # Private methods
-
- def _add_action(self, method, *args):
- """Add an action to the action list.
- """
- action = [method, args]
- self._actions.put(action)
- self._start_worker()
-
-
- def _start_worker(self):
- """Start the worker thread which waits for action to be
- performed."""
- if self._worker is None or not self._worker.is_alive():
- self._worker = threading.Thread(target=self._run, name='mcg-worker', args=())
- self._worker.setDaemon(True)
- self._worker.start()
- else:
- try:
- self._call('noidle')
- except BrokenPipeError:
- pass
- except ConnectionResetError as e:
- self._set_connection_status(False, e)
- except mpd.ConnectionError as e:
- self._set_connection_status(False, e)
-
-
- def _work(self, action):
- method = action[0]
- params = action[1]
- method(*params)
-
-
- def _call(self, command, *args):
- try:
- return getattr(super(), command)(*args)
- except mpd.CommandError as e:
- self._callback(MCGClient.SIGNAL_ERROR, e)
- except mpd.ConnectionError as e:
- self._set_connection_status(False, e)
- except ConnectionResetError as e:
- self._set_connection_status(False, e)
- except BrokenPipeError:
- pass
-
-
- def _run(self):
- while not self._client_stop.is_set() or not self._actions.empty():
- if self._actions.empty():
- self._actions.put([self._idle, ()])
- action = self._actions.get()
-
- self._client_lock.acquire()
- self._work(action)
- self._client_lock.release()
- self._actions.task_done()
-
-
- # Connection commands
-
- def _connect(self, host, port, password):
- try:
- self._call('connect', host, port)
- if password:
- try:
- self._call('password', password)
- except mpd.CommandError as e:
- self._disconnect()
- raise e
- self._set_connection_status(True)
- except OSError as e:
- self._set_connection_status(False, e)
-
-
- def _disconnect(self):
- self._call('noidle')
- self._call('disconnect')
- self._set_connection_status(False)
-
-
- # Status commands
-
- def _get_status(self):
- """Action: Perform the real status determination."""
- # current status
- status = self._call('status')
- if 'state' not in status:
- return
- state = status['state']
- time = 0
- if 'time' in status:
- time = int(status['time'].split(':')[0])
- volume = 0
- if 'volume' in status:
- volume = int(status['volume'])
- error = None
- if 'error' in status:
- error = status['error']
-
- # current song
- song = self._call('currentsong')
- album = None
- pos = None
- if song:
- # Track
- if 'artist' not in song:
- return
- if 'title' not in song:
- return
- if 'track' not in song:
- song['track'] = None
- if 'time' not in song:
- song['time'] = 0
- if 'date' not in song:
- song['date'] = None
- if 'file' not in song:
- return
- track = MCGTrack(song['artist'], song['title'], song['track'], song['time'], song['date'], song['file'])
-
- # Album
- if 'album' not in song:
- song['album'] = 'Various'
- hash = MCGAlbum.hash(song['album'])
- if hash not in self._albums:
- return
- album = self._albums[hash]
-
- # Position
- pos = 0
- if 'pos' in song:
- pos = int(song['pos'])
- for palbum in self._playlist:
- if palbum == album and len(palbum.get_tracks()) >= pos:
- album = palbum
- break
- pos = pos - len(palbum.get_tracks())
-
- self._state = state
- self._callback(MCGClient.SIGNAL_STATUS, state, album, pos, time, volume, error)
-
-
- # Playback option commants
-
- def _set_volume(self, volume):
- self._call('setvol', volume)
-
-
- # Playback control commands
-
- def _playpause(self):
- """Action: Perform the real play/pause command."""
- status = self._call('status')
- state = status['state']
- if state == 'play':
- self._call('pause')
- else:
- self._call('play')
-
-
- def _play_album(self, album):
- if album not in self._albums:
- return
- track_ids = []
- for track in self._albums[album].get_tracks():
- track_id = self._call('addid', track.get_file())
- track_ids.append(track_id)
- if self._state != 'play':
- self._call('playid', track_ids[0])
-
-
- def _seek(self, pos, time):
- self._call('seek', pos, time)
-
-
- def _stop(self):
- self._call('stop')
-
-
- # Playlist commands
-
- def _load_playlist(self):
- self._playlist = []
- for song in self._call('playlistinfo'):
- try:
- # Track
- if 'artist' not in song:
- continue
- if 'title' not in song:
- continue
- if 'track' not in song:
- song['track'] = None
- if 'time' not in song:
- song['time'] = 0
- if 'date' not in song:
- song['date'] = None
- if 'file' not in song:
- continue
- track = MCGTrack(song['artist'], song['title'], song['track'], song['time'], song['date'], song['file'])
-
- # Album
- if 'album' not in song:
- song['album'] = 'Various'
- hash = MCGAlbum.hash(song['album'])
- if len(self._playlist) == 0 or self._playlist[len(self._playlist)-1].get_hash() != hash:
- album = MCGAlbum(song['album'], self._host, self._image_dir)
- self._playlist.append(album)
- else:
- album = self._playlist[len(self._playlist)-1]
- album.add_track(track)
- except KeyError:
- pass
- self._callback(MCGClient.SIGNAL_LOAD_PLAYLIST, self._playlist, None)
-
-
- def _clear_playlist(self):
- """Action: Perform the real clearing of the current playlist."""
- self._call('clear')
-
-
- # Database commands
-
- def _load_albums(self):
- """Action: Perform the real update."""
- self._albums = {}
- for song in self._call('listallinfo'):
- if 'directory' in song:
- continue
-
- # Track
- if 'artist' not in song:
- continue
- if 'title' not in song:
- continue
- if 'track' not in song:
- song['track'] = None
- if 'time' not in song:
- song['time'] = 0
- if 'date' not in song:
- song['date'] = None
- if 'file' not in song:
- continue
- track = MCGTrack(song['artist'], song['title'], song['track'], song['time'], song['date'], song['file'])
-
- # Album
- if 'album' not in song:
- song['album'] = 'Various'
- hash = MCGAlbum.hash(song['album'])
- if hash in self._albums.keys():
- album = self._albums[hash]
- else:
- album = MCGAlbum(song['album'], self._host, self._image_dir)
- self._albums[album.get_hash()] = album
- album.add_track(track)
- self._callback(MCGClient.SIGNAL_LOAD_ALBUMS, self._albums, None)
-
-
- def _update(self):
- self._call('update')
-
- def _set_connection_status(self, status, error=None):
- self._connected = status
- self._callback(MCGClient.SIGNAL_CONNECT, status, error)
- if not status:
- self._client_stop.set()
-
-
- def _idle(self):
- """React to idle events from MPD."""
- modules = self._call('idle')
- if not modules:
- return
- if 'player' in modules:
- self.get_status()
- if 'mixer' in modules:
- self.get_status()
- if 'playlist' in modules:
- self.load_playlist()
- if 'database' in modules:
- self.load_albums()
- self.load_playlist()
- self.get_status()
- if 'update' in modules:
- self.load_albums()
- self.load_playlist()
- self.get_status()
+ def load_playlist(self):
+ self._logger.info("load playlist")
+ self._add_action(self._load_playlist)
+
+
+ def clear_playlist(self):
+ """Clear the current playlist"""
+ self._logger.info("clear playlist")
+ self._add_action(self._clear_playlist)
+
+
+ def playpause(self):
+ """Play or pauses the current state."""
+ self._logger.info("playpause")
+ self._add_action(self._playpause)
+
+
+ def play_album(self, album):
+ """Play the given album."""
+ self._logger.info("play album")
+ self._add_action(self._play_album, album)
+
+
+ def seek(self, pos, time):
+ """Seeks to a song at a position"""
+ self._logger.info("seek")
+ self._add_action(self._seek, pos, time)
+
+
+ def stop(self):
+ self._logger.info("stop")
+ self._add_action(self._stop)
+
+
+ def set_volume(self, volume):
+ self._logger.info("set volume")
+ self._add_action(self._set_volume, volume)
+
+
+ # Private methods
+
+ def _connect(self, host, port, password):
+ self._logger.info("connecting to host %r, port %r", host, port)
+ if self._sock is not None:
+ return
+ try:
+ self._sock = self._connect_socket(host, port)
+ self._sock_read = self._sock.makefile("r", encoding="utf-8")
+ self._sock_write = self._sock.makefile("w", encoding="utf-8")
+ self._greet()
+ self._logger.info("connected")
+ if password:
+ self._logger.info("setting password")
+ self._call("password", password)
+ self._set_connection_status(True)
+ except OSError as e:
+ raise ConnectionException("connection failed: {}".format(e))
+
+
+ def _connect_socket(self, host, port):
+ sock = None
+ error = None
+ for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP):
+ af, socktype, proto, canonname, sa = res
+ try:
+ sock = socket.socket(af, socktype, proto)
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
+ sock.connect(sa)
+ return sock
+ except Exception as e:
+ error = e
+ if sock is not None:
+ sock.close()
+ break
+ if error is not None:
+ raise ConnectionException("connection failed: {}".format(error))
+ else:
+ raise ConnectionException("no suitable socket")
+
+
+ def _greet(self):
+ greeting = self._sock_read.readline()
+ self._logger.debug("greeting: %s", greeting.strip())
+ if not greeting.endswith("\n"):
+ self._disconnect_socket()
+ raise ConnectionException("incomplete line")
+ if not greeting.startswith(Client.PROTOCOL_GREETING):
+ self._disconnect_socket()
+ raise ProtocolException("invalid greeting: {}".format(greeting))
+ self._protocol_version = greeting[len(Client.PROTOCOL_GREETING):].strip()
+ self._logger.debug("protocol version: %s", self._protocol_version)
+
+
+ def _disconnect(self):
+ self._logger.info("disconnecting")
+ self._disconnect_socket()
+
+
+ def _disconnect_socket(self):
+ if self._sock_read is not None:
+ self._sock_read.close()
+ if self._sock_write is not None:
+ self._sock_write.close()
+ if self._sock is not None:
+ self._sock.close()
+ self._logger.info("disconnected")
+ self._set_connection_status(False)
+
+
+ def _idle(self):
+ """React to idle events from MPD."""
+ self._logger.info("idle")
+ self._idling = True
+ subsystems = self._parse_dict(self._call("idle"))
+ self._idling = False
+ self._logger.info("idle subsystems: %r", subsystems)
+ if subsystems:
+ if subsystems['changed'] == 'player':
+ self.get_status()
+ if subsystems['changed'] == 'mixer':
+ self.get_status()
+ if subsystems['changed'] == 'playlist':
+ self.load_playlist()
+ if subsystems['changed'] == 'database':
+ self.load_albums()
+ self.load_playlist()
+ self.get_status()
+ if subsystems['changed'] == 'update':
+ self.load_albums()
+ self.load_playlist()
+ self.get_status()
+
+
+ def _noidle(self):
+ if self._idling:
+ self._logger.debug("noidle")
+ self._write("noidle")
+
+
+ def _get_status(self):
+ """Action: Perform the real status determination."""
+ self._logger.info("getting status")
+ status = self._parse_dict(self._call("status"))
+ self._logger.debug("status: %r", status)
+
+ # State
+ state = None
+ if 'state' in status:
+ state = status['state']
+ self._state = state
+ # Time
+ time = 0
+ if 'time' in status:
+ time = int(status['time'].split(':')[0])
+ # Volume
+ volume = 0
+ if 'volume' in status:
+ volume = int(status['volume'])
+ # Error
+ error = None
+ if 'error' in status:
+ error = status['error']
+ # Album
+ album = None
+ pos = 0
+ song = self._parse_dict(self._call("currentsong"))
+ if song:
+ # Album
+ if 'album' not in song:
+ song['album'] = MCGAlbum.DEFAULT_ALBUM
+ hash = MCGAlbum.hash(song['album'])
+ if hash in self._albums.keys():
+ album = self._albums[hash]
+ # Position
+ if 'pos' in song:
+ pos = int(song['pos'])
+ for palbum in self._playlist:
+ if palbum == album and len(palbum.get_tracks()) >= pos:
+ album = palbum
+ break
+ pos = pos - len(palbum.get_tracks())
+ self._callback(Client.SIGNAL_STATUS, state, album, pos, time, volume, error)
+
+
+ def _load_albums(self):
+ """Action: Perform the real update."""
+ self._albums = {}
+ for song in self._parse_list(self._call('listallinfo'), ['file', 'directory']):
+ self._logger.debug("song: %r", song)
+ if 'file' in song:
+ # Track
+ track = None
+ if 'artist' in song and 'title' in song and 'file' in song:
+ if 'track' not in song:
+ song['track'] = None
+ if 'time' not in song:
+ song['time'] = 0
+ if 'date' not in song:
+ song['date'] = None
+ track = MCGTrack(song['artist'], song['title'], song['track'], song['time'], song['date'], song['file'])
+ self._logger.debug("track: %r", track)
+ # Album
+ if 'album' not in song:
+ song['album'] = MCGAlbum.DEFAULT_ALBUM
+ hash = MCGAlbum.hash(song['album'])
+ if hash in self._albums.keys():
+ album = self._albums[hash]
+ else:
+ album = MCGAlbum(song['album'], self._host, self._image_dir)
+ self._albums[album.get_hash()] = album
+ self._logger.debug("album: %r", album)
+ # Add track to album
+ if track:
+ album.add_track(track)
+ self._callback(Client.SIGNAL_LOAD_ALBUMS, self._albums)
+
+
+ def _update(self):
+ self._call('update')
+
+
+ def _load_playlist(self):
+ self._playlist = []
+ for song in self._parse_list(self._call('playlistinfo'), ['file', 'playlist']):
+ self._logger.debug("song: %r", song)
+ # Track
+ track = None
+ if 'artist' in song and 'title' in song and 'file' in song:
+ if 'track' not in song:
+ song['track'] = None
+ if 'time' not in song:
+ song['time'] = 0
+ if 'date' not in song:
+ song['date'] = None
+ track = MCGTrack(song['artist'], song['title'], song['track'], song['time'], song['date'], song['file'])
+ self._logger.debug("track: %r", track)
+ # Album
+ if 'album' not in song:
+ song['album'] = MCGAlbum.DEFAULT_ALBUM
+ hash = MCGAlbum.hash(song['album'])
+ if len(self._playlist) == 0 or self._playlist[len(self._playlist)-1].get_hash() != hash:
+ album = MCGAlbum(song['album'], self._host, self._image_dir)
+ self._playlist.append(album)
+ else:
+ album = self._playlist[len(self._playlist)-1]
+ self._logger.debug("album: %r", album)
+ if track:
+ album.add_track(track)
+ self._callback(Client.SIGNAL_LOAD_PLAYLIST, self._playlist)
+
+
+ def _clear_playlist(self):
+ """Action: Perform the real clearing of the current playlist."""
+ self._call('clear')
+
+
+ def _playpause(self):
+ """Action: Perform the real play/pause command."""
+ #status = self._parse_dict(self._call('status'))
+ #if 'state' in status:
+ if self._state == 'play':
+ self._call('pause')
+ else:
+ self._call('play')
+
+
+ def _play_album(self, album):
+ if album in self._albums:
+ track_ids = []
+ for track in self._albums[album].get_tracks():
+ self._logger.info("addid: %r", track.get_file())
+ track_id = None
+ track_id_response = self._parse_dict(self._call('addid', track.get_file()))
+ if 'id' in track_id_response:
+ track_id = track_id_response['id']
+ self._logger.debug("track id: %r", track_id)
+ if track_id is not None:
+ track_ids.append(track_id)
+ if self._state != 'play' and track_ids:
+ self._call('playid', track_ids[0])
+
+
+ def _seek(self, pos, time):
+ self._call('seek', pos, time)
+
+
+ def _stop(self):
+ self._call('stop')
+
+
+ def _set_volume(self, volume):
+ self._call('setvol', volume)
+
+
+ def _start_worker(self):
+ """Start the worker thread which waits for action to be performed."""
+ self._logger.debug("start worker")
+ self._worker = threading.Thread(target=self._run, name='mcg-worker', args=())
+ self._worker.setDaemon(True)
+ self._worker.start()
+ self._logger.debug("worker started")
+
+
+ def _run(self):
+ while not self._stop.is_set() or not self._actions.empty():
+ if self._sock is not None and self._actions.empty():
+ self._add_action(self._idle)
+ action = self._actions.get()
+ self._logger.debug("next action: %r", action)
+ self._work(action)
+ self._actions.task_done()
+ self._logger.debug("action done")
+ self._logger.debug("worker finished")
+
+
+ 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)
+ self._actions.put(action)
+ self._noidle()
+
+
+ def _work(self, action):
+ (method, args) = action
+ self._logger.debug("work: %r", method.__name__)
+ try:
+ method(*args)
+ except ConnectionException as e:
+ self._logger.exception(e)
+ self._callback(Client.SIGNAL_ERROR, e)
+ self._disconnect_socket()
+ except Exception as e:
+ self._logger.exception(e)
+ self._callback(Client.SIGNAL_ERROR, e)
+
+
+ def _call(self, command, *args):
+ try:
+ self._write(command, args)
+ return self._read()
+ except MPDException as e:
+ self._callback(Client.SIGNAL_ERROR, e)
+
+
+ def _write(self, command, args=None):
+ if args is not None and len(args) > 0:
+ line = '{} "{}"\n'.format(command, '" "'.join(str(x) for x in args))
+ else:
+ line = '{}\n'.format(command)
+ self._logger.debug("write: %r", line)
+ self._sock_write.write(line)
+ self._sock_write.flush()
+
+
+ def _read(self):
+ self._logger.debug("reading response")
+ response = []
+ line = self._sock_read.readline()
+ if not line.endswith("\n"):
+ self._disconnect_socket()
+ raise ConnectionException("incomplete line")
+ while not line.startswith(Client.PROTOCOL_COMPLETION) and not line.startswith(Client.PROTOCOL_ERROR):
+ response.append(line.strip())
+ line = self._sock_read.readline()
+ if not line.endswith("\n"):
+ self._disconnect_socket()
+ raise ConnectionException("incomplete line")
+ if line.startswith(Client.PROTOCOL_COMPLETION):
+ self._logger.debug("response complete")
+ if line.startswith(Client.PROTOCOL_ERROR):
+ error = line[len(Client.PROTOCOL_ERROR):].strip()
+ self._logger.debug("command failed: %r", error)
+ raise CommandException(error)
+ self._logger.debug("response: %r", response)
+ return response
+
+
+ def _parse_dict(self, response):
+ dict = {}
+ for line in response:
+ key, value = self._split_line(line)
+ dict[key] = value
+ return dict
+
+
+ def _parse_list(self, response, delimiters):
+ entry = {}
+ for line in response:
+ key, value = self._split_line(line)
+ if entry and key in delimiters:
+ yield entry
+ entry = {}
+ entry[key] = value
+ if entry:
+ yield entry
+
+
+ def _split_line(self, line):
+ parts = line.split(': ')
+ return parts[0].lower(), ': '.join(parts[1:])
+
+
+ def _set_connection_status(self, status):
+ self._callback(Client.SIGNAL_CONNECTION, status)
class MCGAlbum:
- SORT_BY_ARTIST = 'artist'
- SORT_BY_TITLE = 'title'
- SORT_BY_YEAR = 'year'
- _FILE_NAMES = ['folder', 'cover']
- _FILE_EXTS = ['jpg', 'png', 'jpeg']
+ DEFAULT_ALBUM = 'Various'
+ SORT_BY_ARTIST = 'artist'
+ SORT_BY_TITLE = 'title'
+ SORT_BY_YEAR = 'year'
+ _FILE_NAMES = ['folder', 'cover']
+ _FILE_EXTS = ['jpg', 'png', 'jpeg']
- def __init__(self, title, host, image_dir):
- self._artists = []
- self._pathes = []
- if type(title) is list:
- title = title[0]
- 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._set_hash()
+ def __init__(self, title, host, image_dir):
+ self._artists = []
+ self._pathes = []
+ if type(title) is list:
+ title = title[0]
+ 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._set_hash()
- def __eq__(self, other):
- return self._hash == other.get_hash()
+ def __eq__(self, other):
+ return self._hash == other.get_hash()
- def get_artists(self):
- return self._artists
+ def get_artists(self):
+ return self._artists
- def get_title(self):
- return self._title
+ def get_title(self):
+ return self._title
- def get_dates(self):
- return self._dates
+ def get_dates(self):
+ return self._dates
- def get_date(self):
- if len(self._dates) == 0:
- return None
- return self._dates[0]
+ def get_date(self):
+ if len(self._dates) == 0:
+ return None
+ return self._dates[0]
- def get_path(self):
- return self._path
+ def get_path(self):
+ return self._path
- def add_track(self, track):
- self._tracks.append(track)
- self._length = self._length + track.get_length()
- for artist in track.get_artists():
- if artist not in self._artists:
- self._artists.append(artist)
- if track.get_date() is not None and track.get_date() not in self._dates:
- self._dates.append(track.get_date())
- path = os.path.dirname(track.get_file())
- if path not in self._pathes:
- self._pathes.append(path)
+ def add_track(self, track):
+ self._tracks.append(track)
+ self._length = self._length + track.get_length()
+ for artist in track.get_artists():
+ if artist not in self._artists:
+ self._artists.append(artist)
+ if track.get_date() is not None and track.get_date() not in self._dates:
+ self._dates.append(track.get_date())
+ path = os.path.dirname(track.get_file())
+ if path not in self._pathes:
+ self._pathes.append(path)
- def get_tracks(self):
- return self._tracks
+ def get_tracks(self):
+ return self._tracks
- def get_length(self):
- return self._length
+ def get_length(self):
+ return self._length
- def get_cover(self):
- if self._cover is None and not self._cover_searched:
- self._find_cover()
- return self._cover
+ def get_cover(self):
+ if self._cover is None and not self._cover_searched:
+ self._find_cover()
+ return self._cover
- def hash(title):
- if type(title) is list:
- title = title[0]
- return md5(title.encode('utf-8')).hexdigest()
+ def hash(title):
+ if type(title) is list:
+ title = title[0]
+ return md5(title.encode('utf-8')).hexdigest()
- def get_hash(self):
- return self._hash
+ def get_hash(self):
+ return self._hash
- def filter(self, filter_string):
- values = self._artists + [self._title]
- values.extend(map(lambda track: track.get_title(), self._tracks))
- for value in values:
- if filter_string.lower() in value.lower():
- return True
- return False
+ def filter(self, filter_string):
+ values = self._artists + [self._title]
+ values.extend(map(lambda track: track.get_title(), self._tracks))
+ for value in values:
+ if filter_string.lower() in value.lower():
+ return True
+ return False
- def compare(album1, album2, criterion=None):
- if criterion == None:
- criterion = MCGAlbum.SORT_BY_TITLE
- if criterion == MCGAlbum.SORT_BY_ARTIST:
- value_function = "get_artists"
- elif criterion == MCGAlbum.SORT_BY_TITLE:
- value_function = "get_title"
- elif criterion == MCGAlbum.SORT_BY_YEAR:
- value_function = "get_date"
+ def compare(album1, album2, criterion=None):
+ if criterion == None:
+ criterion = MCGAlbum.SORT_BY_TITLE
+ if criterion == MCGAlbum.SORT_BY_ARTIST:
+ value_function = "get_artists"
+ elif criterion == MCGAlbum.SORT_BY_TITLE:
+ value_function = "get_title"
+ elif criterion == MCGAlbum.SORT_BY_YEAR:
+ value_function = "get_date"
- value1 = getattr(album1, value_function)()
- value2 = getattr(album2, value_function)()
- if value1 is None and value2 is None:
- return 0
- elif value1 is None:
- return -1
- elif value2 is None:
- return 1
- if value1 < value2:
- return -1
- elif value1 == value2:
- return 0
- else:
- return 1
+ value1 = getattr(album1, value_function)()
+ value2 = getattr(album2, value_function)()
+ if value1 is None and value2 is None:
+ return 0
+ elif value1 is None:
+ return -1
+ elif value2 is None:
+ return 1
+ if value1 < value2:
+ return -1
+ elif value1 == value2:
+ return 0
+ else:
+ return 1
- def _set_hash(self):
- self._hash = MCGAlbum.hash(self._title)
+ def _set_hash(self):
+ self._hash = MCGAlbum.hash(self._title)
- def _find_cover(self):
- names = list(MCGAlbum._FILE_NAMES)
- names.append(self._title)
- names.append(' - '.join([self._artists[0], self._title]))
+ def _find_cover(self):
+ names = list(MCGAlbum._FILE_NAMES)
+ names.append(self._title)
+ names.append(' - '.join([self._artists[0], self._title]))
- if self._host == "localhost" or self._host == "127.0.0.1":
- self._cover = self._find_cover_local(names)
- else:
- self._cover = self._find_cover_web(names)
- self._cover_searched = True
+ 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(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_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(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]
+ 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:
- def __init__(self, artists, title, track, length, date, file):
- if type(artists) is not list:
- artists = [artists]
- self._artists = artists
- if type(title) is list:
- title = title[0]
- self._title = title
- if type(track) is list:
- track = track[0]
- if track is not None and '/' in track:
- track = track[0: track.index('/')]
- if track is not None:
- track = int(track)
- self._track = track
- self._length = int(length)
- if type(date) is list:
- date = date[0]
- self._date = date
- if type(file) is list:
- file = file[0]
- self._file = file
+ def __init__(self, artists, title, track, length, date, file):
+ if type(artists) is not list:
+ artists = [artists]
+ self._artists = artists
+ if type(title) is list:
+ title = title[0]
+ self._title = title
+ if type(track) is list:
+ track = track[0]
+ if track is not None and '/' in track:
+ track = track[0: track.index('/')]
+ if track is not None:
+ track = int(track)
+ self._track = track
+ self._length = int(length)
+ if type(date) is list:
+ date = date[0]
+ self._date = date
+ if type(file) is list:
+ file = file[0]
+ self._file = file
- def __eq__(self, other):
- return self._file == other.get_file()
+ def __eq__(self, other):
+ return self._file == other.get_file()
- def get_artists(self):
- return self._artists
+ def get_artists(self):
+ return self._artists
- def get_title(self):
- return self._title
+ def get_title(self):
+ return self._title
- def get_track(self):
- return self._track
+ def get_track(self):
+ return self._track
- def get_length(self):
- return self._length
+ def get_length(self):
+ return self._length
- def get_date(self):
- return self._date
+ def get_date(self):
+ return self._date
- def get_file(self):
- return self._file
+ def get_file(self):
+ return self._file
class MCGConfig(configparser.ConfigParser):
- CONFIG_DIR = '~/.config/mcg/'
+ CONFIG_DIR = '~/.config/mcg/'
- def __init__(self, filename):
- configparser.ConfigParser.__init__(self)
- self._filename = os.path.expanduser(os.path.join(MCGConfig.CONFIG_DIR, filename))
- self._create_dir()
+ def __init__(self, filename):
+ configparser.ConfigParser.__init__(self)
+ self._filename = os.path.expanduser(os.path.join(MCGConfig.CONFIG_DIR, filename))
+ self._create_dir()
- def load(self):
- if os.path.isfile(self._filename):
- self.read(self._filename)
+ def load(self):
+ if os.path.isfile(self._filename):
+ self.read(self._filename)
- def save(self):
- with open(self._filename, 'w') as configfile:
- self.write(configfile)
+ def save(self):
+ with open(self._filename, 'w') as configfile:
+ self.write(configfile)
- def _create_dir(self):
- dirname = os.path.dirname(self._filename)
- if not os.path.exists(dirname):
- os.makedirs(dirname)
+ def _create_dir(self):
+ dirname = os.path.dirname(self._filename)
+ if not os.path.exists(dirname):
+ os.makedirs(dirname)
class MCGProfileConfig(MCGConfig):
- CONFIG_FILE = 'profiles.conf'
+ CONFIG_FILE = 'profiles.conf'
- def __init__(self):
- MCGConfig.__init__(self, MCGProfileConfig.CONFIG_FILE)
- self._profiles = []
+ def __init__(self):
+ MCGConfig.__init__(self, MCGProfileConfig.CONFIG_FILE)
+ self._profiles = []
- def add_profile(self, profile):
- self._profiles.append(profile)
+ def add_profile(self, profile):
+ self._profiles.append(profile)
- def delete_profile(self, profile):
- if profile in self._profiles:
- self._profiles.remove(profile)
- self._force_default_profile()
+ def delete_profile(self, profile):
+ if profile in self._profiles:
+ self._profiles.remove(profile)
+ self._force_default_profile()
- def get_profiles(self):
- return self._profiles
+ def get_profiles(self):
+ return self._profiles
- def load(self):
- super().load()
- count = 0
- if self.has_section('profiles'):
- if self.has_option('profiles', 'count'):
- count = self.getint('profiles', 'count')
- for index in range(count):
- section = 'profile'+str(index+1)
- if self.has_section(section):
- profile = MCGProfile()
- for attribute in profile.get_attributes():
- if self.has_option(section, attribute):
- profile.set(attribute, self.get(section, attribute))
- self._profiles.append(profile)
- self._force_default_profile()
+ def load(self):
+ super().load()
+ count = 0
+ if self.has_section('profiles'):
+ if self.has_option('profiles', 'count'):
+ count = self.getint('profiles', 'count')
+ for index in range(count):
+ section = 'profile'+str(index+1)
+ if self.has_section(section):
+ profile = MCGProfile()
+ for attribute in profile.get_attributes():
+ if self.has_option(section, attribute):
+ profile.set(attribute, self.get(section, attribute))
+ self._profiles.append(profile)
+ self._force_default_profile()
- def save(self):
- if not self.has_section('profiles'):
- self.add_section('profiles')
- self.set('profiles', 'count', str(len(self._profiles)))
+ def save(self):
+ if not self.has_section('profiles'):
+ self.add_section('profiles')
+ self.set('profiles', 'count', str(len(self._profiles)))
- for index in range(len(self._profiles)):
- profile = self._profiles[index]
- section = 'profile'+str(index+1)
- if not self.has_section(section):
- self.add_section(section)
- for attribute in profile.get_attributes():
- self.set(section, attribute, str(profile.get(attribute)))
- for section in self.sections()[len(self._profiles)+1:]:
- self.remove_section(section)
- super().save()
+ for index in range(len(self._profiles)):
+ profile = self._profiles[index]
+ section = 'profile'+str(index+1)
+ if not self.has_section(section):
+ self.add_section(section)
+ for attribute in profile.get_attributes():
+ self.set(section, attribute, str(profile.get(attribute)))
+ for section in self.sections()[len(self._profiles)+1:]:
+ self.remove_section(section)
+ super().save()
- def _force_default_profile(self):
- if len(self._profiles) == 0:
- self._profiles.append(MCGProfile())
+ def _force_default_profile(self):
+ if len(self._profiles) == 0:
+ self._profiles.append(MCGProfile())
class MCGConfigurable:
- def __init__(self):
- self._attributes = []
+ def __init__(self):
+ self._attributes = []
- def get(self, attribute):
- return getattr(self, attribute)
+ def get(self, attribute):
+ return getattr(self, attribute)
- def set(self, attribute, value):
- setattr(self, attribute, value)
- if attribute not in self._attributes:
- self._attributes.append(attribute)
+ def set(self, attribute, value):
+ setattr(self, attribute, value)
+ if attribute not in self._attributes:
+ self._attributes.append(attribute)
- def get_attributes(self):
- return self._attributes
+ def get_attributes(self):
+ return self._attributes
class MCGProfile(MCGConfigurable):
- def __init__(self):
- MCGConfigurable.__init__(self)
- self.set('host', "localhost")
- self.set('port', 6600)
- self.set('password', "")
- self.set('image_dir', "")
- self.set('tags', "")
+ def __init__(self):
+ MCGConfigurable.__init__(self)
+ self.set('host', "localhost")
+ self.set('port', 6600)
+ self.set('password', "")
+ self.set('image_dir', "")
+ self.set('tags', "")
- def __str__(self):
- return self.get("host")
+ def __str__(self):
+ return self.get("host")
- def get_tags(self):
- return self.get('tags').split(',')
+ def get_tags(self):
+ return self.get('tags').split(',')
- def set_tags(self, tags):
- self.set('tags', ','.join(tags))
+ def set_tags(self, tags):
+ self.set('tags', ','.join(tags))
class MCGCache():
- DIRNAME = '~/.cache/mcg/'
- SIZE_FILENAME = 'size'
- _lock = threading.Lock()
+ DIRNAME = '~/.cache/mcg/'
+ SIZE_FILENAME = 'size'
+ _lock = threading.Lock()
- def __init__(self, host, size):
- self._host = host
- self._size = size
- self._dirname = os.path.expanduser(os.path.join(MCGCache.DIRNAME, host))
- if not os.path.exists(self._dirname):
- os.makedirs(self._dirname)
- self._read_size()
+ def __init__(self, host, size):
+ self._host = host
+ self._size = size
+ self._dirname = os.path.expanduser(os.path.join(MCGCache.DIRNAME, host))
+ if not os.path.exists(self._dirname):
+ os.makedirs(self._dirname)
+ self._read_size()
- def create_filename(self, album):
- return os.path.join(self._dirname, '-'.join([album.get_hash()]))
+ def create_filename(self, album):
+ return os.path.join(self._dirname, '-'.join([album.get_hash()]))
- def _read_size(self):
- size = 100
- MCGCache._lock.acquire()
- # Read old size
- filename = os.path.join(self._dirname, MCGCache.SIZE_FILENAME)
- if os.path.exists(filename):
- with open(filename, 'r') as f:
- size = int(f.readline())
- # Clear cache if size has changed
- if size != self._size:
- self._clear()
- # Write new size
- with open(filename, 'w') as f:
- f.write(str(self._size))
- MCGCache._lock.release()
-
-
- def _clear(self):
- for filename in os.listdir(self._dirname):
- path = os.path.join(self._dirname, filename)
- if os.path.isfile(path):
- try:
- os.unlink(path)
- except Exception as e:
- print("clear:", e)
+ def _read_size(self):
+ size = 100
+ MCGCache._lock.acquire()
+ # Read old size
+ filename = os.path.join(self._dirname, MCGCache.SIZE_FILENAME)
+ if os.path.exists(filename):
+ with open(filename, 'r') as f:
+ size = int(f.readline())
+ # Clear cache if size has changed
+ if size != self._size:
+ self._clear()
+ # Write new size
+ with open(filename, 'w') as f:
+ f.write(str(self._size))
+ MCGCache._lock.release()
+ def _clear(self):
+ for filename in os.listdir(self._dirname):
+ path = os.path.join(self._dirname, filename)
+ if os.path.isfile(path):
+ try:
+ os.unlink(path)
+ except Exception as e:
+ print("clear:", e)
diff --git a/mcgGtk.py b/mcgGtk.py
index 25e6e8f..0089755 100755
--- a/mcgGtk.py
+++ b/mcgGtk.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""MPDCoverGrid (GTK version) is a client for the Music Player Daemon, focused on albums instead of single tracks."""
@@ -25,14 +25,17 @@ from gui import gtk
# Set environment
srcdir = os.path.abspath(os.path.join(os.path.dirname(gtk.__file__), '..'))
if not os.environ.get('GSETTINGS_SCHEMA_DIR'):
- os.environ['GSETTINGS_SCHEMA_DIR'] = os.path.join(srcdir, 'data')
+ os.environ['GSETTINGS_SCHEMA_DIR'] = os.path.join(srcdir, 'data')
+def start():
+ app = gtk.Application()
+ exit_status = app.run(sys.argv)
+ sys.exit(exit_status)
+
+
if __name__ == "__main__":
- # Start application
- app = gtk.Application()
- exit_status = app.run(sys.argv)
- sys.exit(exit_status)
-
+ # Start application
+ start()