migrated MPRISInterface to Gio

This commit is contained in:
Martin Wagner 2020-12-24 13:21:08 +01:00
parent 7ae0f1e216
commit b6be0f0430
2 changed files with 230 additions and 177 deletions

View File

@ -61,7 +61,6 @@ Python modules:
- gi (Gtk, Gio, Gdk, GdkPixbuf, Pango, GObject, GLib, Notify)
- requests
- bs4 (beautifulsoup)
- dbus
Run:
```bash

View File

@ -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 <eon@patapon.info>, Mantas Mikulėnas <grawity@gmail.com>
based on 'Lollypop' (master 22.12.2020) by Cedric Bellegarde <cedric.bellegarde@adishatz.org>
and 'mpDris2' (master 19.03.2020) by Jean-Philippe Braun <eon@patapon.info>, Mantas Mikulėnas <grawity@gmail.com>
"""
_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="""
<!DOCTYPE node PUBLIC
"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg name="data" direction="out" type="s"/>
</method>
</interface>
<interface name="org.freedesktop.DBus.Properties">
<method name="Get">
<arg name="interface" direction="in" type="s"/>
<arg name="property" direction="in" type="s"/>
<arg name="value" direction="out" type="v"/>
</method>
<method name="Set">
<arg name="interface_name" direction="in" type="s"/>
<arg name="property_name" direction="in" type="s"/>
<arg name="value" direction="in" type="v"/>
</method>
<method name="GetAll">
<arg name="interface" direction="in" type="s"/>
<arg name="properties" direction="out" type="a{sv}"/>
</method>
</interface>
<interface name="org.mpris.MediaPlayer2">
<method name="Raise">
</method>
<method name="Quit">
</method>
<property name="CanQuit" type="b" access="read" />
<property name="CanRaise" type="b" access="read" />
<property name="HasTrackList" type="b" access="read"/>
<property name="Identity" type="s" access="read"/>
<property name="DesktopEntry" type="s" access="read"/>
<property name="SupportedUriSchemes" type="as" access="read"/>
<property name="SupportedMimeTypes" type="as" access="read"/>
</interface>
<interface name="org.mpris.MediaPlayer2.Player">
<method name="Next"/>
<method name="Previous"/>
<method name="Pause"/>
<method name="PlayPause"/>
<method name="Stop"/>
<method name="Play"/>
<method name="Seek">
<arg direction="in" name="Offset" type="x"/>
</method>
<method name="SetPosition">
<arg direction="in" name="TrackId" type="o"/>
<arg direction="in" name="Position" type="x"/>
</method>
<method name="OpenUri">
<arg direction="in" name="Uri" type="s"/>
</method>
<signal name="Seeked">
<arg name="Position" type="x"/>
</signal>
<property name="PlaybackStatus" type="s" access="read"/>
<property name="LoopStatus" type="s" access="readwrite"/>
<property name="Rate" type="d" access="readwrite"/>
<property name="Shuffle" type="b" access="readwrite"/>
<property name="Metadata" type="a{sv}" access="read"/>
<property name="Volume" type="d" access="readwrite"/>
<property name="Position" type="x" access="read"/>
<property name="MinimumRate" type="d" access="read"/>
<property name="MaximumRate" type="d" access="read"/>
<property name="CanGoNext" type="b" access="read"/>
<property name="CanGoPrevious" type="b" access="read"/>
<property name="CanPlay" type="b" access="read"/>
<property name="CanPause" type="b" access="read"/>
<property name="CanSeek" type="b" access="read"/>
<property name="CanControl" type="b" access="read"/>
</interface>
</node>
"""
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 #