feat: create project and readme
This commit is contained in:
parent
4e75a4b6fa
commit
89975fec88
3 changed files with 335 additions and 1 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
**/__pycache__
|
67
README.md
67
README.md
|
@ -1,3 +1,68 @@
|
||||||
# spotifyctl
|
# 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
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
|
Loading…
Add table
Reference in a new issue