#!/usr/bin/env python3 # -*- coding: utf-8 -*- """MPDCoverGrid is a client for the Music Player Daemon, focused on albums instead of single tracks.""" __author__ = "coderkun" __email__ = "" __license__ = "GPL" __version__ = "0.4" __status__ = "Development" try: import keyring use_keyring = True except: use_keyring = False import logging import math import os import sys import threading import urllib from gi.repository import Gio, Gtk, Gdk, GObject, GdkPixbuf, GLib from gi.repository import Avahi import mcg class Application(Gtk.Application): TITLE = "MPDCoverGrid (Gtk)" SETTINGS_BASE_KEY = 'de.coderkun.mcg' SETTING_HOST = 'host' SETTING_PORT = 'port' SETTING_CONNECTED = 'connected' SETTING_IMAGE_DIR = 'image-dir' SETTING_WINDOW_SIZE = 'window-size' SETTING_WINDOW_MAXIMIZED = 'window-maximized' SETTING_PANEL = 'panel' SETTING_ITEM_SIZE = 'item-size' SETTING_SORT_ORDER = 'sort-order' SETTING_SORT_TYPE = 'sort-type' KEYRING_SYSTEM = 'MPDCoverGrid (Gtk)' KEYRING_USERNAME = 'mpd' 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_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 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 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.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_CONNECTION].set_host(self._settings.get_string(Application.SETTING_HOST)) self._panels[Window._PANEL_INDEX_CONNECTION].set_port(self._settings.get_int(Application.SETTING_PORT)) if use_keyring: self._panels[Window._PANEL_INDEX_CONNECTION].set_password(keyring.get_password(Application.KEYRING_SYSTEM, Application.KEYRING_USERNAME)) self._panels[Window._PANEL_INDEX_CONNECTION].set_image_dir(self._settings.get_string(Application.SETTING_IMAGE_DIR)) 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_CONNECTION_CHANGED, self.on_connection_panel_connection_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]) if self._settings.get_boolean(Application.SETTING_CONNECTED): self._connect() 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() self._mcg.get_status() def on_header_bar_set_volume(self, volume): self._mcg.set_volume(volume) # Panel callbacks def on_connection_panel_connection_changed(self, host, port, password, image_dir): self._settings.set_string(Application.SETTING_HOST, host) self._settings.set_int(Application.SETTING_PORT, port) if use_keyring: if password: keyring.set_password(Application.KEYRING_SYSTEM, Application.KEYRING_USERNAME, password) else: if keyring.get_password(Application.KEYRING_SYSTEM, Application.KEYRING_USERNAME): keyring.delete_password(Application.KEYRING_SYSTEM, Application.KEYRING_USERNAME) self._settings.set_string(Application.SETTING_IMAGE_DIR, image_dir) 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_set_song(self, pos, time): self._mcg.seek(pos, time) 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_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_type_changed(self, sort_type): self._settings.set_boolean(Application.SETTING_SORT_TYPE, self._panels[Window._PANEL_INDEX_LIBRARY].get_sort_type()) # MCG callbacks 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_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): 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)) # 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_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_type_changed(self, settings, key): sort_type = settings.get_boolean(key) self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_type(sort_type) # 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() self._settings.set_boolean(Application.SETTING_CONNECTED, False) 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) self._settings.set_boolean(Application.SETTING_CONNECTED, True) 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 _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() 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.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_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 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_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 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_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 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) 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) class Panel(mcg.Base): def __init__(self): mcg.Base.__init__(self) def get_name(self): raise NotImplementedError() def get_title(self): raise NotImplementedError() class ConnectionPanel(Panel, Gtk.VBox): SIGNAL_CONNECTION_CHANGED = 'connection-changed' def __init__(self): Panel.__init__(self) Gtk.VBox.__init__(self) self._services = Gtk.ListStore(str, str, int) self._profile = None # Widgets hbox = Gtk.HBox() self.pack_start(hbox, True, False, 0) grid = Gtk.Grid() grid.set_column_spacing(5) grid.set_column_homogeneous(True) hbox.pack_start(grid, True, False, 0) # Zeroconf zeroconf_box = Gtk.HBox() grid.add(zeroconf_box) # Zeroconf list self._zeroconf_list = Gtk.TreeView(self._services) self._zeroconf_list.get_selection().set_mode(Gtk.SelectionMode.SINGLE) renderer = Gtk.CellRendererText() column = Gtk.TreeViewColumn("Zeroconf", renderer, text=0) self._zeroconf_list.append_column(column) zeroconf_box.pack_start(self._zeroconf_list, True, True, 0) # Separator separator = Gtk.Separator.new(Gtk.Orientation.VERTICAL) zeroconf_box.pack_end(separator, False, False, 5) # Connection grid connection_grid = Gtk.Grid() grid.attach_next_to(connection_grid, zeroconf_box, Gtk.PositionType.RIGHT, 1, 1) # Host host_label = Gtk.Label("Host:") host_label.set_alignment(0, 0.5) connection_grid.add(host_label) self._host_entry = Gtk.Entry() self._host_entry.set_text("localhost") connection_grid.attach_next_to(self._host_entry, host_label, Gtk.PositionType.BOTTOM, 1, 1) # Port port_label = Gtk.Label("Port:") port_label.set_alignment(0, 0.5) connection_grid.attach_next_to(port_label, self._host_entry, Gtk.PositionType.BOTTOM, 1, 1) adjustment = Gtk.Adjustment(6600, 1024, 9999, 1, 10, 10) self._port_spinner = Gtk.SpinButton() self._port_spinner.set_adjustment(adjustment) connection_grid.attach_next_to(self._port_spinner, port_label, Gtk.PositionType.BOTTOM, 1, 1) # Passwort password_label = Gtk.Label("Password:") password_label.set_alignment(0, 0.5) connection_grid.attach_next_to(password_label, self._port_spinner, Gtk.PositionType.BOTTOM, 1, 1) self._password_entry = Gtk.Entry() self._password_entry.set_input_purpose(Gtk.InputPurpose.PASSWORD) self._password_entry.set_visibility(False) connection_grid.attach_next_to(self._password_entry, password_label, Gtk.PositionType.BOTTOM, 1, 1) # Image dir image_dir_label = Gtk.Label("Image Dir:") image_dir_label.set_alignment(0, 0.5) connection_grid.attach_next_to(image_dir_label, self._password_entry, Gtk.PositionType.BOTTOM, 1, 1) self._image_dir_entry = Gtk.Entry() connection_grid.attach_next_to(self._image_dir_entry, image_dir_label, Gtk.PositionType.BOTTOM, 1, 1) # Zeroconf provider self._zeroconf_provider = ZeroconfProvider() self._zeroconf_provider.connect_signal(ZeroconfProvider.SIGNAL_SERVICE_NEW, self.on_new_service) # Signals self._zeroconf_list.get_selection().connect('changed', self.on_service_selected) self._zeroconf_list.connect('focus-out-event', self.on_zeroconf_list_outfocused) 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) # Actions #self._load_profiles() def get_name(self): return 'connection' def get_title(self): return "Server" def on_new_service(self, service): name, host, port = service self._services.append([name, host, port]) def on_service_selected(self, selection): model, treeiter = selection.get_selected() if treeiter != None: service = model[treeiter] self.set_host(service[1]) self.set_port(service[2]) def on_zeroconf_list_outfocused(self, widget, event): self._zeroconf_list.get_selection().unselect_all() def on_host_entry_outfocused(self, widget, event): self._call_back() def on_port_spinner_value_changed(self, widget): self._call_back() def on_password_entry_outfocused(self, widget, event): self._call_back() def on_image_dir_entry_outfocused(self, widget, event): self._call_back() def set_host(self, host): self._host_entry.set_text(host) def get_host(self): return self._host_entry.get_text() def set_port(self, port): self._port_spinner.set_value(port) 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 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 get_image_dir(self): return self._image_dir_entry.get_text() def _call_back(self): self._callback(ConnectionPanel.SIGNAL_CONNECTION_CHANGED, self.get_host(), self.get_port(), self.get_password(), self.get_image_dir()) class CoverPanel(Panel, Gtk.VBox): 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 = {} # 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) def get_name(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_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_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_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) 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_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 _playing(self): value = self._songs_scale.get_value() + 1 self._songs_scale.set_value(value) 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 _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() class PlaylistPanel(Panel, Gtk.VBox): 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() # 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) # Signals self._clear_playlist_button.connect('clicked' ,self._callback_from_widget, PlaylistPanel.SIGNAL_CLEAR_PLAYLIST) def get_name(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 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 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() 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 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 _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("search library") 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('search-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) class ZeroconfProvider(mcg.Base): SIGNAL_SERVICE_NEW = 'service-new' TYPE = '_mpd._tcp' def __init__(self): mcg.Base.__init__(self) self._service_resolvers = [] self._services = {} # Client self._client = Avahi.Client(flags=0,) self._client.start() # Browser self._service_browser = Avahi.ServiceBrowser(domain='local', flags=0, interface=-1, protocol=Avahi.Protocol.GA_PROTOCOL_UNSPEC, type=ZeroconfProvider.TYPE) self._service_browser.connect('new_service', self.on_new_service) self._service_browser.attach(self._client) def on_new_service(self, browser, interface, protocol, name, type, domain, flags): if not (flags & Avahi.LookupResultFlags.GA_LOOKUP_RESULT_LOCAL): service_resolver = Avahi.ServiceResolver(interface=interface, protocol=protocol, name=name, type=type, domain=domain, aprotocol=Avahi.Protocol.GA_PROTOCOL_UNSPEC, flags=0,) service_resolver.connect('found', self.on_found) service_resolver.connect('failure', self.on_failure) service_resolver.attach(self._client) self._service_resolvers.append(service_resolver) def on_found(self, resolver, interface, protocol, name, type, domain, host, date, port, *args): if (host, port) not in self._services.keys(): service = (name,host,port) self._services[(host,port)] = service self._callback(ZeroconfProvider.SIGNAL_SERVICE_NEW, service) def on_failure(self, resolver, date): if resolver in self._service_resolvers: self._service_resolvers.remove(resolver)