"""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. """ def _get_spotify_dbus_obj(): """Gets the spotify object from the current dbus session bus. Returns ------- `spotify_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 dbus fails to locate the spotify object. """ session_bus = dbus.SessionBus() bus_data = ("org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2") try: spotify_dbus_object = session_bus.get_object(*bus_data) except dbus.exceptions.DBusException as e: raise SpotifyNotRunningException( "The spotify executable is not running." ) from e return spotify_dbus_object 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