Merge pull request #514 from ncoop/mpris-api

Add DBus callbacks to now_playing
This commit is contained in:
enkore 2017-01-11 12:35:44 +01:00 committed by GitHub
commit 08ccc58ee5

View File

@ -1,5 +1,3 @@
import functools
from os.path import basename from os.path import basename
import dbus import dbus
@ -8,28 +6,69 @@ from i3pystatus import IntervalModule, formatp
from i3pystatus.core.util import TimeWrapper from i3pystatus.core.util import TimeWrapper
class Dbus:
obj_dbus = "org.freedesktop.DBus"
path_dbus = "/org/freedesktop/DBus"
obj_player = "org.mpris.MediaPlayer2"
path_player = "/org/mpris/MediaPlayer2"
intf_props = obj_dbus + ".Properties"
intf_player = obj_player + ".Player"
class NoPlayerException(Exception): class NoPlayerException(Exception):
pass pass
class NowPlaying(IntervalModule): class NowPlaying(IntervalModule):
""" """
Shows currently playing track information, supports most media players Shows currently playing track information. Supports media players that \
conform to the Media Player Remote Interfacing Specification.
* Requires python-dbus available from every distros' package manager. * Requires ``python-dbus`` from your distro package manager, or \
``dbus-python`` from PyPI.
Left click on the module play/pauses, right click goes to the next track. Left click on the module to play/pause, and right click to go to the next \
track.
.. rubric:: Available formatters (uses :ref:`formatp`) .. rubric:: Available formatters (uses :ref:`formatp`)
* `{title}` (the title of the current song) * `{title}` (the title of the current song)
* `{album}` (the album of the current song, can be an empty string (e.g. for online streams)) * `{album}` (the album of the current song, can be an empty string \
(e.g. for online streams))
* `{artist}` (can be empty, too) * `{artist}` (can be empty, too)
* `{filename}` (file name with out extension and path; empty unless title is empty) * `{filename}` (file name with out extension and path; empty unless \
* `{song_elapsed}` (position in the currently playing song, uses :ref:`TimeWrapper`, default is `%m:%S`) title is empty)
* `{song_elapsed}` (position in the currently playing song, uses \
:ref:`TimeWrapper`, default is `%m:%S`)
* `{song_length}` (length of the current song, same as song_elapsed) * `{song_length}` (length of the current song, same as song_elapsed)
* `{status}` (play, pause, stop mapped through the `status` dictionary) * `{status}` (play, pause, stop mapped through the `status` dictionary)
* `{volume}` (volume) * `{volume}` (volume)
.. rubric:: Available callbacks
* ``playpause`` Plays if paused or stopped, otherwise pauses.
* ``next_song`` Goes to next track in the playlist.
* ``player_command`` Invoke a command with the `MediaPlayer2.Player` \
interface. The method name and its arguments are appended as list elements.
* ``player_prop`` Get or set a property of the `MediaPlayer2.Player` \
interface. Append the property name to get, or the name and a value to set.
`MediaPlayer2.Player` methods and properties are documented at \
https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html
Your player may not support the full interface.
Example module registration with callbacks:
::
status.register("now_playing",
on_leftclick=["player_command", "PlayPause"],
on_rightclick=["player_command", "Stop"],
on_middleclick=["player_prop", "Shuffle", True],
on_upscroll=["player_command", "Seek", -10000000],
on_downscroll=["player_command", "Seek", +10000000])
""" """
interval = 1 interval = 1
@ -69,11 +108,11 @@ class NowPlaying(IntervalModule):
old_player = None old_player = None
def find_player(self): def find_player(self):
obj = dbus.SessionBus().get_object("org.freedesktop.DBus", "/org/freedesktop/DBus") obj = dbus.SessionBus().get_object(Dbus.obj_dbus, Dbus.path_dbus)
def get_players(methodname): def get_players(methodname):
method = obj.get_dbus_method(methodname, 'org.freedesktop.DBus') method = obj.get_dbus_method(methodname, Dbus.obj_dbus)
return [a for a in method() if a.startswith("org.mpris.MediaPlayer2.")] return [a for a in method() if a.startswith(Dbus.obj_player + ".")]
players = get_players('ListNames') players = get_players('ListNames')
if not players: if not players:
@ -87,45 +126,51 @@ class NowPlaying(IntervalModule):
def get_player(self): def get_player(self):
if self.player: if self.player:
player = "org.mpris.MediaPlayer2." + self.player player = Dbus.obj_player + "." + self.player
try: try:
return dbus.SessionBus().get_object(player, "/org/mpris/MediaPlayer2") return dbus.SessionBus().get_object(player, Dbus.path_player)
except dbus.exceptions.DBusException: except dbus.exceptions.DBusException:
raise NoPlayerException() raise NoPlayerException()
else: else:
player = self.find_player() player = self.find_player()
return dbus.SessionBus().get_object(player, "/org/mpris/MediaPlayer2") return dbus.SessionBus().get_object(player, Dbus.path_player)
def run(self): def run(self):
try: try:
player = self.get_player() player = self.get_player()
properties = dbus.Interface(player, "org.freedesktop.DBus.Properties") properties = dbus.Interface(player, Dbus.intf_props)
def get_prop(name, default=None): def get_prop(name, default=None):
try: try:
return properties.Get("org.mpris.MediaPlayer2.Player", name) return properties.Get(Dbus.intf_player, name)
except dbus.exceptions.DBusException: except dbus.exceptions.DBusException:
return default return default
currentsong = get_prop("Metadata") currentsong = get_prop("Metadata")
fdict = { fdict = {
"status": self.status[self.statusmap[get_prop("PlaybackStatus")]], "status": self.status[self.statusmap[
"len": 0, # TODO: Use optional(!) TrackList interface for this to gain 100 % mpd<->now_playing compat get_prop("PlaybackStatus")]],
# TODO: Use optional(!) TrackList interface for this to
# gain 100 % mpd<->now_playing compat
"len": 0,
"pos": 0, "pos": 0,
"volume": int(get_prop("Volume", 0) * 100), "volume": int(get_prop("Volume", 0) * 100),
"title": currentsong.get("xesam:title", ""), "title": currentsong.get("xesam:title", ""),
"album": currentsong.get("xesam:album", ""), "album": currentsong.get("xesam:album", ""),
"artist": ", ".join(currentsong.get("xesam:artist", "")), "artist": ", ".join(currentsong.get("xesam:artist", "")),
"song_length": TimeWrapper((currentsong.get("mpris:length") or 0) / 1000 ** 2), "oong_length": TimeWrapper((currentsong.get("mpris:length") or
"song_elapsed": TimeWrapper((get_prop("Position") or 0) / 1000 ** 2), 0) / 1000 ** 2),
"song_elapsed": TimeWrapper((get_prop("Position") or 0) /
1000 ** 2),
"filename": "", "filename": "",
} }
if not fdict["title"]: if not fdict["title"]:
fdict["filename"] = '.'.join( fdict["filename"] = '.'.join(
basename((currentsong.get("xesam:url") or "")).split('.')[:-1]) basename((currentsong.get("xesam:url") or "")).
split('.')[:-1])
self.data = fdict self.data = fdict
self.output = { self.output = {
@ -159,7 +204,7 @@ class NowPlaying(IntervalModule):
def playpause(self): def playpause(self):
try: try:
dbus.Interface(self.get_player(), "org.mpris.MediaPlayer2.Player").PlayPause() dbus.Interface(self.get_player(), Dbus.intf_player).PlayPause()
except NoPlayerException: except NoPlayerException:
return return
except dbus.exceptions.DBusException: except dbus.exceptions.DBusException:
@ -167,7 +212,29 @@ class NowPlaying(IntervalModule):
def next_song(self): def next_song(self):
try: try:
dbus.Interface(self.get_player(), "org.mpris.MediaPlayer2.Player").Next() dbus.Interface(self.get_player(), Dbus.intf_player).Next()
except NoPlayerException:
return
except dbus.exceptions.DBusException:
return
def player_command(self, command, *args):
try:
interface = dbus.Interface(self.get_player(), Dbus.intf_player)
getattr(interface, command)(*args)
except NoPlayerException:
return
except dbus.exceptions.DBusException:
return
def player_prop(self, name, value=None):
try:
properties = dbus.Interface(self.get_player(), Dbus.intf_props)
# None/null/nil implies get because it's not a valid DBus datatype.
if value is None:
return properties.Get(Dbus.intf_player, name)
else:
properties.Set(Dbus.intf_player, name, value)
except NoPlayerException: except NoPlayerException:
return return
except dbus.exceptions.DBusException: except dbus.exceptions.DBusException: