"""This is an API for interacting with the Spotify executable via dbus on linux. It allows you to control playback in Spotify (play, pause, next track, etc.) and retrieve metadata about the currently playing track. The functions in this module require the Spotify executable to be running. If it isn't, they will throw a `SpotifyNotRunningException`. Dependencies: - [dbus-python](https://pypi.org/project/dbus-python/) """ import dbus from datetime import timedelta class SpotifyNotRunningException(RuntimeError): """Raised when spotify is not running. Specifically, this exception is raised when a query to dbus fails to find the dbus object used to communicate with the spotify executable. """ class NoActiveTrackException(RuntimeError): """Raised when there is currently no track playing. This exception is raised when a method related to the currently playing track is called (such as getting the current track's metadata), but there is no active track playing. """ _SPOTIFY_BUS_NAME = "org.mpris.MediaPlayer2.spotify" _SPOTIFY_BUS_PLAYER_OBJECT_PATH = "/org/mpris/MediaPlayer2" def spotify_is_running(): """Checks whether the spotify executable is running. This is done by checking whether the spotify bus is advertised as available by dbus. Returns ------- `True` If spotify is running. `False` If spotify is not running. """ # Connect to the session bus session_bus = dbus.SessionBus() # Check if spotify is in the list of clients return _SPOTIFY_BUS_NAME in session_bus.list_names() def _get_spotify_dbus_obj(): """Gets the spotify player object from the spotify bus. Returns ------- `spotify_player_dbus_object` : (dbus.proxies.ProxyObject)[https://dbus.freedesktop.org/doc/dbus-python/dbus.proxies.html#dbus.proxies.ProxyObject] A dbus-python proxy object for the spotify dbus object. Raises ------ `SpotifyNotRunningException` When the spotify executable is not running. """ # Connect to the session bus session_bus = dbus.SessionBus() if not spotify_is_running(): raise SpotifyNotRunningException( "Failed to connect to the spotify bus: Spotify not running." ) # Get and return the proxy object for the spotify player dbus object return session_bus.get_object( _SPOTIFY_BUS_NAME, _SPOTIFY_BUS_PLAYER_OBJECT_PATH ) def _get_interface(interface_name): """Gets a dbus interface to the specified name in the spotify dbus object. Parameters ---------- `interface_name` : str The name of the interface to get. Returns ------- `interface` : [dbus.Interface](https://dbus.freedesktop.org/doc/dbus-python/dbus.html#dbus.Interface) A python-dbus interface having the specified name, connected to the spotify bus object. Raises ------ `SpotifyNotRunningException` When the spotify executable is not running. """ interface = dbus.Interface(_get_spotify_dbus_obj(), interface_name) return interface _INTERFACE_NAME_PROPERTIES = "org.freedesktop.DBus.Properties" _INTERFACE_NAME_PLAYER = "org.mpris.MediaPlayer2.Player" def player_get_playback_status(): """Returns the current playback status as a string. Returns ------- `playback_status` : str - `"stopped"` if the player is currently stopped (no active track). - `"playing"` if the player is currently playing something. - `"paused"` if the player is currently paused. Raises ------ `SpotifyNotRunningException` When the spotify executable is not running. """ interface = _get_interface(_INTERFACE_NAME_PROPERTIES) playback_status = interface.Get(_INTERFACE_NAME_PLAYER, "PlaybackStatus") return str(playback_status).lower() def player_action_toggle_play_pause(): """Toggles the spotify player's state between "playing" and "paused". Raises ------ `SpotifyNotRunningException` When the spotify executable is not running. `NoActiveTrackException` When there is no currently active track. """ interface = _get_interface(_INTERFACE_NAME_PLAYER) if player_get_playback_status() == "stopped": raise NoActiveTrackException( "Failed to toggle player state: no active track." ) interface.PlayPause() def player_action_play(): """Sets the spotify player's state to "playing". Raises ------ `SpotifyNotRunningException` When the spotify executable is not running. `NoActiveTrackException` When there is no currently active track. """ interface = _get_interface(_INTERFACE_NAME_PLAYER) if player_get_playback_status() == "stopped": raise NoActiveTrackException( "Failed to change player state: no active track." ) interface.Play() def player_action_pause(): """Sets the spotify player's state to "paused". Raises ------ `SpotifyNotRunningException` When the spotify executable is not running. `NoActiveTrackException` When there is no currently active track. """ interface = _get_interface(_INTERFACE_NAME_PLAYER) if player_get_playback_status() == "stopped": raise NoActiveTrackException( "Failed to change player state: no active track." ) interface.Pause() def player_action_previous(): """Instructs the spotify player to play the previous track. Raises ------ `SpotifyNotRunningException` When the spotify executable is not running. `NoActiveTrackException` When there is no currently active track. """ interface = _get_interface(_INTERFACE_NAME_PLAYER) if player_get_playback_status() == "stopped": raise NoActiveTrackException( "Failed to change player state: no active track." ) interface.Previous() def player_action_next(): """Instructs the spotify player to play the next track. Raises ------ `SpotifyNotRunningException` When the spotify executable is not running. `NoActiveTrackException` When there is no currently active track. """ interface = _get_interface(_INTERFACE_NAME_PLAYER) if player_get_playback_status() == "stopped": raise NoActiveTrackException( "Failed to change player state: no active track." ) interface.Next() def player_get_track_metadata(): """Returns a dict containing the currently playing track's metadata. The returned dict contains the following key-value-pairs: Returns ------- `metadata` : dict A dictionary containing track metadata, in the following format: - `'album_artist_names'`: album artist names (list of str) - `'album_name'`: album name (str) - `'artwork_url'`: URL to album art image on Spotify's CDN (str) - `'auto_rating'`: not sure what this is (float) - `'disc_number'`: the album's disc number on which this track is (int) - `'length'`: length of the track ([datetime.timedelta](https://docs.python.org/3/library/datetime.html#timedelta-objects)) - `'title'`: title of the track (str) - `'track_id'`: Spotify track ID (str) - `'track_number'`: number of the track within the album disc (int) - `'track_url'`: URL to the track at `https://open.spotify.com/track/` (str) Raises ------ `SpotifyNotRunningException` When the spotify executable is not running. `NoActiveTrackException` When there is no currently active track. """ interface = _get_interface(_INTERFACE_NAME_PROPERTIES) if player_get_playback_status() == "stopped": raise NoActiveTrackException( "Failed to get current track metadata: no active track." ) # Get metadata as a dbus.Dictionary raw = interface.Get(_INTERFACE_NAME_PLAYER, "Metadata") # HACK manually transfer metadata into a normal python dict metadata = dict() metadata["track_id"] = str(raw["mpris:trackid"]).split("/")[-1] metadata["length"] = timedelta(microseconds=int(str(raw["mpris:length"]))) metadata["artwork_url"] = str(raw["mpris:artUrl"]) metadata["album_name"] = str(raw["xesam:album"]) metadata["album_artist_names"] = [str(name) for name in raw["xesam:albumArtist"]] metadata["artist_names"] = [str(name) for name in raw["xesam:artist"]] metadata["album_name"] = str(raw["xesam:album"]) metadata["auto_rating"] = float(raw["xesam:autoRating"]) metadata["disc_number"] = int(raw["xesam:discNumber"]) metadata["track_number"] = int(raw["xesam:trackNumber"]) metadata["title"] = str(raw["xesam:title"]) metadata["track_url"] = str(raw["xesam:url"]) return metadata