feat: create project and readme

This commit is contained in:
Julian Lobbes 2022-08-23 15:45:55 +02:00
parent 4e75a4b6fa
commit 89975fec88
3 changed files with 335 additions and 1 deletions

1
.gitignore vendored Normal file

@ -0,0 +1 @@
**/__pycache__

@ -1,3 +1,68 @@
# spotifyctl
A python API to query and control Spotify via dbus.
**spotifyctl** is a python module/API with which you can query and control the _
Spotify executable on linux.
It works by talking to Spotify via dbus, letting you control the current playback
or getting metadata about the currently playing track.
## Dependencies
You need [dbus](https://dbus.freedesktop.org) to be installed and enabled on your system.
Dbus is preinstalled on most Linux distros.
**spotifyctl** depends on the [dbus-python](https://pypi.org/project/dbus-python/) package.
Obviously, you need to have Spotify installed as well if you want **spotifyctl** to be useful.
## Getting started
**spotifyctl** comes as a single, self-contained python script/module.
Simply import `spotifyctl` in your python program to access the API:
```python
import spotifyctl
# Get the current state of the Spotify player
spotifyctl.player_get_playback_status()
# returns: "playing"
# Toggle bewteen play/pause
spotifyctl.player_action_toggle_play_pause()
# Skip to the next track
spotifyctl.player_action_next()
# Get some metadata for the current track in a python dict
spotifyctl.player_get_track_metadata()
# returns:
# {
# 'album_artist_names': ['Rick Astley'],
# 'album_name': 'Whenever You Need Somebody',
# 'artist_names': ['Rick Astley'],
# 'artwork_url': 'https://i.scdn.co/image/ab67616d0000b2735755e164993798e0c9ef7d7a',
# 'auto_rating': 0.8,
# 'disc_number': 1,
# 'length': datetime.timedelta(seconds=213, microseconds=573000),
# 'title': 'Never Gonna Give You Up',
# 'track_id': '4cOdK2wGLETKBW3PvgPWqT',
# 'track_number': 1,
# 'track_url': 'https://open.spotify.com/track/4cOdK2wGLETKBW3PvgPWqT'
# }
```
Have a look at [the complete API documentation](https://docs.skyforest.net/spotifyctl/index.html) _
for a more complete list of API calls.
Note that if the Spotify executable is not running, API calls will throw exceptions.
## Documentation
The API documentation is [hosted here](https://docs.skyforest.net/spotifyctl/index.html).
To build it yourself, you can install [pdoc](https://pypi.org/project/pdoc/) and run:
```bash
mkdir doc
pdoc --docformat numpy --output-dir ./doc ./spotify.py
```

268
spotifyctl.py Normal file

@ -0,0 +1,268 @@
"""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/<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