feat: create project and readme
This commit is contained in:
parent
4e75a4b6fa
commit
89975fec88
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
|
||||
|
||||
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…
x
Reference in New Issue
Block a user