diff --git a/README.md b/README.md index c19e9a3..b614506 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,6 @@ Python modules: - gi (Gtk, Gio, Gdk, GdkPixbuf, Pango, GObject, GLib, Notify) - requests - bs4 (beautifulsoup) -- dbus Run: ```bash diff --git a/bin/mpdevil b/bin/mpdevil index dacb6f5..6acdfdd 100755 --- a/bin/mpdevil +++ b/bin/mpdevil @@ -34,12 +34,6 @@ import os import sys import re -# MPRIS modules -import dbus -import dbus.service -from dbus.mainloop.glib import DBusGMainLoop -DBusGMainLoop(set_as_default=True) - VERSION="0.9.7-dev" # sync with setup.py COVER_REGEX=r"^\.?(album|cover|folder|front).*\.(gif|jpeg|jpg|png)$" @@ -48,23 +42,134 @@ COVER_REGEX=r"^\.?(album|cover|folder|front).*\.(gif|jpeg|jpg|png)$" # MPRIS # ######### -class MPRISInterface(dbus.service.Object): # TODO emit Seeked if needed +class MPRISInterface: # TODO emit Seeked if needed """ - based on 'mpDris2' (master 19.03.2020) by Jean-Philippe Braun , Mantas Mikulėnas + based on 'Lollypop' (master 22.12.2020) by Cedric Bellegarde + and 'mpDris2' (master 19.03.2020) by Jean-Philippe Braun , Mantas Mikulėnas + """ + _MPRIS_IFACE="org.mpris.MediaPlayer2" + _MPRIS_PLAYER_IFACE="org.mpris.MediaPlayer2.Player" + _MPRIS_NAME="org.mpris.MediaPlayer2.mpdevil" + _MPRIS_PATH="/org/mpris/MediaPlayer2" + _INTERFACES_XML=""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """ - def __init__(self, window, client, settings): - super().__init__(dbus.SessionBus(), "/org/mpris/MediaPlayer2") - self._name="org.mpris.MediaPlayer2.mpdevil" + def __init__(self, window, client, settings): # adding vars self._window=window self._client=client self._settings=settings self._metadata={} - self._bus=dbus.SessionBus() + + # MPRIS property mappings + self._prop_mapping={ + self._MPRIS_IFACE: + {"CanQuit": (GLib.Variant("b", False), None), + "CanRaise": (GLib.Variant("b", True), None), + "HasTrackList": (GLib.Variant("b", False), None), + "Identity": (GLib.Variant("s", "mpdevil"), None), + "DesktopEntry": (GLib.Variant("s", "mpdevil"), None), + "SupportedUriSchemes": (GLib.Variant("s", "None"), None), + "SupportedMimeTypes": (GLib.Variant("s", "None"), None)}, + self._MPRIS_PLAYER_IFACE: + {"PlaybackStatus": (self._get_playback_status, None), + "LoopStatus": (self._get_loop_status, self._set_loop_status), + "Rate": (GLib.Variant("d", 1.0), None), + "Shuffle": (self._get_shuffle, self._set_shuffle), + "Metadata": (self._get_metadata, None), + "Volume": (self._get_volume, self._set_volume), + "Position": (self._get_position, None), + "MinimumRate": (GLib.Variant("d", 1.0), None), + "MaximumRate": (GLib.Variant("d", 1.0), None), + "CanGoNext": (self._get_can_next_prev, None), + "CanGoPrevious": (self._get_can_next_prev, None), + "CanPlay": (self._get_can_play_pause_seek, None), + "CanPause": (self._get_can_play_pause_seek, None), + "CanSeek": (self._get_can_play_pause_seek, None), + "CanControl": (GLib.Variant("b", True), None)}, + } # start - self._bus_name=dbus.service.BusName(self._name, bus=self._bus, allow_replacement=True, replace_existing=True) # TODO + self._bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) + Gio.bus_own_name_on_connection(self._bus, self._MPRIS_NAME, Gio.BusNameOwnerFlags.NONE, None, None) + self._node_info=Gio.DBusNodeInfo.new_for_xml(self._INTERFACES_XML) + for interface in self._node_info.interfaces: + self._bus.register_object(self._MPRIS_PATH, interface, self._handle_method_call, None, None) # connect self._client.emitter.connect("state", self._on_state_changed) @@ -76,26 +181,25 @@ class MPRISInterface(dbus.service.Object): # TODO emit Seeked if needed self._client.emitter.connect("connection_error", self._on_connection_error) self._client.emitter.connect("reconnected", self._on_reconnected) - # Interfaces - __prop_interface=dbus.PROPERTIES_IFACE - __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 _handle_method_call(self, connection, sender, object_path, interface_name, method_name, parameters, invocation): + args=list(parameters.unpack()) + result=getattr(self, method_name)(*args) + out_args=self._node_info.lookup_interface(interface_name).lookup_method(method_name).out_args + if out_args == []: + invocation.return_value(None) + else: + signature="("+"".join([arg.signature for arg in out_args])+")" + variant=GLib.Variant(signature, (result,)) + invocation.return_value(variant) - def __get_playback_status(self): + # setter and getter + def _get_playback_status(self): if self._client.connected(): status=self._client.wrapped_call("status") - return {"play": "Playing", "pause": "Paused", "stop": "Stopped"}[status["state"]] - return "Stopped" + return GLib.Variant("s", {"play": "Playing", "pause": "Paused", "stop": "Stopped"}[status["state"]]) + return GLib.Variant("s", "Stopped") - def __set_loop_status(self, value): + def _set_loop_status(self, value): if self._client.connected(): if value == "Playlist": self._client.wrapped_call("repeat", 1) @@ -106,177 +210,142 @@ class MPRISInterface(dbus.service.Object): # TODO emit Seeked if needed elif value == "None": self._client.wrapped_call("repeat", 0) self._client.wrapped_call("single", 0) - else: - raise dbus.exceptions.DBusException("Loop mode '{}' not supported".format(value)) - return - def __get_loop_status(self): + def _get_loop_status(self): if self._client.connected(): status=self._client.wrapped_call("status") if int(status["repeat"]) == 1: if int(status.get("single", 0)) == 1: - return "Track" + return GLib.Variant("s", "Track") else: - return "Playlist" + return GLib.Variant("s", "Playlist") else: - return "None" - return "None" + return GLib.Variant("s", "None") + return GLib.Variant("s", "None") - def __set_shuffle(self, value): + def _set_shuffle(self, value): if self._client.connected(): - self._client.wrapped_call("random", value) - return - - def __get_shuffle(self): - if self._client.connected(): - if int(self._client.wrapped_call("status")["random"]) == 1: - return True + if value: + self._client.wrapped_call("random", "1") else: - return False - return False + self._client.wrapped_call("random", "0") - def __get_metadata(self): - return dbus.Dictionary(self._metadata, signature="sv") - - def __get_volume(self): + def _get_shuffle(self): if self._client.connected(): - return float(self._client.wrapped_call("status").get("volume", 0))/100 - return 0.0 + if self._client.wrapped_call("status")["random"] == "1": + return GLib.Variant("b", True) + else: + return GLib.Variant("b", False) + return GLib.Variant("b", False) - def __set_volume(self, value): + def _get_metadata(self): + return GLib.Variant("a{sv}", self._metadata) + + def _get_volume(self): + if self._client.connected(): + return GLib.Variant("d", float(self._client.wrapped_call("status").get("volume", 0))/100) + return GLib.Variant("d", 0) + + def _set_volume(self, value): if self._client.connected(): if value >= 0 and value <= 1: self._client.wrapped_call("setvol", int(value * 100)) - return - def __get_position(self): + def _get_position(self): if self._client.connected(): status=self._client.wrapped_call("status") - return dbus.Int64(float(status.get("elapsed", 0))*1000000) - return dbus.Int64(0) + return GLib.Variant("x", float(status.get("elapsed", 0))*1000000) + return GLib.Variant("x", 0) - def __get_can_next_prev(self): + def _get_can_next_prev(self): if self._client.connected(): status=self._client.wrapped_call("status") if status["state"] == "stop": - return False + return GLib.Variant("b", False) else: - return True - return False + return GLib.Variant("b", True) + return GLib.Variant("b", False) - def __get_can_play_pause_seek(self): - return self._client.connected() + def _get_can_play_pause_seek(self): + return GLib.Variant("b", self._client.connected()) - __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": (__get_can_play_pause_seek, None), - "CanPause": (__get_can_play_pause_seek, None), - "CanSeek": (__get_can_play_pause_seek, None), - "CanControl": (True, None), - } - __prop_mapping={ - __player_interface: __player_props, - __root_interface: __root_props, - } + # introspect methods + def Introspect(self): + return self._INTERFACES_XML - # Prop methods - @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] + # property methods + def Get(self, interface_name, prop): + getter, setter=self._prop_mapping[interface_name][prop] if callable(getter): - return getter(self) + return getter() 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] + def Set(self, interface_name, prop, value): + getter, setter=self._prop_mapping[interface_name][prop] if setter is not None: - setter(self, value) + setter(value) - @dbus.service.method(__prop_interface, in_signature="s", out_signature="a{sv}") - def GetAll(self, interface): + def GetAll(self, interface_name): read_props={} - props=self.__prop_mapping[interface] - for key, (getter, setter) in props.items(): - if callable(getter): - getter=getter(self) - read_props[key]=getter + try: + props=self._prop_mapping[interface_name] + for key, (getter, setter) in props.items(): + if callable(getter): + getter=getter() + read_props[key]=getter + except KeyError: # interface has no properties + pass return read_props - # Root methods - @dbus.service.method(__root_interface, in_signature="", out_signature="") + def PropertiesChanged(self, interface_name, changed_properties, invalidated_properties): + self._bus.emit_signal( + None, self._MPRIS_PATH, "org.freedesktop.DBus.Properties", "PropertiesChanged", + GLib.Variant.new_tuple( + GLib.Variant("s", interface_name), + GLib.Variant("a{sv}", changed_properties), + GLib.Variant("as", invalidated_properties) + ) + ) + + # root methods def Raise(self): self._window.present() - return - @dbus.service.method(__root_interface, in_signature="", out_signature="") def Quit(self): - return + app_action_group=self._window.get_action_group("app") + quit_action=app_action_group.lookup_action("quit") + quit_action.activate() - # Player methods - @dbus.service.signal(__player_interface, signature="x") - def Seeked(self, position): - return float(position) - - @dbus.service.method(__player_interface, in_signature="", out_signature="") + # player methods 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): if offset > 0: offset="+"+str(offset/1000000) else: offset=str(offset/1000000) self._client.wrapped_call("seekcur", offset) - return - @dbus.service.method(__player_interface, in_signature="ox", out_signature="") def SetPosition(self, trackid, position): song=self._client.wrapped_call("currentsong") if str(trackid).split("/")[-1] != song["id"]: @@ -284,29 +353,17 @@ class MPRISInterface(dbus.service.Object): # TODO emit Seeked if needed mpd_pos=position/1000000 if mpd_pos >= 0 and mpd_pos <= float(song["duration"]): self._client.wrapped_call("seekcur", str(mpd_pos)) - return - @dbus.service.method(__player_interface, in_signature="s", out_signature="") def OpenUri(self, uri): - return + pass - # MPRIS implemented metadata tags (all: http://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata) - implemented_tags={ - "mpris:trackid": dbus.ObjectPath, - "mpris:length": dbus.Int64, - "mpris:artUrl": str, - "xesam:album": str, - "xesam:albumArtist": list, - "xesam:artist": list, - "xesam:composer": list, - "xesam:contentCreated": str, - "xesam:discNumber": int, - "xesam:genre": list, - "xesam:title": str, - "xesam:trackNumber": int, - "xesam:url": str, - } + def Seeked(self, position): + self._bus.emit_signal( + None, self._MPRIS_PATH, self._MPRIS_PLAYER_IFACE, "Seeked", + GLib.Variant.new_tuple(GLib.Variant("x", position)) + ) + # other methods def _update_metadata(self): """ Translate metadata returned by MPD to the MPRIS v2 syntax. @@ -315,67 +372,64 @@ class MPRISInterface(dbus.service.Object): # TODO emit Seeked if needed mpd_meta=self._client.wrapped_call("currentsong") # raw values needed for cover song=ClientHelper.song_to_list_dict(mpd_meta) self._metadata={} - for tag, xesam_tag in (("album","album"),("title","title"),("track","trackNumber"),("disc","discNumber"),("date","contentCreated")): + for tag, xesam_tag in (("album","album"),("title","title"),("date","contentCreated")): if tag in song: - self._metadata["xesam:{}".format(xesam_tag)]=song[tag][0] + self._metadata["xesam:{}".format(xesam_tag)]=GLib.Variant("s", song[tag][0]) + for tag, xesam_tag in (("track","trackNumber"),("disc","discNumber")): + if tag in song: + self._metadata["xesam:{}".format(xesam_tag)]=GLib.Variant("i", int(song[tag][0])) for tag, xesam_tag in (("albumartist","albumArtist"),("artist","artist"),("composer","composer"),("genre","genre")): if tag in song: - self._metadata["xesam:{}".format(xesam_tag)]=song[tag] + self._metadata["xesam:{}".format(xesam_tag)]=GLib.Variant("as", song[tag]) if "id" in song: - self._metadata["mpris:trackid"]="/org/mpris/MediaPlayer2/Track/{}".format(song["id"][0]) + self._metadata["mpris:trackid"]=GLib.Variant("o", self._MPRIS_PATH+"/Track/{}".format(song["id"][0])) if "time" in song: - self._metadata["mpris:length"]=float(song["duration"][0]) * 1000000 + self._metadata["mpris:length"]=GLib.Variant("x", float(song["duration"][0])*1000000) if "file" in song: song_file=song["file"][0] lib_path=self._settings.get_value("paths")[self._settings.get_int("active-profile")] - self._metadata["xesam:url"]="file://{}".format(os.path.join(lib_path, song_file)) + self._metadata["xesam:url"]=GLib.Variant("s", "file://{}".format(os.path.join(lib_path, song_file))) cover=Cover(self._settings, mpd_meta) if cover.path is not None: - self._metadata["mpris:artUrl"]="file://{}".format(cover.path) - # Cast self._metadata to the correct type, or discard it - for key, value in self._metadata.items(): - try: - self._metadata[key]=self.implemented_tags[key](value) - except: - self._metadata[key]="" + self._metadata["mpris:artUrl"]=GLib.Variant("s", "file://{}".format(cover.path)) - def _update_property(self, interface, prop): - getter, setter=self.__prop_mapping[interface][prop] + def _update_property(self, interface_name, prop): + getter, setter=self._prop_mapping[interface_name][prop] if callable(getter): - value=getter(self) + value=getter() else: value=getter - self.PropertiesChanged(interface, {prop: value}, []) + self.PropertiesChanged(interface_name, {prop: value}, []) return value 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") + self._update_property(self._MPRIS_PLAYER_IFACE, "PlaybackStatus") + self._update_property(self._MPRIS_PLAYER_IFACE, "CanGoNext") + self._update_property(self._MPRIS_PLAYER_IFACE, "CanGoPrevious") def _on_song_changed(self, *args): self._update_metadata() - self._update_property("org.mpris.MediaPlayer2.Player", "Metadata") + self._update_property(self._MPRIS_PLAYER_IFACE, "Metadata") def _on_volume_changed(self, *args): - self._update_property("org.mpris.MediaPlayer2.Player", "Volume") + self._update_property(self._MPRIS_PLAYER_IFACE, "Volume") def _on_loop_changed(self, *args): - self._update_property("org.mpris.MediaPlayer2.Player", "LoopStatus") + self._update_property(self._MPRIS_PLAYER_IFACE, "LoopStatus") def _on_random_changed(self, *args): - self._update_property("org.mpris.MediaPlayer2.Player", "Shuffle") + self._update_property(self._MPRIS_PLAYER_IFACE, "Shuffle") def _on_reconnected(self, *args): properties=("CanPlay","CanPause","CanSeek") for p in properties: - self._update_property("org.mpris.MediaPlayer2.Player", p) + self._update_property(self._MPRIS_PLAYER_IFACE, p) def _on_connection_error(self, *args): self._metadata={} properties=("PlaybackStatus","CanGoNext","CanGoPrevious","Metadata","Volume","LoopStatus","Shuffle","CanPlay","CanPause","CanSeek") for p in properties: - self._update_property("org.mpris.MediaPlayer2.Player", p) + self._update_property(self._MPRIS_PLAYER_IFACE, p) ###################### # MPD client wrapper #