From 965a536779afdb167f95032db171e0153a791836 Mon Sep 17 00:00:00 2001 From: coderkun Date: Mon, 1 Dec 2014 20:55:28 +0100 Subject: [PATCH] rewrite client, implement protocol without lib, replace tabs with spaces --- gui/gtk.py | 2449 ++++++++++++++++++++++++++-------------------------- mcg.py | 1473 ++++++++++++++++--------------- mcgGtk.py | 17 +- 3 files changed, 2024 insertions(+), 1915 deletions(-) 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()