diff --git a/i3pystatus/now_playing.py b/i3pystatus/now_playing.py index 953c4d8..70c08b9 100644 --- a/i3pystatus/now_playing.py +++ b/i3pystatus/now_playing.py @@ -1,5 +1,3 @@ - -import functools from os.path import basename import dbus @@ -8,28 +6,69 @@ from i3pystatus import IntervalModule, formatp 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): pass 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`) * `{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) - * `{filename}` — (file name with out extension and path; empty unless title is empty) - * `{song_elapsed}` — (position in the currently playing song, uses :ref:`TimeWrapper`, default is `%m:%S`) + * `{filename}` — (file name with out extension and path; empty unless \ +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) * `{status}` — (play, pause, stop mapped through the `status` dictionary) * `{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 @@ -69,11 +108,11 @@ class NowPlaying(IntervalModule): old_player = None 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): - method = obj.get_dbus_method(methodname, 'org.freedesktop.DBus') - return [a for a in method() if a.startswith("org.mpris.MediaPlayer2.")] + method = obj.get_dbus_method(methodname, Dbus.obj_dbus) + return [a for a in method() if a.startswith(Dbus.obj_player + ".")] players = get_players('ListNames') if not players: @@ -87,45 +126,51 @@ class NowPlaying(IntervalModule): def get_player(self): if self.player: - player = "org.mpris.MediaPlayer2." + self.player + player = Dbus.obj_player + "." + self.player try: - return dbus.SessionBus().get_object(player, "/org/mpris/MediaPlayer2") + return dbus.SessionBus().get_object(player, Dbus.path_player) except dbus.exceptions.DBusException: raise NoPlayerException() else: 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): try: 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): try: - return properties.Get("org.mpris.MediaPlayer2.Player", name) + return properties.Get(Dbus.intf_player, name) except dbus.exceptions.DBusException: return default currentsong = get_prop("Metadata") fdict = { - "status": self.status[self.statusmap[get_prop("PlaybackStatus")]], - "len": 0, # TODO: Use optional(!) TrackList interface for this to gain 100 % mpd<->now_playing compat + "status": self.status[self.statusmap[ + get_prop("PlaybackStatus")]], + # TODO: Use optional(!) TrackList interface for this to + # gain 100 % mpd<->now_playing compat + "len": 0, "pos": 0, "volume": int(get_prop("Volume", 0) * 100), "title": currentsong.get("xesam:title", ""), "album": currentsong.get("xesam:album", ""), "artist": ", ".join(currentsong.get("xesam:artist", "")), - "song_length": TimeWrapper((currentsong.get("mpris:length") or 0) / 1000 ** 2), - "song_elapsed": TimeWrapper((get_prop("Position") or 0) / 1000 ** 2), + "oong_length": TimeWrapper((currentsong.get("mpris:length") or + 0) / 1000 ** 2), + "song_elapsed": TimeWrapper((get_prop("Position") or 0) / + 1000 ** 2), "filename": "", } if not fdict["title"]: fdict["filename"] = '.'.join( - basename((currentsong.get("xesam:url") or "")).split('.')[:-1]) + basename((currentsong.get("xesam:url") or "")). + split('.')[:-1]) self.data = fdict self.output = { @@ -159,7 +204,7 @@ class NowPlaying(IntervalModule): def playpause(self): try: - dbus.Interface(self.get_player(), "org.mpris.MediaPlayer2.Player").PlayPause() + dbus.Interface(self.get_player(), Dbus.intf_player).PlayPause() except NoPlayerException: return except dbus.exceptions.DBusException: @@ -167,7 +212,29 @@ class NowPlaying(IntervalModule): def next_song(self): 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: return except dbus.exceptions.DBusException: