#!/usr/bin/python3 # -*- coding: utf-8 -*- # # mpdevil - MPD Client. # Copyright 2020 Martin Wagner # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3 of the License. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 # USA # MPRIS interface based on 'mpDris2' (master 19.03.2020) by Jean-Philippe Braun , Mantas Mikulėnas import gi gi.require_version('Gtk', '3.0') gi.require_version('Notify', '0.7') from gi.repository import Gtk, Gio, Gdk, GdkPixbuf, Pango, GObject, GLib, Notify from mpd import MPDClient, base as MPDBase import requests from bs4 import BeautifulSoup, Comment import threading import locale import gettext import datetime import os import sys import re # MPRIS modules import dbus import dbus.service from dbus.mainloop.glib import DBusGMainLoop import base64 DATADIR='@datadir@' NAME='mpdevil' VERSION='@version@' PACKAGE=NAME.lower() COVER_REGEX="^\.?(album|cover|folder|front).*\.(gif|jpeg|jpg|png)$" ################# # lang settings # ################# try: locale.setlocale(locale.LC_ALL, '') locale.bindtextdomain(PACKAGE, '@datadir@/locale') gettext.bindtextdomain(PACKAGE, '@datadir@/locale') gettext.textdomain(PACKAGE) gettext.install(PACKAGE, localedir='@datadir@/locale') except locale.Error: print(' cannot use system locale.') locale.setlocale(locale.LC_ALL, 'C') gettext.textdomain(PACKAGE) gettext.install(PACKAGE, localedir='@datadir@/locale') ######### # MPRIS # ######### class MPRISInterface(dbus.service.Object): # TODO emit Seeked if needed __introspect_interface="org.freedesktop.DBus.Introspectable" __prop_interface=dbus.PROPERTIES_IFACE # python dbus bindings don't include annotations and properties MPRIS2_INTROSPECTION=""" """ # MPRIS allowed metadata tags allowed_tags={ 'mpris:trackid': dbus.ObjectPath, 'mpris:length': dbus.Int64, 'mpris:artUrl': str, 'xesam:album': str, 'xesam:albumArtist': list, 'xesam:artist': list, 'xesam:asText': str, 'xesam:audioBPM': int, 'xesam:comment': list, 'xesam:composer': list, 'xesam:contentCreated': str, 'xesam:discNumber': int, 'xesam:firstUsed': str, 'xesam:genre': list, 'xesam:lastUsed': str, 'xesam:lyricist': str, 'xesam:title': str, 'xesam:trackNumber': int, 'xesam:url': str, 'xesam:useCount': int, 'xesam:userRating': float, } def __init__(self, window, client, settings): dbus.service.Object.__init__(self, dbus.SessionBus(), "/org/mpris/MediaPlayer2") self._name="org.mpris.MediaPlayer2.mpdevil" self._bus=dbus.SessionBus() self._uname=self._bus.get_unique_name() self._dbus_obj=self._bus.get_object("org.freedesktop.DBus", "/org/freedesktop/DBus") self._dbus_obj.connect_to_signal("NameOwnerChanged", self._name_owner_changed_callback, arg0=self._name) self.window=window self.client=client self.settings=settings self.metadata={} # connect self.client.emitter.connect("state", self.on_state_changed) self.client.emitter.connect("current_song_changed", self.on_song_changed) self.client.emitter.connect("volume_changed", self.on_volume_changed) self.client.emitter.connect("repeat", self.on_loop_changed) self.client.emitter.connect("single", self.on_loop_changed) self.client.emitter.connect("random", self.on_random_changed) def on_state_changed(self, *args): self.update_property('org.mpris.MediaPlayer2.Player', 'PlaybackStatus') self.update_property('org.mpris.MediaPlayer2.Player', 'CanGoNext') self.update_property('org.mpris.MediaPlayer2.Player', 'CanGoPrevious') def on_song_changed(self, *args): self.update_metadata() self.update_property('org.mpris.MediaPlayer2.Player', 'Metadata') def on_volume_changed(self, *args): self.update_property('org.mpris.MediaPlayer2.Player', 'Volume') def on_loop_changed(self, *args): self.update_property('org.mpris.MediaPlayer2.Player', 'LoopStatus') def on_random_changed(self, *args): self.update_property('org.mpris.MediaPlayer2.Player', 'Shuffle') def update_metadata(self): # TODO """ Translate metadata returned by MPD to the MPRIS v2 syntax. http://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata """ mpd_meta=self.client.wrapped_call("currentsong") self.metadata={} for tag in ('album', 'title'): if tag in mpd_meta: self.metadata['xesam:%s' % tag]=mpd_meta[tag] if 'id' in mpd_meta: self.metadata['mpris:trackid']="/org/mpris/MediaPlayer2/Track/%s" % mpd_meta['id'] if 'time' in mpd_meta: self.metadata['mpris:length']=int(mpd_meta['time']) * 1000000 if 'date' in mpd_meta: self.metadata['xesam:contentCreated']=mpd_meta['date'][0:4] if 'track' in mpd_meta: # TODO: Is it even *possible* for mpd_meta['track'] to be a list? if type(mpd_meta['track']) == list and len(mpd_meta['track']) > 0: track=str(mpd_meta['track'][0]) else: track=str(mpd_meta['track']) m=re.match('^([0-9]+)', track) if m: self.metadata['xesam:trackNumber']=int(m.group(1)) # Ensure the integer is signed 32bit if self.metadata['xesam:trackNumber'] & 0x80000000: self.metadata['xesam:trackNumber'] += -0x100000000 else: self.metadata['xesam:trackNumber']=0 if 'disc' in mpd_meta: # TODO: Same as above. When is it a list? if type(mpd_meta['disc']) == list and len(mpd_meta['disc']) > 0: disc=str(mpd_meta['disc'][0]) else: disc=str(mpd_meta['disc']) m=re.match('^([0-9]+)', disc) if m: self.metadata['xesam:discNumber']=int(m.group(1)) if 'artist' in mpd_meta: if type(mpd_meta['artist']) == list: self.metadata['xesam:artist']=mpd_meta['artist'] else: self.metadata['xesam:artist']=[mpd_meta['artist']] if 'composer' in mpd_meta: if type(mpd_meta['composer']) == list: self.metadata['xesam:composer']=mpd_meta['composer'] else: self.metadata['xesam:composer']=[mpd_meta['composer']] # Stream: populate some missings tags with stream's name if 'name' in mpd_meta: if 'xesam:title' not in self.metadata: self.metadata['xesam:title']=mpd_meta['name'] elif 'xesam:album' not in self.metadata: self.metadata['xesam:album']=mpd_meta['name'] if 'file' in mpd_meta: song_file=mpd_meta['file'] self.metadata['xesam:url']="file://"+os.path.join(self.settings.get_value("paths")[self.settings.get_int("active-profile")], song_file) cover=Cover(self.settings, mpd_meta) if not cover.path == None: self.metadata['mpris:artUrl']="file://"+cover.path else: self.metadata['mpris:artUrl']=None # Cast self.metadata to the correct type, or discard it for key, value in self.metadata.items(): try: self.metadata[key]=self.allowed_tags[key](value) except ValueError: del self.metadata[key] def _name_owner_changed_callback(self, name, old_owner, new_owner): if name == self._name and old_owner == self._uname and new_owner != "": try: pid=self._dbus_obj.GetConnectionUnixProcessID(new_owner) except: pid=None loop.quit() def acquire_name(self): self._bus_name=dbus.service.BusName(self._name, bus=self._bus, allow_replacement=True, replace_existing=True) def release_name(self): if hasattr(self, "_bus_name"): del self._bus_name __root_interface="org.mpris.MediaPlayer2" __root_props={ "CanQuit": (False, None), "CanRaise": (True, None), "DesktopEntry": ("mpdevil", None), "HasTrackList": (False, None), "Identity": ("mpdevil", None), "SupportedUriSchemes": (dbus.Array(signature="s"), None), "SupportedMimeTypes": (dbus.Array(signature="s"), None) } def __get_playback_status(self): status=self.client.wrapped_call("status") return {'play': 'Playing', 'pause': 'Paused', 'stop': 'Stopped'}[status['state']] def __set_loop_status(self, value): if value == "Playlist": self.client.wrapped_call("repeat", 1) self.client.wrapped_call("single", 0) elif value == "Track": self.client.wrapped_call("repeat", 1) self.client.wrapped_call("single", 1) elif value == "None": self.client.wrapped_call("repeat", 0) self.client.wrapped_call("single", 0) else: raise dbus.exceptions.DBusException("Loop mode %r not supported" % value) return def __get_loop_status(self): status=self.client.wrapped_call("status") if int(status['repeat']) == 1: if int(status.get('single', 0)) == 1: return "Track" else: return "Playlist" else: return "None" def __set_shuffle(self, value): self.client.wrapped_call("random", value) return def __get_shuffle(self): if int(self.client.wrapped_call("status")['random']) == 1: return True else: return False def __get_metadata(self): return dbus.Dictionary(self.metadata, signature='sv') def __get_volume(self): vol=float(self.client.wrapped_call("status").get('volume', 0)) if vol > 0: return vol / 100.0 else: return 0.0 def __set_volume(self, value): if value >= 0 and value <= 1: self.client.wrapped_call("setvol", int(value * 100)) return def __get_position(self): status=self.client.wrapped_call("status") if 'time' in status: current, end=status['time'].split(':') return dbus.Int64((int(current) * 1000000)) else: return dbus.Int64(0) def __get_can_next_prev(self): status=self.client.wrapped_call("status") if status['state'] == "stop": return False else: return True __player_interface="org.mpris.MediaPlayer2.Player" __player_props={ "PlaybackStatus": (__get_playback_status, None), "LoopStatus": (__get_loop_status, __set_loop_status), "Rate": (1.0, None), "Shuffle": (__get_shuffle, __set_shuffle), "Metadata": (__get_metadata, None), "Volume": (__get_volume, __set_volume), "Position": (__get_position, None), "MinimumRate": (1.0, None), "MaximumRate": (1.0, None), "CanGoNext": (__get_can_next_prev, None), "CanGoPrevious": (__get_can_next_prev, None), "CanPlay": (True, None), "CanPause": (True, None), "CanSeek": (True, None), "CanControl": (True, None), } __prop_mapping={ __player_interface: __player_props, __root_interface: __root_props, } @dbus.service.method(__introspect_interface) def Introspect(self): return self.MPRIS2_INTROSPECTION @dbus.service.signal(__prop_interface, signature="sa{sv}as") def PropertiesChanged(self, interface, changed_properties, invalidated_properties): pass @dbus.service.method(__prop_interface, in_signature="ss", out_signature="v") def Get(self, interface, prop): getter, setter=self.__prop_mapping[interface][prop] if callable(getter): return getter(self) return getter @dbus.service.method(__prop_interface, in_signature="ssv", out_signature="") def Set(self, interface, prop, value): getter, setter=self.__prop_mapping[interface][prop] if setter is not None: setter(self, value) @dbus.service.method(__prop_interface, in_signature="s", out_signature="a{sv}") def GetAll(self, interface): read_props={} props=self.__prop_mapping[interface] for key, (getter, setter) in props.items(): if callable(getter): getter=getter(self) read_props[key]=getter return read_props def update_property(self, interface, prop): getter, setter=self.__prop_mapping[interface][prop] if callable(getter): value=getter(self) else: value=getter self.PropertiesChanged(interface, {prop: value}, []) return value # Root methods @dbus.service.method(__root_interface, in_signature='', out_signature='') def Raise(self): self.window.present() return @dbus.service.method(__root_interface, in_signature='', out_signature='') def Quit(self): return # Player methods @dbus.service.method(__player_interface, in_signature='', out_signature='') def Next(self): self.client.wrapped_call("next") return @dbus.service.method(__player_interface, in_signature='', out_signature='') def Previous(self): self.client.wrapped_call("previous") return @dbus.service.method(__player_interface, in_signature='', out_signature='') def Pause(self): self.client.wrapped_call("pause", 1) return @dbus.service.method(__player_interface, in_signature='', out_signature='') def PlayPause(self): status=self.client.wrapped_call("status") if status['state'] == 'play': self.client.wrapped_call("pause", 1) else: self.client.wrapped_call("play") return @dbus.service.method(__player_interface, in_signature='', out_signature='') def Stop(self): self.client.wrapped_call("stop") return @dbus.service.method(__player_interface, in_signature='', out_signature='') def Play(self): self.client.wrapped_call("play") return @dbus.service.method(__player_interface, in_signature='x', out_signature='') def Seek(self, offset): # TODO status=self.client.wrapped_call("status") current, end=status['time'].split(':') current=int(current) end=int(end) offset=int(offset) / 1000000 if current + offset <= end: position=current + offset if position < 0: position=0 self.client.wrapped_call("seekid", int(status['songid']), position) self.Seeked(position * 1000000) return @dbus.service.method(__player_interface, in_signature='ox', out_signature='') def SetPosition(self, trackid, position): song=self.client.wrapped_call("currentsong") # FIXME: use real dbus objects if str(trackid) != '/org/mpris/MediaPlayer2/Track/%s' % song['id']: return # Convert position to seconds position=int(position) / 1000000 if position <= int(song['time']): self.client.wrapped_call("seekid", int(song['id']), position) self.Seeked(position * 1000000) return @dbus.service.signal(__player_interface, signature='x') def Seeked(self, position): return float(position) @dbus.service.method(__player_interface, in_signature='', out_signature='') def OpenUri(self): return ################################# # small general purpose widgets # ################################# class IntEntry(Gtk.SpinButton): def __init__(self, default, lower, upper, step): Gtk.SpinButton.__init__(self) adj=Gtk.Adjustment(value=default, lower=lower, upper=upper, step_increment=step) self.set_adjustment(adj) def get_int(self): return int(self.get_value()) def set_int(self, value): self.set_value(value) class PixelSizedIcon(Gtk.Image): def __init__(self, icon_name, pixel_size): Gtk.Image.__init__(self) self.set_from_icon_name(icon_name, Gtk.IconSize.BUTTON) if pixel_size > 0: self.set_pixel_size(pixel_size) class FocusFrame(Gtk.Overlay): def __init__(self): Gtk.Overlay.__init__(self) self.frame=Gtk.Frame() self.frame.set_no_show_all(True) self.style_context=self.frame.get_style_context() self.provider=Gtk.CssProvider() css=b"""* {border-color: @theme_selected_bg_color; border-width: 2px;}""" self.provider.load_from_data(css) self.style_context.add_provider(self.provider, 800) self.add_overlay(self.frame) self.set_overlay_pass_through(self.frame, True) def set_widget(self, widget): widget.connect("focus-in-event", self.on_focus_in_event) widget.connect("focus-out-event", self.on_focus_out_event) def on_focus_in_event(self, *args): self.frame.show() def on_focus_out_event(self, *args): self.frame.hide() class SongPopover(Gtk.Popover): def __init__(self, song, relative, x, y): Gtk.Popover.__init__(self) rect=Gdk.Rectangle() rect.x=x # Gtk places popovers 26px above the given position for no obvious reasons, so I move them 26px rect.y=y+26 rect.width = 1 rect.height = 1 self.set_pointing_to(rect) self.set_relative_to(relative) # Store # (tag, display-value, tooltip) self.store=Gtk.ListStore(str, str, str) # TreeView self.treeview=Gtk.TreeView(model=self.store) self.treeview.set_can_focus(False) self.treeview.set_search_column(-1) self.treeview.set_tooltip_column(2) self.treeview.set_headers_visible(False) sel=self.treeview.get_selection() sel.set_mode(Gtk.SelectionMode.NONE) frame=Gtk.Frame() frame.add(self.treeview) frame.set_property("border-width", 3) # Column renderer_text=Gtk.CellRendererText(width_chars=50, ellipsize=Pango.EllipsizeMode.MIDDLE, ellipsize_set=True) renderer_text_ralign=Gtk.CellRendererText(xalign=1.0) self.column_tag=Gtk.TreeViewColumn(_("MPD-Tag"), renderer_text_ralign, text=0) self.column_tag.set_property("resizable", False) self.treeview.append_column(self.column_tag) self.column_value=Gtk.TreeViewColumn(_("Value"), renderer_text, text=1) self.column_value.set_property("resizable", False) self.treeview.append_column(self.column_value) # packing self.add(frame) song=ClientHelper.song_to_str_dict(song) for tag, value in song.items(): tooltip=value.replace("&", "&") if tag == "time": self.store.append([tag+":", str(datetime.timedelta(seconds=int(value))), tooltip]) elif tag == "last-modified": time=datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ') self.store.append([tag+":", time.strftime('%a %d %B %Y, %H:%M UTC'), tooltip]) else: self.store.append([tag+":", value, tooltip]) frame.show_all() class Cover(object): def __init__(self, settings, song): self.path=None if song != {}: song_file=song["file"] active_profile=settings.get_int("active-profile") self.lib_path=settings.get_value("paths")[active_profile] regex_str=settings.get_value("regex")[active_profile] if regex_str == "": self.regex=re.compile(r''+COVER_REGEX+'', flags=re.IGNORECASE) else: try: artist=song["albumartist"] except: artist="" try: album=song["album"] except: album="" regex_str=regex_str.replace("%AlbumArtist%", artist) regex_str=regex_str.replace("%Album%", album) try: self.regex=re.compile(r''+regex_str+'', flags=re.IGNORECASE) except: print("illegal regex:", regex_str) if not song_file == None: head, tail=os.path.split(song_file) song_dir=os.path.join(self.lib_path, head) if os.path.exists(song_dir): for f in os.listdir(song_dir): if self.regex.match(f): self.path=os.path.join(song_dir, f) break def get_pixbuf(self, size): if self.path == None: self.path=Gtk.IconTheme.get_default().lookup_icon("media-optical", size, Gtk.IconLookupFlags.FORCE_SVG).get_filename() # fallback cover return GdkPixbuf.Pixbuf.new_from_file_at_size(self.path, size, size) ###################### # MPD client wrapper # ###################### class ClientHelper(): def song_to_str_dict(song): # converts tags with multiple values to comma separated strings return_song=song for tag, value in return_song.items(): if type(value) == list: return_song[tag]=(', '.join(value)) return return_song def song_to_first_str_dict(song): # extracts the first value of multiple value tags return_song=song for tag, value in return_song.items(): if type(value) == list: return_song[tag]=value[0] return return_song def extend_song_for_display(song): base_song={"title": _("Unknown Title"), "track": "0", "disc": "", "artist": _("Unknown Artist"), "album": _("Unknown Album"), "duration": "0.0", "date": "", "genre": ""} base_song.update(song) base_song["human_duration"]=str(datetime.timedelta(seconds=int(float(base_song["duration"])))).lstrip("0").lstrip(":") return base_song def calc_display_length(songs): length=float(0) for song in songs: try: dura=float(song["duration"]) except: dura=0.0 length=length+dura return str(datetime.timedelta(seconds=int(length))).lstrip("0").lstrip(":") class MpdEventEmitter(GObject.Object): __gsignals__={ 'update': (GObject.SignalFlags.RUN_FIRST, None, ()), 'disconnected': (GObject.SignalFlags.RUN_FIRST, None, ()), 'reconnected': (GObject.SignalFlags.RUN_FIRST, None, ()), 'current_song_changed': (GObject.SignalFlags.RUN_FIRST, None, ()), 'state': (GObject.SignalFlags.RUN_FIRST, None, (str,)), 'elapsed_changed': (GObject.SignalFlags.RUN_FIRST, None, (float,float,)), 'volume_changed': (GObject.SignalFlags.RUN_FIRST, None, (float,)), 'playlist_changed': (GObject.SignalFlags.RUN_FIRST, None, (int,)), 'repeat': (GObject.SignalFlags.RUN_FIRST, None, (bool,)), 'random': (GObject.SignalFlags.RUN_FIRST, None, (bool,)), 'single': (GObject.SignalFlags.RUN_FIRST, None, (bool,)), 'consume': (GObject.SignalFlags.RUN_FIRST, None, (bool,)), 'audio': (GObject.SignalFlags.RUN_FIRST, None, (float,int,int,)), 'bitrate': (GObject.SignalFlags.RUN_FIRST, None, (float,)) } def __init__(self): super().__init__() # gsignals def do_update(self): pass def do_disconnected(self): pass def do_reconnected(self): pass def do_current_file_changed(self): pass def do_state(self, state): pass def do_elapsed_changed(self, elapsed, duration): pass def do_volume_changed(self, volume): pass def do_playlist_changed(self, version): pass def do_audio(self, sampelrate, bits, channels): pass def do_bitrate(self, rate): pass class Client(MPDClient): def __init__(self, settings): MPDClient.__init__(self) self.settings=settings self.settings.connect("changed::active-profile", self.on_settings_changed) # adding vars self.settings=settings self.emitter=MpdEventEmitter() self.last_status={} self.current_file=None def wrapped_call(self, name, *args): try: func=getattr(self, name) except: raise ValueError return func(*args) def start(self): if self.disconnected_loop(): self.disconnected_timeout_id=GLib.timeout_add(1000, self.disconnected_loop) def connected(self): try: self.wrapped_call("ping") return True except: return False def on_settings_changed(self, *args): self.disconnect() def files_to_playlist(self, files, append, force=False): if append: for f in files: self.add(f) else: if self.settings.get_boolean("force-mode") or force or self.status()["state"] == "stop": if not files == []: self.clear() for f in files: self.add(f) self.play() else: status=self.status() self.moveid(status["songid"], 0) current_song_file=self.playlistinfo()[0]["file"] try: self.delete((1,)) # delete all songs, but the first. bad song index possible except: pass for f in files: if not f == current_song_file: self.add(f) else: self.move(0, (len(self.playlistinfo())-1)) def album_to_playlist(self, album, artist, year, append, force=False): songs=self.find("album", album, "date", year, self.settings.get_artist_type(), artist) self.files_to_playlist([song['file'] for song in songs], append, force) def comp_list(self, *args): # simulates listing behavior of python-mpd2 1.0 if "group" in args: raise ValueError("'group' is not supported") native_list=self.list(*args) if len(native_list) > 0: if type(native_list[0]) == dict: return ([l[args[0]] for l in native_list]) else: return native_list else: return([]) def get_metadata(self, uri): meta_base=self.lsinfo(uri)[0] meta_extra=self.readcomments(uri) # contains comment tag meta_base.update(meta_extra) return meta_base def loop(self, *args): try: status=self.status() diff=set(status.items())-set(self.last_status.items()) for key, val in diff: if key == "elapsed": self.emitter.emit("elapsed_changed", float(val), float(status["duration"])) elif key == "bitrate": self.emitter.emit("bitrate", float(val)) elif key == "songid": self.emitter.emit("current_song_changed") elif key == "state": self.emitter.emit("state", val) elif key == "audio": samplerate, bits, channels=val.split(':') self.emitter.emit("audio", float(samplerate), int(bits), int(channels)) elif key == "volume": self.emitter.emit("volume_changed", float(val)) elif key == "playlist": self.emitter.emit("playlist_changed", int(val)) elif key in ["repeat", "random", "single", "consume"]: if val == "1": self.emitter.emit(key, True) else: self.emitter.emit(key, False) diff=set(self.last_status)-set(status) if "songid" in diff: self.emitter.emit("current_song_changed") if "volume" in diff: self.emitter.emit("volume_changed", 0) if "updating_db" in diff: self.emitter.emit("update") self.last_status=status except MPDBase.ConnectionError: self.last_status={} self.emitter.emit("disconnected") if self.disconnected_loop(): self.disconnected_timeout_id=GLib.timeout_add(1000, self.disconnected_loop) return False return True def disconnected_loop(self, *args): self.current_file=None active=self.settings.get_int("active-profile") try: self.connect(self.settings.get_value("hosts")[active], self.settings.get_value("ports")[active]) if self.settings.get_value("passwords")[active] != "": self.password(self.settings.get_value("passwords")[active]) except: print("connect failed") return True # connect successful self.main_timeout_id=GLib.timeout_add(100, self.loop) self.emitter.emit("reconnected") return False ######################## # gio settings wrapper # ######################## class Settings(Gio.Settings): BASE_KEY="org.mpdevil" def __init__(self): super().__init__(schema=self.BASE_KEY) # fix profile settings if len(self.get_value("profiles")) < (self.get_int("active-profile")+1): self.set_int("active-profile", 0) profile_keys=[('as', "profiles", "new profile"), ('as', "hosts", "localhost"), ('ai', "ports", 6600), ('as', "passwords", ""), ('as', "paths", ""), ('as', "regex", "")] profile_arrays=[] for vtype, key, default in profile_keys: profile_arrays.append(self.get_value(key).unpack()) max_len=max(len(x) for x in profile_arrays) for index, (vtype, key, default) in enumerate(profile_keys): profile_arrays[index]=(profile_arrays[index]+max_len*[default])[:max_len] self.set_value(key, GLib.Variant(vtype, profile_arrays[index])) def array_append(self, vtype, key, value): # append to Gio.Settings (self.settings) array array=self.get_value(key).unpack() array.append(value) self.set_value(key, GLib.Variant(vtype, array)) def array_delete(self, vtype, key, pos): # delete entry of Gio.Settings (self.settings) array array=self.get_value(key).unpack() array.pop(pos) self.set_value(key, GLib.Variant(vtype, array)) def array_modify(self, vtype, key, pos, value): # modify entry of Gio.Settings (self.settings) array array=self.get_value(key).unpack() array[pos]=value self.set_value(key, GLib.Variant(vtype, array)) def get_gtk_icon_size(self, key): icon_size=self.get_int(key) sizes=[(48, Gtk.IconSize.DIALOG), (32, Gtk.IconSize.DND), (24, Gtk.IconSize.LARGE_TOOLBAR), (16, Gtk.IconSize.BUTTON)] for pixel_size, gtk_size in sizes: if icon_size >= pixel_size: return gtk_size return Gtk.IconSize.INVALID def get_artist_type(self): if self.get_boolean("use-album-artist"): return ("albumartist") else: return ("artist") ########### # browser # ########### class SearchWindow(Gtk.Box): def __init__(self, client): Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) # adding vars self.client=client # tag switcher self.tags=Gtk.ComboBoxText() # search entry self.search_entry=Gtk.SearchEntry() # label self.label=Gtk.Label() self.label.set_xalign(1) self.label.set_margin_end(6) # store # (track, title, artist, album, duration, file) self.store=Gtk.ListStore(int, str, str, str, str, str) # songs view self.songs_view=SongsView(self.client, self.store, 5) # columns renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True) renderer_text_ralign=Gtk.CellRendererText(xalign=1.0) self.column_track=Gtk.TreeViewColumn(_("No"), renderer_text_ralign, text=0) self.column_track.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) self.column_track.set_property("resizable", False) self.songs_view.append_column(self.column_track) self.column_title=Gtk.TreeViewColumn(_("Title"), renderer_text, text=1) self.column_title.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) self.column_title.set_property("resizable", False) self.column_title.set_property("expand", True) self.songs_view.append_column(self.column_title) self.column_artist=Gtk.TreeViewColumn(_("Artist"), renderer_text, text=2) self.column_artist.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) self.column_artist.set_property("resizable", False) self.column_artist.set_property("expand", True) self.songs_view.append_column(self.column_artist) self.column_album=Gtk.TreeViewColumn(_("Album"), renderer_text, text=3) self.column_album.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) self.column_album.set_property("resizable", False) self.column_album.set_property("expand", True) self.songs_view.append_column(self.column_album) self.column_time=Gtk.TreeViewColumn(_("Length"), renderer_text, text=4) self.column_time.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) self.column_time.set_property("resizable", False) self.songs_view.append_column(self.column_time) self.column_track.set_sort_column_id(0) self.column_title.set_sort_column_id(1) self.column_artist.set_sort_column_id(2) self.column_album.set_sort_column_id(3) self.column_time.set_sort_column_id(4) # scroll scroll=Gtk.ScrolledWindow() scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scroll.add(self.songs_view) # buttons self.add_button=Gtk.Button(image=Gtk.Image(stock=Gtk.STOCK_ADD), label=_("Add")) self.add_button.set_sensitive(False) self.add_button.set_relief(Gtk.ReliefStyle.NONE) self.play_button=Gtk.Button(image=Gtk.Image(stock=Gtk.STOCK_MEDIA_PLAY), label=_("Play")) self.play_button.set_sensitive(False) self.play_button.set_relief(Gtk.ReliefStyle.NONE) self.open_button=Gtk.Button(image=Gtk.Image(stock=Gtk.STOCK_OPEN), label=_("Open")) self.open_button.set_sensitive(False) self.open_button.set_relief(Gtk.ReliefStyle.NONE) # connect self.search_entry.connect("search-changed", self.on_search_changed) self.tags.connect("changed", self.on_search_changed) self.add_button.connect("clicked", self.on_add_clicked) self.play_button.connect("clicked", self.on_play_clicked) self.open_button.connect("clicked", self.on_open_clicked) self.client.emitter.connect("reconnected", self.on_reconnected) # packing vbox=Gtk.Box(spacing=6) vbox.set_property("border-width", 6) vbox.pack_start(self.search_entry, True, True, 0) vbox.pack_end(self.tags, False, False, 0) frame=FocusFrame() frame.set_widget(self.songs_view) frame.add(scroll) ButtonBox=Gtk.ButtonBox(spacing=1) ButtonBox.set_property("border-width", 1) ButtonBox.pack_start(self.add_button, True, True, 0) ButtonBox.pack_start(self.play_button, True, True, 0) ButtonBox.pack_start(self.open_button, True, True, 0) hbox=Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) hbox.pack_start(ButtonBox, 0, False, False) hbox.pack_end(self.label, 0, False, False) self.pack_start(vbox, False, False, 0) self.pack_start(Gtk.Separator.new(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0) self.pack_start(frame, True, True, 0) self.pack_start(Gtk.Separator.new(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0) self.pack_start(hbox, False, False, 0) def start(self): self.search_entry.grab_focus() def started(self): return self.search_entry.has_focus() def clear(self, *args): self.songs_view.clear() self.search_entry.set_text("") self.tags.remove_all() def on_reconnected(self, *args): self.tags.append_text("any") for tag in self.client.wrapped_call("tagtypes"): if not tag.startswith("MUSICBRAINZ"): self.tags.append_text(tag) self.tags.set_active(0) def on_search_changed(self, widget): self.songs_view.clear() self.label.set_text("") if len(self.search_entry.get_text()) > 1: songs=self.client.wrapped_call("search", self.tags.get_active_text(), self.search_entry.get_text()) for s in songs: song=ClientHelper.extend_song_for_display(ClientHelper.song_to_str_dict(s)) self.store.append([int(song["track"]), song["title"], song["artist"], song["album"], song["human_duration"], song["file"]]) self.label.set_text(_("hits: %i") % (self.songs_view.count())) if self.songs_view.count() == 0: self.add_button.set_sensitive(False) self.play_button.set_sensitive(False) self.open_button.set_sensitive(False) else: self.add_button.set_sensitive(True) self.play_button.set_sensitive(True) self.open_button.set_sensitive(True) def on_add_clicked(self, *args): self.client.wrapped_call("files_to_playlist", self.songs_view.get_files(), True) def on_play_clicked(self, *args): self.client.wrapped_call("files_to_playlist", self.songs_view.get_files(), False, True) def on_open_clicked(self, *args): self.client.wrapped_call("files_to_playlist", self.songs_view.get_files(), False) class SongsView(Gtk.TreeView): def __init__(self, client, store, file_column_id): Gtk.TreeView.__init__(self) self.set_model(store) self.set_search_column(-1) self.columns_autosize() # add vars self.client=client self.store=store self.file_column_id=file_column_id # selection self.selection=self.get_selection() self.selection.set_mode(Gtk.SelectionMode.SINGLE) # connect self.connect("row-activated", self.on_row_activated) self.connect("button-press-event", self.on_button_press_event) self.key_press_event=self.connect("key-press-event", self.on_key_press_event) def on_row_activated(self, widget, path, view_column): self.client.wrapped_call("files_to_playlist", [self.store[path][self.file_column_id]], False, True) def on_button_press_event(self, widget, event): if event.button == 1 and event.type == Gdk.EventType.BUTTON_PRESS: try: path=widget.get_path_at_pos(int(event.x), int(event.y))[0] self.client.wrapped_call("files_to_playlist", [self.store[path][self.file_column_id]], False) except: pass elif event.button == 2 and event.type == Gdk.EventType.BUTTON_PRESS: try: path=widget.get_path_at_pos(int(event.x), int(event.y))[0] self.client.wrapped_call("files_to_playlist", [self.store[path][self.file_column_id]], True) except: pass elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS: try: path=widget.get_path_at_pos(int(event.x), int(event.y))[0] file_name=self.store[path][self.file_column_id] pop=SongPopover(self.client.wrapped_call("get_metadata", file_name), widget, int(event.x), int(event.y)) pop.popup() pop.show_all() except: pass def on_key_press_event(self, widget, event): self.handler_block(self.key_press_event) if event.keyval == 112: # p treeview, treeiter=self.selection.get_selected() if not treeiter == None: self.client.wrapped_call("files_to_playlist", [self.store.get_value(treeiter, self.file_column_id)], False) elif event.keyval == 97: # a treeview, treeiter=self.selection.get_selected() if not treeiter == None: self.client.wrapped_call("files_to_playlist", [self.store.get_value(treeiter, self.file_column_id)], True) elif event.keyval == 65383: # menu key treeview, treeiter=self.selection.get_selected() if not treeiter == None: path=self.store.get_path(treeiter) cell=self.get_cell_area(path, None) file_name=self.store[path][self.file_column_id] pop=SongPopover(self.client.wrapped_call("get_metadata", file_name), widget, int(cell.x), int(cell.y)) pop.popup() pop.show_all() self.handler_unblock(self.key_press_event) def clear(self): self.store.clear() def count(self): return len(self.store) def get_files(self): return_list=[] for row in self.store: return_list.append(row[self.file_column_id]) return return_list class AlbumDialog(Gtk.Dialog): def __init__(self, parent, client, settings, album, artist, year): Gtk.Dialog.__init__(self, transient_for=parent) self.add_buttons(Gtk.STOCK_ADD, Gtk.ResponseType.ACCEPT, Gtk.STOCK_MEDIA_PLAY, Gtk.ResponseType.YES, Gtk.STOCK_OPEN, Gtk.ResponseType.OK, Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE) # metadata self.album=album self.artist=artist self.year=year # adding vars self.client=client self.settings=settings songs=self.client.wrapped_call("find", "album", self.album, "date", self.year, self.settings.get_artist_type(), self.artist) # determine size size=parent.get_size() diagonal=(size[0]**2+size[1]**2)**(0.5) h=diagonal//4 w=h*5//4 self.set_default_size(w, h) # title album_duration=ClientHelper.calc_display_length(songs) if year == "": self.set_title(artist+" - "+album+" ("+album_duration+")") else: self.set_title(artist+" - "+album+" ("+year+") ("+album_duration+")") # store # (track, title (artist), duration, file) self.store=Gtk.ListStore(int, str, str, str) # songs view self.songs_view=SongsView(self.client, self.store, 3) for s in songs: song=ClientHelper.extend_song_for_display(s) if type(song["title"]) == list: # could be impossible title=(', '.join(song["title"])) else: title=song["title"] if type(song["artist"]) == list: try: song["artist"].remove(self.artist) except: pass artist=(', '.join(song["artist"])) else: artist=song["artist"] if artist != self.artist: title_artist=""+title+" - "+artist else: title_artist=""+title+"" title_artist=title_artist.replace("&", "&") self.store.append([int(song["track"]), title_artist, song["human_duration"], song["file"]]) # columns renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True) renderer_text_ralign=Gtk.CellRendererText(xalign=1.0) self.column_track=Gtk.TreeViewColumn(_("No"), renderer_text_ralign, text=0) self.column_track.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) self.column_track.set_property("resizable", False) self.songs_view.append_column(self.column_track) self.column_title=Gtk.TreeViewColumn(_("Title"), renderer_text, markup=1) self.column_title.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) self.column_title.set_property("resizable", False) self.column_title.set_property("expand", True) self.songs_view.append_column(self.column_title) self.column_time=Gtk.TreeViewColumn(_("Length"), renderer_text, text=2) self.column_time.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) self.column_time.set_property("resizable", False) self.songs_view.append_column(self.column_time) # scroll scroll=Gtk.ScrolledWindow() scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scroll.add(self.songs_view) # packing self.vbox.pack_start(scroll, True, True, 0) # vbox default widget of dialogs self.vbox.set_spacing(3) self.show_all() def open(self): response=self.run() if response == Gtk.ResponseType.OK: self.client.wrapped_call("album_to_playlist", self.album, self.artist, self.year, False) elif response == Gtk.ResponseType.ACCEPT: self.client.wrapped_call("album_to_playlist", self.album, self.artist, self.year, True) elif response == Gtk.ResponseType.YES: self.client.wrapped_call("album_to_playlist", self.album, self.artist, self.year, False, True) class GenreSelect(Gtk.ComboBoxText): def __init__(self, client, settings): Gtk.ComboBoxText.__init__(self) # adding vars self.client=client self.settings=settings # connect self.changed=self.connect("changed", self.on_changed) self.client.emitter.connect("reconnected", self.refresh) self.update_signal=self.client.emitter.connect("update", self.refresh) def deactivate(self): self.set_active(0) def refresh(self, *args): self.handler_block(self.changed) self.remove_all() self.append_text(_("all genres")) for genre in self.client.wrapped_call("comp_list", "genre"): self.append_text(genre) self.set_active(0) self.handler_unblock(self.changed) def clear(self, *args): self.handler_block(self.changed) self.remove_all() self.handler_unblock(self.changed) def get_value(self): if self.get_active() == 0: return None else: return self.get_active_text() @GObject.Signal def genre_changed(self): pass def on_changed(self, *args): self.emit("genre_changed") class ArtistView(FocusFrame): def __init__(self, client, settings, genre_select): FocusFrame.__init__(self) # adding vars self.client=client self.settings=settings self.genre_select=genre_select # artistStore # (name, weight, initial-letter, weight-initials) self.store=Gtk.ListStore(str, Pango.Weight, str, Pango.Weight) # TreeView self.treeview=Gtk.TreeView(model=self.store) self.treeview.set_search_column(0) self.treeview.columns_autosize() self.treeview.set_property("activate-on-single-click", True) # artistSelection self.selection=self.treeview.get_selection() self.selection.set_mode(Gtk.SelectionMode.SINGLE) # Columns renderer_text_malign=Gtk.CellRendererText(xalign=0.5) self.column_initials=Gtk.TreeViewColumn("", renderer_text_malign, text=2, weight=3) self.column_initials.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) self.column_initials.set_property("resizable", False) self.column_initials.set_visible(self.settings.get_boolean("show-initials")) self.treeview.append_column(self.column_initials) renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True) self.column_name=Gtk.TreeViewColumn("", renderer_text, text=0, weight=1) self.column_name.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) self.column_name.set_property("resizable", False) self.treeview.append_column(self.column_name) # scroll scroll=Gtk.ScrolledWindow() scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scroll.add(self.treeview) # connect self.treeview.connect("row-activated", self.on_row_activated) self.settings.connect("changed::use-album-artist", self.refresh) self.settings.connect("changed::show-initials", self.on_show_initials_settings_changed) self.client.emitter.connect("reconnected", self.refresh) self.client.emitter.connect("update", self.refresh) self.genre_select.connect("genre_changed", self.refresh) self.set_widget(self.treeview) self.add(scroll) @GObject.Signal def artists_changed(self): pass def clear(self): self.store.clear() def refresh(self, *args): self.selection.set_mode(Gtk.SelectionMode.NONE) self.clear() if self.settings.get_artist_type() == "albumartist": self.column_name.set_title(_("Album Artist")) else: self.column_name.set_title(_("Artist")) self.store.append([_("all artists"), Pango.Weight.BOOK, "", Pango.Weight.BOOK]) genre=self.genre_select.get_value() if genre == None: artists=self.client.wrapped_call("comp_list", self.settings.get_artist_type()) else: artists=self.client.wrapped_call("comp_list", self.settings.get_artist_type(), "genre", genre) current_char="" for artist in artists: try: if current_char != artist[0]: self.store.append([artist, Pango.Weight.BOOK, artist[0], Pango.Weight.BOLD]) current_char=artist[0] else: self.store.append([artist, Pango.Weight.BOOK, "", Pango.Weight.BOOK]) except: self.store.append([artist, Pango.Weight.BOOK, "", Pango.Weight.BOOK]) self.selection.set_mode(Gtk.SelectionMode.SINGLE) def get_selected_artists(self): artists=[] if self.store[Gtk.TreePath(0)][1] == Pango.Weight.BOLD: for row in self.store: artists.append(row[0]) return artists[1:] else: for row in self.store: if row[1] == Pango.Weight.BOLD: artists.append(row[0]) break return artists def highlight_selected(self): for path, row in enumerate(self.store): if row[1] == Pango.Weight.BOLD: self.treeview.set_cursor(path, None, False) break def on_row_activated(self, widget, path, view_column): for row in self.store: # reset bold text row[1]=Pango.Weight.BOOK self.store[path][1]=Pango.Weight.BOLD self.emit("artists_changed") def on_show_initials_settings_changed(self, *args): self.column_initials.set_visible(self.settings.get_boolean("show-initials")) class AlbumIconView(Gtk.IconView): def __init__(self, client, settings, genre_select, window): Gtk.IconView.__init__(self) # adding vars self.settings=settings self.client=client self.genre_select=genre_select self.window=window self.stop_flag=True self.button_event=(None, None) # cover, display_label, display_label_artist, tooltip(titles), album, year, artist self.store=Gtk.ListStore(GdkPixbuf.Pixbuf, str, str, str, str, str, str) self.sort_settings() # iconview self.set_model(self.store) self.set_pixbuf_column(0) self.set_markup_column(1) self.set_item_width(0) self.tooltip_settings() # connect self.connect("item-activated", self.on_item_activated) self.connect("button-release-event", self.on_button_release_event) self.connect("button-press-event", self.on_button_press_event) self.key_press_event=self.connect("key-press-event", self.on_key_press_event) self.settings.connect("changed::show-album-view-tooltips", self.tooltip_settings) self.settings.connect("changed::sort-albums-by-year", self.sort_settings) @GObject.Signal def done(self): self.stop_flag=True pass def tooltip_settings(self, *args): if self.settings.get_boolean("show-album-view-tooltips"): self.set_tooltip_column(3) else: self.set_tooltip_column(-1) def sort_settings(self, *args): if self.settings.get_boolean("sort-albums-by-year"): self.store.set_sort_column_id(5, Gtk.SortType.ASCENDING) else: self.store.set_sort_column_id(1, Gtk.SortType.ASCENDING) return False def add_row(self, row): self.store.append(row) return False def populate(self, artists): self.stop_flag=False # prepare albmus list self.store.clear() if len(artists) > 1: self.set_markup_column(2) else: self.set_markup_column(1) albums=[] genre=self.genre_select.get_value() artist_type=self.settings.get_artist_type() for artist in artists: try: # client cloud meanwhile disconnect if not self.stop_flag: if genre == None: album_candidates=self.client.wrapped_call("comp_list", "album", artist_type, artist) else: album_candidates=self.client.wrapped_call("comp_list", "album", artist_type, artist, "genre", genre) for album in album_candidates: years=self.client.wrapped_call("comp_list", "date", "album", album, artist_type, artist) for year in years: songs=self.client.wrapped_call("find", "album", album, "date", year, artist_type, artist) albums.append({"artist": artist, "album": album, "year": year, "songs": songs}) while Gtk.events_pending(): Gtk.main_iteration_do(True) else: GLib.idle_add(self.emit, "done") return except MPDBase.ConnectionError: GLib.idle_add(self.emit, "done") return # display albums if self.settings.get_boolean("sort-albums-by-year"): albums=sorted(albums, key=lambda k: k['year']) else: albums=sorted(albums, key=lambda k: k['album']) size=self.settings.get_int("album-cover") for i, album in enumerate(albums): if not self.stop_flag: cover=Cover(self.settings, album["songs"][0]).get_pixbuf(size) # tooltip length_human_readable=ClientHelper.calc_display_length(album["songs"]) try: discs=int(album["songs"][-1]["disc"]) except: discs=1 if discs > 1: tooltip=(_("%(total_tracks)i titles on %(discs)i discs (%(total_length)s)") % {"total_tracks": len(album["songs"]), "discs": discs, "total_length": length_human_readable}) else: tooltip=(_("%(total_tracks)i titles (%(total_length)s)") % {"total_tracks": len(album["songs"]), "total_length": length_human_readable}) display_label=""+album["album"]+"" if album["year"] != "": display_label=display_label+" ("+album["year"]+")" display_label_artist=display_label+"\n"+album["artist"] display_label=display_label.replace("&", "&") display_label_artist=display_label_artist.replace("&", "&") GLib.idle_add(self.add_row, [cover, display_label, display_label_artist, tooltip, album["album"], album["year"], album["artist"]]) if i%16 == 0: while Gtk.events_pending(): Gtk.main_iteration_do(True) else: break GLib.idle_add(self.emit, "done") def scroll_to_selected_album(self): song=ClientHelper.song_to_first_str_dict(self.client.wrapped_call("currentsong")) self.unselect_all() row_num=len(self.store) for i in range(0, row_num): path=Gtk.TreePath(i) treeiter=self.store.get_iter(path) if self.store.get_value(treeiter, 4) == song["album"]: self.set_cursor(path, None, False) self.select_path(path) self.scroll_to_path(path, True, 0, 0) break def path_to_playlist(self, path, add, force=False): album=self.store[path][4] year=self.store[path][5] artist=self.store[path][6] self.client.wrapped_call("album_to_playlist", album, artist, year, add, force) def open_album_dialog(self, path): if self.client.connected(): album=self.store[path][4] year=self.store[path][5] artist=self.store[path][6] album_dialog=AlbumDialog(self.window, self.client, self.settings, album, artist, year) album_dialog.open() album_dialog.destroy() def on_button_press_event(self, widget, event): path=widget.get_path_at_pos(int(event.x), int(event.y)) if event.type == Gdk.EventType.BUTTON_PRESS: self.button_event=(event.button, path) def on_button_release_event(self, widget, event): path=widget.get_path_at_pos(int(event.x), int(event.y)) if not path == None: if self.button_event == (event.button, path): if event.button == 1 and event.type == Gdk.EventType.BUTTON_RELEASE: self.path_to_playlist(path, False) elif event.button == 2 and event.type == Gdk.EventType.BUTTON_RELEASE: self.path_to_playlist(path, True) elif event.button == 3 and event.type == Gdk.EventType.BUTTON_RELEASE: self.open_album_dialog(path) def on_key_press_event(self, widget, event): self.handler_block(self.key_press_event) if event.keyval == 112: # p paths=self.get_selected_items() if not len(paths) == 0: self.path_to_playlist(paths[0], False) elif event.keyval == 97: # a paths=self.get_selected_items() if not len(paths) == 0: self.path_to_playlist(paths[0], True) elif event.keyval == 65383: # menu key paths=self.get_selected_items() if not len(paths) == 0: self.open_album_dialog(paths[0]) self.handler_unblock(self.key_press_event) def on_item_activated(self, widget, path): treeiter=self.store.get_iter(path) selected_album=self.store.get_value(treeiter, 4) selected_album_year=self.store.get_value(treeiter, 5) selected_artist=self.store.get_value(treeiter, 6) self.client.wrapped_call("album_to_playlist", selected_album, selected_artist, selected_album_year, False, True) class AlbumView(FocusFrame): def __init__(self, client, settings, genre_select, window): FocusFrame.__init__(self) # adding vars self.settings=settings self.client=client self.genre_select=genre_select self.window=window self.artists=[] self.done=True self.pending=[] # iconview self.iconview=AlbumIconView(self.client, self.settings, self.genre_select, self.window) # scroll scroll=Gtk.ScrolledWindow() scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scroll.add(self.iconview) # connect self.settings.connect("changed::album-cover", self.on_settings_changed) self.iconview.connect("done", self.on_done) self.client.emitter.connect("update", self.clear) self.genre_select.connect("genre_changed", self.clear) self.settings.connect("changed::use-album-artist", self.clear) self.set_widget(self.iconview) self.add(scroll) def clear(self, *args): if self.done: self.iconview.store.clear() elif not self.clear in self.pending: self.iconview.stop_flag=True self.pending.append(self.clear) def refresh(self, artists): self.artists=artists if self.done: self.done=False self.populate() elif not self.populate in self.pending: self.iconview.stop_flag=True self.pending.append(self.populate) def populate(self): self.iconview.populate(self.artists) def scroll_to_selected_album(self): if self.done: self.iconview.scroll_to_selected_album() elif not self.scroll_to_selected_album in self.pending: self.pending.append(self.scroll_to_selected_album) def on_done(self, *args): self.done=True pending=self.pending self.pending=[] for p in pending: try: p() except: pass def on_settings_changed(self, *args): if self.done: self.populate() class Browser(Gtk.Paned): def __init__(self, client, settings, window): Gtk.Paned.__init__(self) # paned1 self.set_orientation(Gtk.Orientation.HORIZONTAL) # adding vars self.client=client self.settings=settings self.window=window self.use_csd=self.settings.get_boolean("use-csd") if self.use_csd: self.icon_size=0 else: self.icon_size=self.settings.get_int("icon-size") # widgets self.icons={} icons_data=["go-previous-symbolic", "system-search-symbolic"] for data in icons_data: self.icons[data]=PixelSizedIcon(data, self.icon_size) self.back_to_album_button=Gtk.Button(image=self.icons["go-previous-symbolic"]) self.back_to_album_button.set_tooltip_text(_("Back to current album")) self.search_button=Gtk.ToggleButton(image=self.icons["system-search-symbolic"]) self.search_button.set_tooltip_text(_("Search")) self.genre_select=GenreSelect(self.client, self.settings) self.artist_view=ArtistView(self.client, self.settings, self.genre_select) self.search=SearchWindow(self.client) self.album_view=AlbumView(self.client, self.settings, self.genre_select, self.window) # connect self.back_to_album_button.connect("clicked", self.back_to_album) self.search_button.connect("toggled", self.on_search_toggled) self.artist_view.connect("artists_changed", self.on_artists_changed) if not self.use_csd: self.settings.connect("changed::icon-size", self.on_icon_size_changed) self.client.emitter.connect("disconnected", self.on_disconnected) self.client.emitter.connect("reconnected", self.on_reconnected) # packing self.stack=Gtk.Stack() self.stack.set_transition_type(1) self.stack.add_named(self.album_view, "albums") self.stack.add_named(self.search, "search") if self.use_csd: self.pack1(self.artist_view, False, False) else: hbox=Gtk.Box(spacing=6) hbox.set_property("border-width", 6) hbox.pack_start(self.back_to_album_button, False, False, 0) hbox.pack_start(self.genre_select, True, True, 0) hbox.pack_start(self.search_button, False, False, 0) box1=Gtk.Box(orientation=Gtk.Orientation.VERTICAL) box1.pack_start(hbox, False, False, 0) box1.pack_start(Gtk.Separator.new(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0) box1.pack_start(self.artist_view, True, True, 0) self.pack1(box1, False, False) self.pack2(self.stack, True, False) self.set_position(self.settings.get_int("paned1")) def save_settings(self): self.settings.set_int("paned1", self.get_position()) def clear(self, *args): self.genre_select.clear() self.artist_view.clear() self.album_view.clear() self.search.clear() def search_started(self): return self.search.started() def back_to_album(self, *args): # TODO try: # since this can still be running when the connection is lost, various exceptions can occur song=ClientHelper.song_to_first_str_dict(self.client.wrapped_call("currentsong")) try: artist=song[self.settings.get_artist_type()] except: try: artist=song["artist"] except: artist="" try: if not song['genre'] == self.genre_select.get_value(): self.genre_select.deactivate() # deactivate genre filter to show all artists except: self.genre_select.deactivate() # deactivate genre filter to show all artists if len(self.artist_view.get_selected_artists()) <= 1: row_num=len(self.artist_view.store) for i in range(0, row_num): path=Gtk.TreePath(i) if self.artist_view.store[path][0] == artist: self.artist_view.treeview.set_cursor(path, None, False) if not self.artist_view.get_selected_artists() == [artist]: self.artist_view.treeview.row_activated(path, self.artist_view.column_name) else: self.search_button.set_active(False) self.artist_view.highlight_selected() break else: self.search_button.set_active(False) self.artist_view.treeview.set_cursor(Gtk.TreePath(0), None, False) # set cursor to 'all artists' self.album_view.scroll_to_selected_album() except: pass def on_search_toggled(self, widget): if widget.get_active(): self.stack.set_visible_child_name("search") self.search.start() else: self.stack.set_visible_child_name("albums") def on_reconnected(self, *args): self.back_to_album_button.set_sensitive(True) self.search_button.set_sensitive(True) self.genre_select.set_sensitive(True) def on_disconnected(self, *args): self.clear() self.back_to_album_button.set_sensitive(False) self.search_button.set_active(False) self.search_button.set_sensitive(False) self.genre_select.set_sensitive(False) def on_artists_changed(self, *args): self.search_button.set_active(False) artists=self.artist_view.get_selected_artists() self.album_view.refresh(artists) def on_icon_size_changed(self, *args): pixel_size=self.settings.get_int("icon-size") for icon in self.icons.values(): icon.set_pixel_size(pixel_size) ###################### # playlist and cover # ###################### class LyricsWindow(Gtk.Overlay): def __init__(self, client, settings): Gtk.Overlay.__init__(self) # adding vars self.settings=settings self.client=client # widgets self.text_view=Gtk.TextView() self.text_view.set_editable(False) self.text_view.set_left_margin(5) self.text_view.set_bottom_margin(5) self.text_view.set_cursor_visible(False) self.text_view.set_wrap_mode(Gtk.WrapMode.WORD) self.text_view.set_justification(Gtk.Justification.CENTER) self.text_buffer=self.text_view.get_buffer() # scroll self.scroll=Gtk.ScrolledWindow() self.scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.scroll.add(self.text_view) # frame frame=FocusFrame() frame.set_widget(self.text_view) style_context=frame.get_style_context() provider=Gtk.CssProvider() css=b"""* {border: 0px; background-color: @theme_base_color; opacity: 0.9;}""" provider.load_from_data(css) style_context.add_provider(provider, 800) # close button close_button=Gtk.ToggleButton(image=Gtk.Image.new_from_icon_name("window-close-symbolic", Gtk.IconSize.BUTTON)) close_button.set_margin_top(6) close_button.set_margin_end(6) style_context=close_button.get_style_context() style_context.add_class("circular") close_button.set_halign(2) close_button.set_valign(1) # connect self.song_changed=self.client.emitter.connect("current_song_changed", self.refresh) self.connect("destroy", self.remove_handlers) close_button.connect("clicked", self.on_close_button_clicked) # packing frame.add(self.scroll) self.add(frame) self.add_overlay(close_button) self.show_all() self.refresh() GLib.idle_add(self.text_view.grab_focus) # focus textview def remove_handlers(self, *args): self.client.emitter.disconnect(self.song_changed) def display_lyrics(self, current_song): GLib.idle_add(self.text_buffer.set_text, _("searching..."), -1) try: text=self.getLyrics(current_song["artist"], current_song["title"]) except: text=_("lyrics not found") GLib.idle_add(self.text_buffer.set_text, text, -1) def refresh(self, *args): update_thread=threading.Thread(target=self.display_lyrics, kwargs={"current_song": ClientHelper.song_to_first_str_dict(self.client.wrapped_call("currentsong"))}, daemon=True) update_thread.start() def getLyrics(self, singer, song): # partially copied from PyLyrics 1.1.0 # Replace spaces with _ singer=singer.replace(' ', '_') song=song.replace(' ', '_') r=requests.get('http://lyrics.wikia.com/{0}:{1}'.format(singer,song)) s=BeautifulSoup(r.text) # Get main lyrics holder lyrics=s.find("div",{'class':'lyricbox'}) if lyrics is None: raise ValueError("Song or Singer does not exist or the API does not have Lyrics") return None # Remove Scripts [s.extract() for s in lyrics('script')] # Remove Comments comments=lyrics.findAll(text=lambda text:isinstance(text, Comment)) [comment.extract() for comment in comments] # Remove span tag (Needed for instrumantal) if not lyrics.span == None: lyrics.span.extract() # Remove unecessary tags for tag in ['div','i','b','a']: for match in lyrics.findAll(tag): match.replaceWithChildren() # Get output as a string and remove non unicode characters and replace
with newlines output=str(lyrics).encode('utf-8', errors='replace')[22:-6:].decode("utf-8").replace('\n','').replace('
','\n') try: return output except: return output.encode('utf-8') def on_close_button_clicked(self, *args): self.destroy() class AudioType(Gtk.Label): def __init__(self, client): Gtk.Label.__init__(self) # adding vars self.client=client self.freq=0 self.res=0 self.chan=0 self.brate=0 self.file_type="" # connect self.client.emitter.connect("audio", self.on_audio) self.client.emitter.connect("bitrate", self.on_bitrate) self.client.emitter.connect("current_song_changed", self.on_song_changed) self.client.emitter.connect("disconnected", self.clear) self.client.emitter.connect("state", self.on_state) def clear(self, *args): self.set_text("") self.freq=0 self.res=0 self.chan=0 self.brate=0 self.file_type="" def refresh(self, *args): string=_("%(bitrate)s kb/s, %(frequency)s kHz, %(resolution)i bit, %(channels)i channels, %(file_type)s") % {"bitrate": str(self.brate), "frequency": str(self.freq), "resolution": self.res, "channels": self.chan, "file_type": self.file_type} self.set_text(string) def on_audio(self, emitter, freq, res, chan): self.freq=freq/1000 self.res=res self.chan=chan self.refresh() def on_bitrate(self, emitter, brate): self.brate=brate self.refresh() def on_song_changed(self, *args): try: self.file_type=self.client.wrapped_call("currentsong")["file"].split('.')[-1] self.refresh() except: pass def on_state(self, emitter, state): if state == "stop": self.clear() class MainCover(Gtk.Frame): def __init__(self, client, settings, window): Gtk.Frame.__init__(self) # diable auto resize self.set_halign(3) self.set_valign(3) # css style_context=self.get_style_context() provider=Gtk.CssProvider() css=b"""* {background-color: @theme_base_color; border-radius: 6px;}""" provider.load_from_data(css) style_context.add_provider(provider, 800) # adding vars self.client=client self.settings=settings self.window=window # event box event_box=Gtk.EventBox() event_box.set_property("border-width", 6) # cover self.cover=Gtk.Image.new() size=self.settings.get_int("track-cover") self.cover.set_from_pixbuf(Cover(self.settings, {}).get_pixbuf(size)) # set to fallback cover # set default size self.cover.set_size_request(size, size) # connect event_box.connect("button-press-event", self.on_button_press_event) self.client.emitter.connect("current_song_changed", self.refresh) self.settings.connect("changed::track-cover", self.on_settings_changed) event_box.add(self.cover) self.add(event_box) def refresh(self, *args): current_song=self.client.wrapped_call("currentsong") self.cover.set_from_pixbuf(Cover(self.settings, current_song).get_pixbuf(self.settings.get_int("track-cover"))) def clear(self, *args): self.cover.set_from_pixbuf(Cover(self.settings, {}).get_pixbuf(self.settings.get_int("track-cover"))) self.song_file=None def on_button_press_event(self, widget, event): if self.client.connected(): song=ClientHelper.song_to_first_str_dict(self.client.wrapped_call("currentsong")) if not song == {}: try: artist=song[self.settings.get_artist_type()] except: try: artist=song["artist"] except: artist="" try: album=song["album"] except: album="" try: album_year=song["date"] except: album_year="" if event.button == 1 and event.type == Gdk.EventType.BUTTON_PRESS: self.client.wrapped_call("album_to_playlist", album, artist, album_year, False) elif event.button == 2 and event.type == Gdk.EventType.BUTTON_PRESS: self.client.wrapped_call("album_to_playlist", album, artist, album_year, True) elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS: album_dialog=AlbumDialog(self.window, self.client, self.settings, album, artist, album_year) album_dialog.open() album_dialog.destroy() def on_settings_changed(self, *args): size=self.settings.get_int("track-cover") self.cover.set_size_request(size, size) self.song_file=None self.refresh() class PlaylistView(Gtk.Box): def __init__(self, client, settings): Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) # adding vars self.client=client self.settings=settings self.playlist_version=None # buttons self.back_to_song_button=Gtk.Button(image=Gtk.Image.new_from_icon_name("go-previous-symbolic", Gtk.IconSize.BUTTON)) self.back_to_song_button.set_tooltip_text(_("Scroll to current song")) self.back_to_song_button.set_relief(Gtk.ReliefStyle.NONE) # Store # (track, disc, title, artist, album, duration, date, genre, file, weight) self.store=Gtk.ListStore(str, str, str, str, str, str, str, str, str, Pango.Weight) # TreeView self.treeview=Gtk.TreeView(model=self.store) self.treeview.set_search_column(2) self.treeview.set_property("activate-on-single-click", True) # selection self.selection=self.treeview.get_selection() self.selection.set_mode(Gtk.SelectionMode.SINGLE) # Column renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True) renderer_text_ralign=Gtk.CellRendererText(xalign=1.0) self.columns=[None, None, None, None, None, None, None, None] self.columns[0]=Gtk.TreeViewColumn(_("No"), renderer_text_ralign, text=0, weight=9) self.columns[0].set_property("resizable", True) self.columns[1]=Gtk.TreeViewColumn(_("Disc"), renderer_text_ralign, text=1, weight=9) self.columns[1].set_property("resizable", True) self.columns[2]=Gtk.TreeViewColumn(_("Title"), renderer_text, text=2, weight=9) self.columns[2].set_property("resizable", True) self.columns[3]=Gtk.TreeViewColumn(_("Artist"), renderer_text, text=3, weight=9) self.columns[3].set_property("resizable", True) self.columns[4]=Gtk.TreeViewColumn(_("Album"), renderer_text, text=4, weight=9) self.columns[4].set_property("resizable", True) self.columns[5]=Gtk.TreeViewColumn(_("Length"), renderer_text, text=5, weight=9) self.columns[5].set_property("resizable", True) self.columns[6]=Gtk.TreeViewColumn(_("Year"), renderer_text, text=6, weight=9) self.columns[6].set_property("resizable", True) self.columns[7]=Gtk.TreeViewColumn(_("Genre"), renderer_text, text=7, weight=9) self.columns[7].set_property("resizable", True) self.load_settings() # scroll scroll=Gtk.ScrolledWindow() scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scroll.add(self.treeview) # frame frame=FocusFrame() frame.set_widget(self.treeview) frame.add(scroll) # audio infos audio=AudioType(self.client) audio.set_xalign(1) audio.set_ellipsize(Pango.EllipsizeMode.END) # playlist info self.playlist_info=Gtk.Label() self.playlist_info.set_xalign(0) self.playlist_info.set_ellipsize(Pango.EllipsizeMode.END) # status bar status_bar=Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) status_bar.set_property("border-width", 1) status_bar.pack_start(self.back_to_song_button, False, False, 0) self.playlist_info.set_margin_start(3) status_bar.pack_start(self.playlist_info, True, True, 0) audio.set_margin_end(5) audio.set_margin_start(12) status_bar.pack_end(audio, False, False, 0) # connect self.treeview.connect("row-activated", self.on_row_activated) self.key_press_event=self.treeview.connect("key-press-event", self.on_key_press_event) self.treeview.connect("button-press-event", self.on_button_press_event) self.back_to_song_button.connect("clicked", self.scroll_to_selected_title) self.client.emitter.connect("playlist_changed", self.on_playlist_changed) self.client.emitter.connect("current_song_changed", self.on_song_changed) self.client.emitter.connect("disconnected", self.on_disconnected) self.settings.connect("changed::column-visibilities", self.load_settings) self.settings.connect("changed::column-permutation", self.load_settings) # packing self.pack_start(frame, True, True, 0) self.pack_start(Gtk.Separator.new(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0) self.pack_end(status_bar, False, False, 0) def save_settings(self): # only saves the column sizes columns=self.treeview.get_columns() permutation=self.settings.get_value("column-permutation").unpack() sizes=[0] * len(permutation) for i in range(len(permutation)): sizes[permutation[i]]=columns[i].get_width() self.settings.set_value("column-sizes", GLib.Variant("ai", sizes)) def load_settings(self, *args): columns=self.treeview.get_columns() for column in columns: self.treeview.remove_column(column) sizes=self.settings.get_value("column-sizes").unpack() visibilities=self.settings.get_value("column-visibilities").unpack() for i in self.settings.get_value("column-permutation"): if sizes[i] > 0: self.columns[i].set_fixed_width(sizes[i]) self.columns[i].set_visible(visibilities[i]) self.treeview.append_column(self.columns[i]) def scroll_to_selected_title(self, *args): treeview, treeiter=self.selection.get_selected() if not treeiter == None: path=treeview.get_path(treeiter) self.treeview.scroll_to_cell(path, None, True, 0.25) def refresh_playlist_info(self): songs=self.client.wrapped_call("playlistinfo") if not songs == []: whole_length_human_readable=ClientHelper.calc_display_length(songs) self.playlist_info.set_text(_("%(total_tracks)i titles (%(total_length)s)") % {"total_tracks": len(songs), "total_length": whole_length_human_readable}) else: self.playlist_info.set_text("") def refresh_selection(self, scroll=True): # Gtk.TreePath(len(self.store) is used to generate an invalid TreePath (needed to unset cursor) self.treeview.set_cursor(Gtk.TreePath(len(self.store)), None, False) for row in self.store: # reset bold text row[9]=Pango.Weight.BOOK try: song=self.client.wrapped_call("status")["song"] path=Gtk.TreePath(int(song)) self.selection.select_path(path) self.store[path][9]=Pango.Weight.BOLD if scroll: self.scroll_to_selected_title() except: self.selection.unselect_all() def clear(self, *args): self.playlist_info.set_text("") self.store.clear() self.playlist_version=None def remove_song(self, path): self.client.wrapped_call("delete", path) # bad song index possible self.store.remove(self.store.get_iter(path)) self.playlist_version=self.client.wrapped_call("status")["playlist"] def on_key_press_event(self, widget, event): self.treeview.handler_block(self.key_press_event) if event.keyval == 65535: # entf treeview, treeiter=self.selection.get_selected() if not treeiter == None: path=self.store.get_path(treeiter) try: self.remove_song(path) except: pass elif event.keyval == 65383: # menu key treeview, treeiter=self.selection.get_selected() if not treeiter == None: path=self.store.get_path(treeiter) cell=self.treeview.get_cell_area(path, None) file_name=self.store[path][8] pop=SongPopover(self.client.wrapped_call("get_metadata", file_name), widget, int(cell.x), int(cell.y)) pop.popup() pop.show_all() self.treeview.handler_unblock(self.key_press_event) def on_button_press_event(self, widget, event): if event.button == 2 and event.type == Gdk.EventType.BUTTON_PRESS: try: path=widget.get_path_at_pos(int(event.x), int(event.y))[0] self.remove_song(path) except: pass elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS: try: path=widget.get_path_at_pos(int(event.x), int(event.y))[0] pop=SongPopover(self.client.wrapped_call("get_metadata", self.store[path][8]), widget, int(event.x), int(event.y)) pop.popup() except: pass def on_row_activated(self, widget, path, view_column): self.client.wrapped_call("play", path) def on_playlist_changed(self, emitter, version): songs=[] if not self.playlist_version == None: songs=self.client.wrapped_call("plchanges", self.playlist_version) else: songs=self.client.wrapped_call("playlistinfo") if not songs == []: self.playlist_info.set_text("") for s in songs: song=ClientHelper.extend_song_for_display(ClientHelper.song_to_str_dict(s)) try: treeiter=self.store.get_iter(song["pos"]) self.store.set(treeiter, 0, song["track"], 1, song["disc"], 2, song["title"], 3, song["artist"], 4, song["album"], 5, song["human_duration"], 6, song["date"], 7, song["genre"], 8, song["file"], 9, Pango.Weight.BOOK) except: self.store.append([song["track"], song["disc"], song["title"], song["artist"], song["album"], song["human_duration"], song["date"], song["genre"], song["file"], Pango.Weight.BOOK]) for i in reversed(range(int(self.client.wrapped_call("status")["playlistlength"]), len(self.store))): treeiter=self.store.get_iter(i) self.store.remove(treeiter) self.refresh_playlist_info() if self.playlist_version == None or not songs == []: self.refresh_selection() self.playlist_version=version def on_song_changed(self, *args): if self.client.wrapped_call("status")["state"] == "play": self.refresh_selection() else: self.refresh_selection(False) def on_disconnected(self, *args): self.clear() class CoverLyricsOSD(Gtk.Overlay): def __init__(self, client, settings, window): Gtk.Overlay.__init__(self) # adding vars self.client=client self.settings=settings self.window=window # cover self.cover=MainCover(self.client, self.settings, self.window) self.cover.set_property("border-width", 3) # lyrics button self.lyrics_button=Gtk.Button(image=Gtk.Image.new_from_icon_name("media-view-subtitles-symbolic", Gtk.IconSize.BUTTON)) self.lyrics_button.set_tooltip_text(_("Show lyrics")) style_context=self.lyrics_button.get_style_context() style_context.add_class("circular") # revealer # workaround to get tooltips in overlay self.revealer=Gtk.Revealer() self.revealer.set_halign(2) self.revealer.set_valign(1) self.revealer.set_margin_top(6) self.revealer.set_margin_end(6) self.revealer.add(self.lyrics_button) # event box self.event_box=Gtk.EventBox() self.event_box.add(self.cover) # packing self.add(self.event_box) self.add_overlay(self.revealer) # connect self.lyrics_button.connect("clicked", self.on_lyrics_clicked) self.client.emitter.connect("disconnected", self.on_disconnected) self.client.emitter.connect("reconnected", self.on_reconnected) self.settings.connect("changed::show-lyrics-button", self.on_settings_changed) self.on_settings_changed() # hide lyrics button def show_lyrics(self, *args): if self.lyrics_button.get_sensitive(): self.lyrics_button.emit("clicked") def on_reconnected(self, *args): self.lyrics_button.set_sensitive(True) def on_disconnected(self, *args): self.lyrics_button.set_sensitive(False) self.cover.clear() try: self.lyrics_win.destroy() except: pass def on_lyrics_clicked(self, widget): self.lyrics_button.set_sensitive(False) self.lyrics_win=LyricsWindow(self.client, self.settings) def on_destroy(*args): self.lyrics_button.set_sensitive(True) self.lyrics_win.connect("destroy", on_destroy) self.add_overlay(self.lyrics_win) def on_settings_changed(self, *args): if self.settings.get_boolean("show-lyrics-button"): self.revealer.set_reveal_child(True) else: self.revealer.set_reveal_child(False) class CoverPlaylistView(Gtk.Paned): def __init__(self, client, settings, window): Gtk.Paned.__init__(self) # paned0 # adding vars self.client=client self.settings=settings self.window=window # widgets self.cover=CoverLyricsOSD(self.client, self.settings, self.window) self.playlist_view=PlaylistView(self.client, self.settings) # packing self.pack1(self.cover, False, False) self.pack2(self.playlist_view, True, False) self.set_position(self.settings.get_int("paned0")) def show_lyrics(self, *args): self.cover.show_lyrics() def save_settings(self): self.settings.set_int("paned0", self.get_position()) self.playlist_view.save_settings() ################### # settings dialog # ################### class GeneralSettings(Gtk.Box): def __init__(self, settings): Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=6) self.set_property("border-width", 18) # adding vars self.settings=settings self.settings_handlers=[] # int_settings int_settings={} int_settings_data=[(_("Main cover size:"), (100, 1200, 10), "track-cover"),\ (_("Album view cover size:"), (50, 600, 10), "album-cover"),\ (_("Button icon size:"), (16, 64, 2), "icon-size")] for data in int_settings_data: int_settings[data[2]]=(Gtk.Label(), IntEntry(self.settings.get_int(data[2]), data[1][0], data[1][1], data[1][2])) int_settings[data[2]][0].set_label(data[0]) int_settings[data[2]][0].set_xalign(0) int_settings[data[2]][1].connect("value-changed", self.on_int_changed, data[2]) self.settings_handlers.append(self.settings.connect("changed::"+data[2], self.on_int_settings_changed, int_settings[data[2]][1])) # combo_settings combo_settings={} combo_settings_data=[(_("Sort albums by:"), _("name"), _("year"), "sort-albums-by-year"), \ (_("Position of playlist:"), _("bottom"), _("right"), "playlist-right")] for data in combo_settings_data: combo_settings[data[3]]=(Gtk.Label(), Gtk.ComboBoxText()) combo_settings[data[3]][0].set_label(data[0]) combo_settings[data[3]][0].set_xalign(0) combo_settings[data[3]][1].set_entry_text_column(0) combo_settings[data[3]][1].append_text(data[1]) combo_settings[data[3]][1].append_text(data[2]) if self.settings.get_boolean(data[3]): combo_settings[data[3]][1].set_active(1) else: combo_settings[data[3]][1].set_active(0) combo_settings[data[3]][1].connect("changed", self.on_combo_changed, data[3]) self.settings_handlers.append(self.settings.connect("changed::"+data[3], self.on_combo_settings_changed, combo_settings[data[3]][1])) # check buttons check_buttons={} check_buttons_data=[(_("Use Client-side decoration"), "use-csd"), \ (_("Show stop button"), "show-stop"), \ (_("Show lyrics button"), "show-lyrics-button"), \ (_("Show initials in artist view"), "show-initials"), \ (_("Show tooltips in album view"), "show-album-view-tooltips"), \ (_("Use 'Album Artist' tag"), "use-album-artist"), \ (_("Send notification on title change"), "send-notify"), \ (_("Stop playback on quit"), "stop-on-quit"), \ (_("Play selected albums and titles immediately"), "force-mode")] for data in check_buttons_data: check_buttons[data[1]]=Gtk.CheckButton(label=data[0]) check_buttons[data[1]].set_active(self.settings.get_boolean(data[1])) check_buttons[data[1]].set_margin_start(12) check_buttons[data[1]].connect("toggled", self.on_toggled, data[1]) self.settings_handlers.append(self.settings.connect("changed::"+data[1], self.on_check_settings_changed, check_buttons[data[1]])) # headings view_heading=Gtk.Label() view_heading.set_markup(_("View")) view_heading.set_xalign(0) behavior_heading=Gtk.Label() behavior_heading.set_markup(_("Behavior")) behavior_heading.set_xalign(0) # view grid view_grid=Gtk.Grid() view_grid.set_row_spacing(6) view_grid.set_column_spacing(12) view_grid.set_margin_start(12) view_grid.add(int_settings["track-cover"][0]) view_grid.attach_next_to(int_settings["album-cover"][0], int_settings["track-cover"][0], Gtk.PositionType.BOTTOM, 1, 1) view_grid.attach_next_to(int_settings["icon-size"][0], int_settings["album-cover"][0], Gtk.PositionType.BOTTOM, 1, 1) view_grid.attach_next_to(combo_settings["playlist-right"][0], int_settings["icon-size"][0], Gtk.PositionType.BOTTOM, 1, 1) view_grid.attach_next_to(int_settings["track-cover"][1], int_settings["track-cover"][0], Gtk.PositionType.RIGHT, 1, 1) view_grid.attach_next_to(int_settings["album-cover"][1], int_settings["album-cover"][0], Gtk.PositionType.RIGHT, 1, 1) view_grid.attach_next_to(int_settings["icon-size"][1], int_settings["icon-size"][0], Gtk.PositionType.RIGHT, 1, 1) view_grid.attach_next_to(combo_settings["playlist-right"][1], combo_settings["playlist-right"][0], Gtk.PositionType.RIGHT, 1, 1) # behavior grid behavior_grid=Gtk.Grid() behavior_grid.set_row_spacing(6) behavior_grid.set_column_spacing(12) behavior_grid.set_margin_start(12) behavior_grid.add(combo_settings["sort-albums-by-year"][0]) behavior_grid.attach_next_to(combo_settings["sort-albums-by-year"][1], combo_settings["sort-albums-by-year"][0], Gtk.PositionType.RIGHT, 1, 1) # connect self.connect("destroy", self.remove_handlers) # packing box=Gtk.Box(spacing=12) box.pack_start(check_buttons["use-csd"], False, False, 0) box.pack_start(Gtk.Label(label=_("(restart required)"), sensitive=False), False, False, 0) self.pack_start(view_heading, True, True, 0) self.pack_start(box, True, True, 0) self.pack_start(check_buttons["show-stop"], True, True, 0) self.pack_start(check_buttons["show-lyrics-button"], True, True, 0) self.pack_start(check_buttons["show-initials"], True, True, 0) self.pack_start(check_buttons["show-album-view-tooltips"], True, True, 0) self.pack_start(view_grid, True, True, 0) self.pack_start(behavior_heading, True, True, 0) self.pack_start(check_buttons["use-album-artist"], True, True, 0) self.pack_start(check_buttons["send-notify"], True, True, 0) self.pack_start(check_buttons["stop-on-quit"], True, True, 0) self.pack_start(check_buttons["force-mode"], True, True, 0) self.pack_start(behavior_grid, True, True, 0) def remove_handlers(self, *args): for handler in self.settings_handlers: self.settings.disconnect(handler) def on_int_settings_changed(self, settings, key, entry): entry.set_value(settings.get_int(key)) def on_combo_settings_changed(self, settings, key, combo): if settings.get_boolean(key): combo.set_active(1) else: combo.set_active(0) def on_check_settings_changed(self, settings, key, button): button.set_active(settings.get_boolean(key)) def on_int_changed(self, widget, key): self.settings.set_int(key, widget.get_int()) def on_combo_changed(self, box, key): active=box.get_active() if active == 0: self.settings.set_boolean(key, False) else: self.settings.set_boolean(key, True) def on_toggled(self, widget, key): self.settings.set_boolean(key, widget.get_active()) class ProfileSettings(Gtk.Grid): def __init__(self, parent, settings): Gtk.Grid.__init__(self) self.set_row_spacing(6) self.set_column_spacing(12) self.set_property("border-width", 18) # adding vars self.settings=settings self.gui_modification=False # indicates whether the settings were changed from the settings dialog # widgets self.profiles_combo=Gtk.ComboBoxText(hexpand=True) self.profiles_combo.set_entry_text_column(0) add_button=Gtk.Button(label=None, image=Gtk.Image(stock=Gtk.STOCK_ADD)) delete_button=Gtk.Button(label=None, image=Gtk.Image(stock=Gtk.STOCK_DELETE)) add_delete_buttons=Gtk.ButtonBox() add_delete_buttons.set_property("layout-style", Gtk.ButtonBoxStyle.EXPAND) add_delete_buttons.pack_start(add_button, True, True, 0) add_delete_buttons.pack_start(delete_button, True, True, 0) self.profile_entry=Gtk.Entry(hexpand=True) self.host_entry=Gtk.Entry(hexpand=True) self.port_entry=IntEntry(0, 0, 65535, 1) address_entry=Gtk.Box(spacing=6) address_entry.pack_start(self.host_entry, True, True, 0) address_entry.pack_start(self.port_entry, False, False, 0) self.password_entry=Gtk.Entry(hexpand=True) self.password_entry.set_visibility(False) self.path_entry=Gtk.Entry(hexpand=True) self.path_select_button=Gtk.Button(image=Gtk.Image(stock=Gtk.STOCK_OPEN)) path_box=Gtk.Box(spacing=6) path_box.pack_start(self.path_entry, True, True, 0) path_box.pack_start(self.path_select_button, False, False, 0) self.regex_entry=Gtk.Entry(hexpand=True) self.regex_entry.set_property("placeholder-text", COVER_REGEX) self.regex_entry.set_tooltip_text(_("The first image in the same directory as the song file matching this regex will be displayed. %AlbumArtist% and %Album% will be replaced by the corresponding tags of the song.")) profiles_label=Gtk.Label(label=_("Profile:")) profiles_label.set_xalign(1) profile_label=Gtk.Label(label=_("Name:")) profile_label.set_xalign(1) host_label=Gtk.Label(label=_("Host:")) host_label.set_xalign(1) password_label=Gtk.Label(label=_("Password:")) password_label.set_xalign(1) path_label=Gtk.Label(label=_("Music lib:")) path_label.set_xalign(1) regex_label=Gtk.Label(label=_("Cover regex:")) regex_label.set_xalign(1) # connect add_button.connect("clicked", self.on_add_button_clicked) delete_button.connect("clicked", self.on_delete_button_clicked) self.path_select_button.connect("clicked", self.on_path_select_button_clicked, parent) self.profiles_combo_changed=self.profiles_combo.connect("changed", self.on_profiles_changed) self.entry_changed_handlers=[] self.entry_changed_handlers.append((self.profile_entry, self.profile_entry.connect("changed", self.on_profile_entry_changed))) self.entry_changed_handlers.append((self.host_entry, self.host_entry.connect("changed", self.on_host_entry_changed))) self.entry_changed_handlers.append((self.port_entry, self.port_entry.connect("value-changed", self.on_port_entry_changed))) self.entry_changed_handlers.append((self.password_entry, self.password_entry.connect("changed", self.on_password_entry_changed))) self.entry_changed_handlers.append((self.path_entry, self.path_entry.connect("changed", self.on_path_entry_changed))) self.entry_changed_handlers.append((self.regex_entry, self.regex_entry.connect("changed", self.on_regex_entry_changed))) self.settings_handlers=[] self.settings_handlers.append(self.settings.connect("changed::profiles", self.on_settings_changed)) self.settings_handlers.append(self.settings.connect("changed::hosts", self.on_settings_changed)) self.settings_handlers.append(self.settings.connect("changed::ports", self.on_settings_changed)) self.settings_handlers.append(self.settings.connect("changed::passwords", self.on_settings_changed)) self.settings_handlers.append(self.settings.connect("changed::paths", self.on_settings_changed)) self.settings_handlers.append(self.settings.connect("changed::regex", self.on_settings_changed)) self.connect("destroy", self.remove_handlers) self.profiles_combo_reload() self.profiles_combo.set_active(0) # packing self.add(profiles_label) self.attach_next_to(profile_label, profiles_label, Gtk.PositionType.BOTTOM, 1, 1) self.attach_next_to(host_label, profile_label, Gtk.PositionType.BOTTOM, 1, 1) self.attach_next_to(password_label, host_label, Gtk.PositionType.BOTTOM, 1, 1) self.attach_next_to(path_label, password_label, Gtk.PositionType.BOTTOM, 1, 1) self.attach_next_to(regex_label, path_label, Gtk.PositionType.BOTTOM, 1, 1) self.attach_next_to(self.profiles_combo, profiles_label, Gtk.PositionType.RIGHT, 2, 1) self.attach_next_to(add_delete_buttons, self.profiles_combo, Gtk.PositionType.RIGHT, 1, 1) self.attach_next_to(self.profile_entry, profile_label, Gtk.PositionType.RIGHT, 2, 1) self.attach_next_to(address_entry, host_label, Gtk.PositionType.RIGHT, 2, 1) self.attach_next_to(self.password_entry, password_label, Gtk.PositionType.RIGHT, 2, 1) self.attach_next_to(path_box, path_label, Gtk.PositionType.RIGHT, 2, 1) self.attach_next_to(self.regex_entry, regex_label, Gtk.PositionType.RIGHT, 2, 1) def remove_handlers(self, *args): for handler in self.settings_handlers: self.settings.disconnect(handler) def on_settings_changed(self, *args): if self.gui_modification: self.gui_modification=False else: self.profiles_combo_reload() self.profiles_combo.set_active(0) def block_entry_changed_handlers(self, *args): for obj, handler in self.entry_changed_handlers: obj.handler_block(handler) def unblock_entry_changed_handlers(self, *args): for obj, handler in self.entry_changed_handlers: obj.handler_unblock(handler) def profiles_combo_reload(self, *args): self.block_entry_changed_handlers() self.profiles_combo.remove_all() for profile in self.settings.get_value("profiles"): self.profiles_combo.append_text(profile) self.unblock_entry_changed_handlers() def on_add_button_clicked(self, *args): pos=self.profiles_combo.get_active() self.settings.array_append('as', "profiles", "new profile") self.settings.array_append('as', "hosts", "localhost") self.settings.array_append('ai', "ports", 6600) self.settings.array_append('as', "passwords", "") self.settings.array_append('as', "paths", "") self.settings.array_append('as', "regex", "") self.profiles_combo_reload() self.profiles_combo.set_active(pos) def on_delete_button_clicked(self, *args): pos=self.profiles_combo.get_active() self.settings.array_delete('as', "profiles", pos) self.settings.array_delete('as', "hosts", pos) self.settings.array_delete('ai', "ports", pos) self.settings.array_delete('as', "passwords", pos) self.settings.array_delete('as', "paths", pos) self.settings.array_delete('as', "regex", pos) if len(self.settings.get_value("profiles")) == 0: self.on_add_button_clicked() else: self.profiles_combo_reload() self.profiles_combo.set_active(0) def on_profile_entry_changed(self, *args): self.gui_modification=True pos=self.profiles_combo.get_active() self.settings.array_modify('as', "profiles", pos, self.profile_entry.get_text()) self.profiles_combo_reload() self.profiles_combo.set_active(pos) def on_host_entry_changed(self, *args): self.gui_modification=True self.settings.array_modify('as', "hosts", self.profiles_combo.get_active(), self.host_entry.get_text()) def on_port_entry_changed(self, *args): self.gui_modification=True self.settings.array_modify('ai', "ports", self.profiles_combo.get_active(), self.port_entry.get_int()) def on_password_entry_changed(self, *args): self.gui_modification=True self.settings.array_modify('as', "passwords", self.profiles_combo.get_active(), self.password_entry.get_text()) def on_path_entry_changed(self, *args): self.gui_modification=True self.settings.array_modify('as', "paths", self.profiles_combo.get_active(), self.path_entry.get_text()) def on_regex_entry_changed(self, *args): self.gui_modification=True self.settings.array_modify('as', "regex", self.profiles_combo.get_active(), self.regex_entry.get_text()) def on_path_select_button_clicked(self, widget, parent): dialog=Gtk.FileChooserDialog(title=_("Choose directory"), transient_for=parent, action=Gtk.FileChooserAction.SELECT_FOLDER) dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) dialog.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK) dialog.set_default_size(800, 400) dialog.set_current_folder(self.settings.get_value("paths")[self.profiles_combo.get_active()]) response=dialog.run() if response == Gtk.ResponseType.OK: self.gui_modification=True self.settings.array_modify('as', "paths", self.profiles_combo.get_active(), dialog.get_filename()) self.path_entry.set_text(dialog.get_filename()) dialog.destroy() def on_profiles_changed(self, *args): active=self.profiles_combo.get_active() self.block_entry_changed_handlers() self.profile_entry.set_text(self.settings.get_value("profiles")[active]) self.host_entry.set_text(self.settings.get_value("hosts")[active]) self.port_entry.set_int(self.settings.get_value("ports")[active]) self.password_entry.set_text(self.settings.get_value("passwords")[active]) self.path_entry.set_text(self.settings.get_value("paths")[active]) self.regex_entry.set_text(self.settings.get_value("regex")[active]) self.unblock_entry_changed_handlers() class PlaylistSettings(Gtk.Box): def __init__(self, settings): Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=6) self.set_property("border-width", 18) # adding vars self.settings=settings # label label=Gtk.Label(label=_("Choose the order of information to appear in the playlist:")) label.set_line_wrap(True) label.set_xalign(0) # Store # (toggle, header, actual_index) self.store=Gtk.ListStore(bool, str, int) # TreeView self.treeview=Gtk.TreeView(model=self.store) self.treeview.set_search_column(-1) self.treeview.set_reorderable(True) self.treeview.set_headers_visible(False) # selection self.selection=self.treeview.get_selection() # Column renderer_text=Gtk.CellRendererText() renderer_toggle=Gtk.CellRendererToggle() column_toggle=Gtk.TreeViewColumn("", renderer_toggle, active=0) self.treeview.append_column(column_toggle) column_text=Gtk.TreeViewColumn("", renderer_text, text=1) self.treeview.append_column(column_text) # fill store self.headers=[_("No"), _("Disc"), _("Title"), _("Artist"), _("Album"), _("Length"), _("Year"), _("Genre")] self.fill() # scroll scroll=Gtk.ScrolledWindow() scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scroll.add(self.treeview) frame=Gtk.Frame() frame.add(scroll) # Toolbar toolbar=Gtk.Toolbar() style_context=toolbar.get_style_context() style_context.add_class("inline-toolbar") self.up_button=Gtk.ToolButton.new(Gtk.Image.new_from_icon_name("go-up-symbolic", Gtk.IconSize.SMALL_TOOLBAR)) self.up_button.set_sensitive(False) self.down_button=Gtk.ToolButton.new(Gtk.Image.new_from_icon_name("go-down-symbolic", Gtk.IconSize.SMALL_TOOLBAR)) self.down_button.set_sensitive(False) toolbar.insert(self.up_button, 0) toolbar.insert(self.down_button, 1) # column chooser column_chooser=Gtk.Box(orientation=Gtk.Orientation.VERTICAL) column_chooser.pack_start(frame, True, True, 0) column_chooser.pack_start(toolbar, False, False, 0) # connect self.row_deleted=self.store.connect("row-deleted", self.save_permutation) renderer_toggle.connect("toggled", self.on_cell_toggled) self.up_button.connect("clicked", self.on_up_button_clicked) self.down_button.connect("clicked", self.on_down_button_clicked) self.selection.connect("changed", self.set_button_sensitivity) self.settings_handlers=[] self.settings_handlers.append(self.settings.connect("changed::column-visibilities", self.on_visibilities_changed)) self.settings_handlers.append(self.settings.connect("changed::column-permutation", self.on_permutation_changed)) self.connect("destroy", self.remove_handlers) # packing self.pack_start(label, False, False, 0) self.pack_start(column_chooser, True, True, 0) def remove_handlers(self, *args): for handler in self.settings_handlers: self.settings.disconnect(handler) def fill(self, *args): visibilities=self.settings.get_value("column-visibilities").unpack() for actual_index in self.settings.get_value("column-permutation"): self.store.append([visibilities[actual_index], self.headers[actual_index], actual_index]) def save_permutation(self, *args): permutation=[] for row in self.store: permutation.append(row[2]) self.settings.set_value("column-permutation", GLib.Variant("ai", permutation)) def set_button_sensitivity(self, *args): treeiter=self.selection.get_selected()[1] if treeiter == None: self.up_button.set_sensitive(False) self.down_button.set_sensitive(False) else: path=self.store.get_path(treeiter) if self.store.iter_next(treeiter) == None: self.up_button.set_sensitive(True) self.down_button.set_sensitive(False) elif not path.prev(): self.up_button.set_sensitive(False) self.down_button.set_sensitive(True) else: self.up_button.set_sensitive(True) self.down_button.set_sensitive(True) def on_cell_toggled(self, widget, path): self.store[path][0]=not self.store[path][0] self.settings.array_modify('ab', "column-visibilities", self.store[path][2], self.store[path][0]) def on_up_button_clicked(self, *args): treeiter=self.selection.get_selected()[1] path=self.store.get_path(treeiter) path.prev() prev=self.store.get_iter(path) self.store.move_before(treeiter, prev) self.set_button_sensitivity() self.save_permutation() def on_down_button_clicked(self, *args): treeiter=self.selection.get_selected()[1] path=self.store.get_path(treeiter) next=self.store.iter_next(treeiter) self.store.move_after(treeiter, next) self.set_button_sensitivity() self.save_permutation() def on_visibilities_changed(self, *args): visibilities=self.settings.get_value("column-visibilities").unpack() for i, actual_index in enumerate(self.settings.get_value("column-permutation")): self.store[i][0]=visibilities[actual_index] def on_permutation_changed(self, *args): equal=True perm=self.settings.get_value("column-permutation") for i, e in enumerate(self.store): if e[2] != perm[i]: equal=False break if not equal: self.store.handler_block(self.row_deleted) self.store.clear() self.fill() self.store.handler_unblock(self.row_deleted) class SettingsDialog(Gtk.Dialog): def __init__(self, parent, settings): Gtk.Dialog.__init__(self, title=_("Settings"), transient_for=parent) self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK) self.set_default_size(500, 400) # adding vars self.settings=settings # widgets general=GeneralSettings(self.settings) profiles=ProfileSettings(parent, self.settings) playlist=PlaylistSettings(self.settings) # packing tabs=Gtk.Notebook() tabs.append_page(general, Gtk.Label(label=_("General"))) tabs.append_page(profiles, Gtk.Label(label=_("Profiles"))) tabs.append_page(playlist, Gtk.Label(label=_("Playlist"))) self.vbox.pack_start(tabs, True, True, 0) # vbox default widget of dialogs self.vbox.set_spacing(3) self.show_all() ################### # control widgets # ################### class ClientControl(Gtk.ButtonBox): def __init__(self, client, settings): Gtk.ButtonBox.__init__(self, spacing=6) self.set_property("layout-style", Gtk.ButtonBoxStyle.EXPAND) # adding vars self.client=client self.settings=settings self.icon_size=self.settings.get_int("icon-size") # widgets self.icons={} icons_data=["media-playback-start-symbolic", "media-playback-stop-symbolic", "media-playback-pause-symbolic", \ "media-skip-backward-symbolic", "media-skip-forward-symbolic"] for data in icons_data: self.icons[data]=PixelSizedIcon(data, self.icon_size) self.play_button=Gtk.Button(image=self.icons["media-playback-start-symbolic"]) self.stop_button=Gtk.Button(image=self.icons["media-playback-stop-symbolic"]) self.prev_button=Gtk.Button(image=self.icons["media-skip-backward-symbolic"]) self.next_button=Gtk.Button(image=self.icons["media-skip-forward-symbolic"]) # connect self.play_button.connect("clicked", self.on_play_clicked) self.stop_button.connect("clicked", self.on_stop_clicked) self.prev_button.connect("clicked", self.on_prev_clicked) self.next_button.connect("clicked", self.on_next_clicked) self.settings.connect("changed::show-stop", self.on_settings_changed) self.settings.connect("changed::icon-size", self.on_icon_size_changed) self.client.emitter.connect("state", self.on_state) # packing self.pack_start(self.prev_button, True, True, 0) self.pack_start(self.play_button, True, True, 0) if self.settings.get_boolean("show-stop"): self.pack_start(self.stop_button, True, True, 0) self.pack_start(self.next_button, True, True, 0) def on_state(self, emitter, state): if state == "play": self.play_button.set_image(self.icons["media-playback-pause-symbolic"]) self.prev_button.set_sensitive(True) self.next_button.set_sensitive(True) elif state == "pause": self.play_button.set_image(self.icons["media-playback-start-symbolic"]) self.prev_button.set_sensitive(True) self.next_button.set_sensitive(True) else: self.play_button.set_image(self.icons["media-playback-start-symbolic"]) self.prev_button.set_sensitive(False) self.next_button.set_sensitive(False) def on_play_clicked(self, widget): if self.client.connected(): status=self.client.wrapped_call("status") if status["state"] == "play": self.client.wrapped_call("pause", 1) elif status["state"] == "pause": self.client.wrapped_call("pause", 0) else: try: self.client.wrapped_call("play", status["song"]) except: try: self.client.wrapped_call("play", ) except: pass def on_stop_clicked(self, widget): if self.client.connected(): self.client.wrapped_call("stop") def on_prev_clicked(self, widget): if self.client.connected(): self.client.wrapped_call("previous") def on_next_clicked(self, widget): if self.client.connected(): self.client.wrapped_call("next") def on_settings_changed(self, *args): if self.settings.get_boolean("show-stop"): self.pack_start(self.stop_button, True, True, 0) self.reorder_child(self.stop_button, 2) self.stop_button.show() else: self.remove(self.stop_button) def on_icon_size_changed(self, *args): pixel_size=self.settings.get_int("icon-size") for icon in self.icons.values(): icon.set_pixel_size(pixel_size) class SeekBar(Gtk.Box): def __init__(self, client): Gtk.Box.__init__(self) self.set_hexpand(True) # adding vars self.client=client self.seek_time="10" # seek increment in seconds self.update=True self.jumped=False # labels self.elapsed=Gtk.Label() self.elapsed.set_width_chars(5) self.rest=Gtk.Label() self.rest.set_width_chars(6) # progress bar self.scale=Gtk.Scale.new_with_range(orientation=Gtk.Orientation.HORIZONTAL, min=0, max=100, step=0.001) self.scale.set_show_fill_level(True) self.scale.set_restrict_to_fill_level(False) self.scale.set_draw_value(False) # css (scale) style_context=self.scale.get_style_context() provider=Gtk.CssProvider() css=b"""scale fill { background-color: @theme_selected_bg_color; }""" provider.load_from_data(css) style_context.add_provider(provider, 800) # event boxes self.elapsed_event_box=Gtk.EventBox() self.rest_event_box=Gtk.EventBox() # connect self.elapsed_event_box.connect("button-press-event", self.on_elapsed_button_press_event) self.rest_event_box.connect("button-press-event", self.on_rest_button_press_event) self.scale.connect("change-value", self.on_change_value) self.scale.connect("scroll-event", self.dummy) # disable mouse wheel self.scale.connect("button-press-event", self.on_scale_button_press_event) self.scale.connect("button-release-event", self.on_scale_button_release_event) self.client.emitter.connect("disconnected", self.disable) self.client.emitter.connect("state", self.on_state) self.client.emitter.connect("elapsed_changed", self.refresh) # packing self.elapsed_event_box.add(self.elapsed) self.rest_event_box.add(self.rest) self.pack_start(self.elapsed_event_box, False, False, 0) self.pack_start(self.scale, True, True, 0) self.pack_end(self.rest_event_box, False, False, 0) def dummy(self, *args): return True def on_scale_button_press_event(self, widget, event): if event.button == 1 and event.type == Gdk.EventType.BUTTON_PRESS: self.update=False self.scale.set_has_origin(False) if event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS: self.jumped=False def on_scale_button_release_event(self, widget, event): if event.button == 1: self.update=True self.scale.set_has_origin(True) status=self.client.wrapped_call("status") if self.jumped: # actual seek duration=float(status["duration"]) factor=(self.scale.get_value()/100) pos=(duration*factor) self.client.wrapped_call("seekcur", pos) self.jumped=False else: self.refresh(None, float(status["elapsed"]), float(status["duration"])) def on_change_value(self, range, scroll, value): # value is inaccurate if scroll == Gtk.ScrollType.STEP_BACKWARD: self.seek_backward() elif scroll == Gtk.ScrollType.STEP_FORWARD: self.seek_forward() elif scroll == Gtk.ScrollType.JUMP: status=self.client.wrapped_call("status") duration=float(status["duration"]) factor=(value/100) if factor > 1: # fix display error factor=1 elapsed=(factor*duration) self.elapsed.set_text(str(datetime.timedelta(seconds=int(elapsed))).lstrip("0").lstrip(":")) self.rest.set_text("-"+str(datetime.timedelta(seconds=int(duration-elapsed))).lstrip("0").lstrip(":")) self.jumped=True def seek_forward(self): self.client.wrapped_call("seekcur", "+"+self.seek_time) def seek_backward(self): self.client.wrapped_call("seekcur", "-"+self.seek_time) def enable(self, *args): self.scale.set_sensitive(True) self.scale.set_range(0, 100) self.elapsed_event_box.set_sensitive(True) self.rest_event_box.set_sensitive(True) def disable(self, *args): self.scale.set_sensitive(False) self.scale.set_range(0, 0) self.elapsed_event_box.set_sensitive(False) self.rest_event_box.set_sensitive(False) self.elapsed.set_text("00:00") self.rest.set_text("-00:00") def on_elapsed_button_press_event(self, widget, event): if event.button == 1 and event.type == Gdk.EventType.BUTTON_PRESS: self.seek_backward() elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS: self.seek_forward() def on_rest_button_press_event(self, widget, event): if event.button == 1 and event.type == Gdk.EventType.BUTTON_PRESS: self.seek_forward() elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS: self.seek_backward() def on_state(self, emitter, state): if state == "stop": self.disable() else: self.enable() def refresh(self, emitter, elapsed, duration): if elapsed > duration: # fix display error elapsed=duration fraction=(elapsed/duration)*100 if self.update: self.scale.set_value(fraction) self.elapsed.set_text(str(datetime.timedelta(seconds=int(elapsed))).lstrip("0").lstrip(":")) self.rest.set_text("-"+str(datetime.timedelta(seconds=int(duration-elapsed))).lstrip("0").lstrip(":")) self.scale.set_fill_level(fraction) class PlaybackOptions(Gtk.Box): def __init__(self, client, settings): Gtk.Box.__init__(self, spacing=6) # adding vars self.client=client self.settings=settings self.icon_size=self.settings.get_int("icon-size") # widgets self.icons={} icons_data=["media-playlist-shuffle-symbolic", "media-playlist-repeat-symbolic", "zoom-original-symbolic", "edit-cut-symbolic"] for data in icons_data: self.icons[data]=PixelSizedIcon(data, self.icon_size) self.random=Gtk.ToggleButton(image=self.icons["media-playlist-shuffle-symbolic"]) self.random.set_tooltip_text(_("Random mode")) self.repeat=Gtk.ToggleButton(image=self.icons["media-playlist-repeat-symbolic"]) self.repeat.set_tooltip_text(_("Repeat mode")) self.single=Gtk.ToggleButton(image=self.icons["zoom-original-symbolic"]) self.single.set_tooltip_text(_("Single mode")) self.consume=Gtk.ToggleButton(image=self.icons["edit-cut-symbolic"]) self.consume.set_tooltip_text(_("Consume mode")) self.volume_button=Gtk.VolumeButton() self.volume_button.set_property("use-symbolic", True) self.volume_button.set_property("size", self.settings.get_gtk_icon_size("icon-size")) # connect self.random_toggled=self.random.connect("toggled", self.set_option, "random") self.repeat_toggled=self.repeat.connect("toggled", self.set_option, "repeat") self.single_toggled=self.single.connect("toggled", self.set_option, "single") self.consume_toggled=self.consume.connect("toggled", self.set_option, "consume") self.volume_button_changed=self.volume_button.connect("value-changed", self.set_volume) self.repeat_changed=self.client.emitter.connect("repeat", self.repeat_refresh) self.random_changed=self.client.emitter.connect("random", self.random_refresh) self.single_changed=self.client.emitter.connect("single", self.single_refresh) self.consume_changed=self.client.emitter.connect("consume", self.consume_refresh) self.volume_changed=self.client.emitter.connect("volume_changed", self.volume_refresh) self.settings.connect("changed::icon-size", self.on_icon_size_changed) # packing ButtonBox=Gtk.ButtonBox() ButtonBox.set_property("layout-style", Gtk.ButtonBoxStyle.EXPAND) ButtonBox.pack_start(self.repeat, True, True, 0) ButtonBox.pack_start(self.random, True, True, 0) ButtonBox.pack_start(self.single, True, True, 0) ButtonBox.pack_start(self.consume, True, True, 0) self.pack_start(ButtonBox, True, True, 0) self.pack_start(self.volume_button, True, True, 0) def set_option(self, widget, option): if widget.get_active(): self.client.wrapped_call(option, "1") else: self.client.wrapped_call(option, "0") def set_volume(self, widget, value): self.client.wrapped_call("setvol", str(int(value*100))) def repeat_refresh(self, emitter, val): self.repeat.handler_block(self.repeat_toggled) self.repeat.set_active(val) self.repeat.handler_unblock(self.repeat_toggled) def random_refresh(self, emitter, val): self.random.handler_block(self.random_toggled) self.random.set_active(val) self.random.handler_unblock(self.random_toggled) def single_refresh(self, emitter, val): self.single.handler_block(self.single_toggled) self.single.set_active(val) self.single.handler_unblock(self.single_toggled) def consume_refresh(self, emitter, val): self.consume.handler_block(self.consume_toggled) self.consume.set_active(val) self.consume.handler_unblock(self.consume_toggled) def volume_refresh(self, emitter, volume): self.volume_button.handler_block(self.volume_button_changed) try: self.volume_button.set_value(volume/100) except: self.volume_button.set_value(0) self.volume_button.handler_unblock(self.volume_button_changed) def on_icon_size_changed(self, *args): pixel_size=self.settings.get_int("icon-size") for icon in self.icons.values(): icon.set_pixel_size(pixel_size) self.volume_button.set_property("size", self.settings.get_gtk_icon_size("icon-size")) ################# # other dialogs # ################# class ServerStats(Gtk.Dialog): def __init__(self, parent, client): Gtk.Dialog.__init__(self, title=_("Stats"), transient_for=parent) self.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK) # adding vars self.client=client # Store # (tag, value) self.store=Gtk.ListStore(str, str) # TreeView self.treeview=Gtk.TreeView(model=self.store) self.treeview.set_can_focus(False) self.treeview.set_search_column(-1) self.treeview.set_headers_visible(False) # selection sel=self.treeview.get_selection() sel.set_mode(Gtk.SelectionMode.NONE) # Column renderer_text=Gtk.CellRendererText() renderer_text_ralign=Gtk.CellRendererText(xalign=1.0) self.column_tag=Gtk.TreeViewColumn("", renderer_text_ralign, text=0) self.treeview.append_column(self.column_tag) self.column_value=Gtk.TreeViewColumn("", renderer_text, text=1) self.treeview.append_column(self.column_value) self.store.append(["protocol:", str(self.client.mpd_version)]) stats=self.client.wrapped_call("stats") for key in stats: print_key=key+":" if key == "uptime" or key == "playtime" or key == "db_playtime": self.store.append([print_key, str(datetime.timedelta(seconds=int(stats[key])))]) elif key == "db_update": self.store.append([print_key, str(datetime.datetime.fromtimestamp(int(stats[key])))]) else: self.store.append([print_key, stats[key]]) frame=Gtk.Frame() frame.add(self.treeview) self.vbox.pack_start(frame, True, True, 0) self.vbox.set_spacing(3) self.show_all() self.run() class AboutDialog(Gtk.AboutDialog): def __init__(self, window): Gtk.AboutDialog.__init__(self, transient_for=window, modal=True) self.set_program_name(NAME) self.set_version(VERSION) self.set_comments(_("A small MPD client written in python")) self.set_authors(["Martin Wagner"]) self.set_website("https://github.com/SoongNoonien/mpdevil") self.set_copyright("\xa9 2020 Martin Wagner") self.set_logo_icon_name(PACKAGE) ############### # main window # ############### class ProfileSelect(Gtk.ComboBoxText): def __init__(self, client, settings): Gtk.ComboBoxText.__init__(self) # adding vars self.client=client self.settings=settings # connect self.changed=self.connect("changed", self.on_changed) self.settings.connect("changed::profiles", self.refresh) self.settings.connect("changed::hosts", self.refresh) self.settings.connect("changed::ports", self.refresh) self.settings.connect("changed::passwords", self.refresh) self.settings.connect("changed::paths", self.refresh) self.refresh() def refresh(self, *args): self.handler_block(self.changed) self.remove_all() for profile in self.settings.get_value("profiles"): self.append_text(profile) self.set_active(self.settings.get_int("active-profile")) self.handler_unblock(self.changed) def on_changed(self, *args): active=self.get_active() self.settings.set_int("active-profile", active) class MainWindow(Gtk.ApplicationWindow): def __init__(self, app, client, settings): Gtk.ApplicationWindow.__init__(self, title=("mpdevil"), application=app) Notify.init("mpdevil") self.set_icon_name("mpdevil") self.settings=settings self.set_default_size(self.settings.get_int("width"), self.settings.get_int("height")) # adding vars self.app=app self.client=client self.use_csd=self.settings.get_boolean("use-csd") if self.use_csd: self.icon_size=0 else: self.icon_size=self.settings.get_int("icon-size") # MPRIS DBusGMainLoop(set_as_default=True) self.dbus_service=MPRISInterface(self, self.client, self.settings) # actions save_action=Gio.SimpleAction.new("save", None) save_action.connect("activate", self.on_save) self.add_action(save_action) settings_action=Gio.SimpleAction.new("settings", None) settings_action.connect("activate", self.on_settings) self.add_action(settings_action) stats_action=Gio.SimpleAction.new("stats", None) stats_action.connect("activate", self.on_stats) self.add_action(stats_action) self.update_action=Gio.SimpleAction.new("update", None) self.update_action.connect("activate", self.on_update) self.add_action(self.update_action) self.help_action=Gio.SimpleAction.new("help", None) self.help_action.connect("activate", self.on_help) self.add_action(self.help_action) # widgets self.icons={} icons_data=["open-menu-symbolic"] for data in icons_data: self.icons[data]=PixelSizedIcon(data, self.icon_size) self.browser=Browser(self.client, self.settings, self) self.cover_playlist_view=CoverPlaylistView(self.client, self.settings, self) self.profiles=ProfileSelect(self.client, self.settings) self.profiles.set_tooltip_text(_("Select profile")) self.control=ClientControl(self.client, self.settings) self.progress=SeekBar(self.client) self.play_opts=PlaybackOptions(self.client, self.settings) # menu subsection=Gio.Menu() subsection.append(_("Settings"), "win.settings") subsection.append(_("Help"), "win.help") subsection.append(_("About"), "app.about") subsection.append(_("Quit"), "app.quit") menu=Gio.Menu() menu.append(_("Save window layout"), "win.save") menu.append(_("Update database"), "win.update") menu.append(_("Server stats"), "win.stats") menu.append_section(None, subsection) menu_button=Gtk.MenuButton.new() menu_popover=Gtk.Popover.new_from_model(menu_button, menu) menu_button.set_popover(menu_popover) menu_button.set_tooltip_text(_("Menu")) menu_button.set_image(image=self.icons["open-menu-symbolic"]) # connect self.settings.connect("changed::profiles", self.on_settings_changed) self.settings.connect("changed::playlist-right", self.on_playlist_pos_settings_changed) if not self.use_csd: self.settings.connect("changed::icon-size", self.on_icon_size_changed) self.client.emitter.connect("current_song_changed", self.on_song_changed) self.client.emitter.connect("disconnected", self.on_disconnected) self.client.emitter.connect("reconnected", self.on_reconnected) # unmap space binding_set=Gtk.binding_set_find('GtkTreeView') Gtk.binding_entry_remove(binding_set, 32, Gdk.ModifierType.MOD2_MASK) # map space play/pause self.connect("key-press-event", self.on_key_press_event) # packing self.paned2=Gtk.Paned() self.paned2.set_position(self.settings.get_int("paned2")) self.on_playlist_pos_settings_changed() # set orientation self.paned2.pack1(self.browser, True, False) self.paned2.pack2(self.cover_playlist_view, False, False) self.vbox=Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.action_bar=Gtk.ActionBar() self.vbox.pack_start(self.paned2, True, True, 0) self.vbox.pack_start(self.action_bar, False, False, 0) self.action_bar.pack_start(self.control) self.action_bar.pack_start(self.progress) self.action_bar.pack_start(self.play_opts) if self.use_csd: self.header_bar=Gtk.HeaderBar() self.header_bar.set_show_close_button(True) self.header_bar.set_title("mpdevil") self.set_titlebar(self.header_bar) self.header_bar.pack_start(self.browser.back_to_album_button) self.header_bar.pack_start(self.browser.genre_select) self.header_bar.pack_end(menu_button) self.header_bar.pack_end(self.profiles) self.header_bar.pack_end(self.browser.search_button) else: self.action_bar.pack_start(Gtk.Separator.new(orientation=Gtk.Orientation.VERTICAL)) self.action_bar.pack_start(self.profiles) self.action_bar.pack_start(menu_button) self.add(self.vbox) self.show_all() if self.settings.get_boolean("maximize"): self.maximize() self.on_settings_changed() # hide profiles button self.client.start() # connect client def on_song_changed(self, *args): song=self.client.wrapped_call("currentsong") if song == {}: if self.use_csd: self.header_bar.set_title("mpdevil") self.header_bar.set_subtitle("") else: self.set_title("mpdevil") else: song=ClientHelper.extend_song_for_display(ClientHelper.song_to_str_dict(song)) if song["date"] != "": date=" ("+song["date"]+")" else: date="" if self.use_csd: self.header_bar.set_title(song["title"]+" - "+song["artist"]) self.header_bar.set_subtitle(song["album"]+date) else: self.set_title(song["title"]+" - "+song["artist"]+" - "+song["album"]+date) if self.settings.get_boolean("send-notify"): if not self.is_active() and self.client.wrapped_call("status")["state"] == "play": notify=Notify.Notification.new(song["title"], song["artist"]+"\n"+song["album"]+date) pixbuf=Cover(self.settings, song).get_pixbuf(400) notify.set_image_from_pixbuf(pixbuf) notify.show() def on_reconnected(self, *args): self.dbus_service.acquire_name() self.progress.set_sensitive(True) self.control.set_sensitive(True) self.play_opts.set_sensitive(True) self.browser.back_to_album() def on_disconnected(self, *args): self.dbus_service.release_name() if self.use_csd: self.header_bar.set_title("mpdevil") self.header_bar.set_subtitle("(not connected)") else: self.set_title("mpdevil (not connected)") self.songid_playing=None self.progress.set_sensitive(False) self.control.set_sensitive(False) self.play_opts.set_sensitive(False) def on_key_press_event(self, widget, event): ctrl = (event.state & Gdk.ModifierType.CONTROL_MASK) if ctrl: if event.keyval == 108: # ctrl + l self.cover_playlist_view.show_lyrics() else: if event.keyval == 32: # space if not self.browser.search_started(): self.control.play_button.grab_focus() elif event.keyval == 269025044: # AudioPlay self.control.play_button.grab_focus() self.control.play_button.emit("clicked") elif event.keyval == 269025047: # AudioNext self.control.next_button.grab_focus() self.control.next_button.emit("clicked") elif event.keyval == 43 or event.keyval == 65451: # + if not self.browser.search_started(): self.control.next_button.grab_focus() self.control.next_button.emit("clicked") elif event.keyval == 269025046: # AudioPrev self.control.prev_button.grab_focus() self.control.prev_button.emit("clicked") elif event.keyval == 45 or event.keyval == 65453: # - if not self.browser.search_started(): self.control.prev_button.grab_focus() self.control.prev_button.emit("clicked") elif event.keyval == 65307: # esc self.browser.back_to_album() elif event.keyval == 65450: # * if not self.browser.search_started(): self.progress.scale.grab_focus() self.progress.seek_forward() elif event.keyval == 65455: # / if not self.browser.search_started(): self.progress.scale.grab_focus() self.progress.seek_backward() elif event.keyval == 65474: # F5 self.update_action.emit("activate", None) elif event.keyval == 65470: # F1 self.help_action.emit("activate", None) def on_save(self, action, param): size=self.get_size() self.settings.set_int("width", size[0]) self.settings.set_int("height", size[1]) self.settings.set_boolean("maximize", self.is_maximized()) self.browser.save_settings() self.cover_playlist_view.save_settings() self.settings.set_int("paned2", self.paned2.get_position()) def on_settings(self, action, param): settings=SettingsDialog(self, self.settings) settings.run() settings.destroy() def on_stats(self, action, param): if self.client.connected(): stats=ServerStats(self, self.client) stats.destroy() def on_update(self, action, param): if self.client.connected(): self.client.wrapped_call("update") def on_help(self, action, param): Gtk.show_uri_on_window(self, "https://github.com/SoongNoonien/mpdevil/wiki/Usage", Gdk.CURRENT_TIME) def on_settings_changed(self, *args): if len(self.settings.get_value("profiles")) > 1: self.profiles.set_property("visible", True) else: self.profiles.set_property("visible", False) def on_playlist_pos_settings_changed(self, *args): if self.settings.get_boolean("playlist-right"): self.cover_playlist_view.set_orientation(Gtk.Orientation.VERTICAL) self.paned2.set_orientation(Gtk.Orientation.HORIZONTAL) else: self.cover_playlist_view.set_orientation(Gtk.Orientation.HORIZONTAL) self.paned2.set_orientation(Gtk.Orientation.VERTICAL) def on_icon_size_changed(self, *args): pixel_size=self.settings.get_int("icon-size") for icon in self.icons.values(): icon.set_pixel_size(pixel_size) ################### # Gtk application # ################### class mpdevil(Gtk.Application): def __init__(self, *args, **kwargs): super().__init__(*args, application_id="org.mpdevil", flags=Gio.ApplicationFlags.FLAGS_NONE, **kwargs) self.settings=Settings() self.client=Client(self.settings) self.window=None def do_activate(self): if not self.window: # allow just one instance self.window=MainWindow(self, self.client, self.settings) self.window.connect("delete-event", self.on_delete_event) self.window.present() def do_startup(self): Gtk.Application.do_startup(self) action=Gio.SimpleAction.new("about", None) action.connect("activate", self.on_about) self.add_action(action) action=Gio.SimpleAction.new("quit", None) action.connect("activate", self.on_quit) self.add_action(action) def on_delete_event(self, *args): if self.settings.get_boolean("stop-on-quit") and self.client.connected(): self.client.wrapped_call("stop") self.quit() def on_about(self, action, param): dialog=AboutDialog(self.window) dialog.run() dialog.destroy() def on_quit(self, action, param): if self.settings.get_boolean("stop-on-quit") and self.client.connected(): self.client.wrapped_call("stop") self.quit() if __name__ == '__main__': app=mpdevil() app.run(sys.argv)