297 lines
8.7 KiB
Python
297 lines
8.7 KiB
Python
"""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/<track_id>` (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
|