Events - librespot-org/librespot GitHub Wiki
With librespot --onevent=/path/to/my/event/script/program
users can subscribe to non-blocking events. By also passing emit-sink-events
users can also subscribe to blocking sink events with the same script/program. The purpose of blocking sink events is to block the player thread to allow for something to be done before the sink is opened or after it is closed.
The type of script/program you use to handle events is completely up to you, librespot
simply runs the script/program every time an event is fired and passes events via environment variables.
Event Keys and Values
PLAYER_EVENT
will contain the name of the Event. Additional environment variables will follow depending on the Event.
[!TIP] Some fields use
\n
to separate multiple occurrences of a field. When usingecho
to print out the value of an$ENV_VAR
, it needs to be surrounded by quotation marks to properly print out the value. Otherwise, the\n
might be replaced by a whitespace.echo -e "$ENV_VAR"
The mapping is as follows:
Current; >= 0.5.0
Non-blocking Events
Non-blocking events do not block librespot
threads but are buffered and blocking to themselves so that subsequent events are not fired until the event script/program exits after processing the previous event to help guarantee that event scripts/programs do not process events out of order.
Session Connected / Session Disconnected
Fired when a user connects to / disconnects from librespot
.
Session Connected events will be followed immediately by Client Changed, Volume Changed, Auto Play Changed, Filter Explicit Content, Shuffle Changed, and Repeat Changed events so that event consumers will have the initial state of the session.
These Events both share common fields, just PLAYER_EVENT
is different.
Key | Value | Description |
---|---|---|
PLAYER_EVENT | session_connected / session_disconnected |
Name of the Event |
USER_NAME | User Name | Session User Name (really an ID not a display name) |
CONNECTION_ID | Connection ID | Session Connection ID |
Session Client Changed
Fired when the Client changes or immediately after a Session Connected event.
Key | Value | Description |
---|---|---|
PLAYER_EVENT | session_client_changed |
Name of the Event |
CLIENT_ID | Client ID | ID of the Client |
CLIENT_NAME | Client Name | Name of the Client |
CLIENT_BRAND_NAME | Client Brand Name | Brand Name of the Client |
CLIENT_MODEL_NAME | Client Model Name | Model Name of the Client |
Volume Changed
Fired when the Volume changes or immediately after a Session Connected event.
Key | Value | Description |
---|---|---|
PLAYER_EVENT | volume_changed |
Name of the Event |
VOLUME | volume | Volume 0 - 65535 |
Shuffle Changed
Fired when the Shuffle toggle is flipped or immediately after a Session Connected event.
Key | Value | Description |
---|---|---|
PLAYER_EVENT | shuffle_changed |
Name of the Event |
SHUFFLE | True/False | State of the Shuffle toggle |
Repeat Changed
Fired when the Repeat toggle is flipped or immediately after a Session Connected event.
Key | Value | Description |
---|---|---|
PLAYER_EVENT | repeat_changed |
Name of the Event |
REPEAT | True/False | State of the Repeat toggle |
Auto Play Changed
Fired when the Auto Play toggle is flipped or immediately after a Session Connected event.
Key | Value | Description |
---|---|---|
PLAYER_EVENT | auto_play_changed |
Name of the Event |
AUTO_PLAY | True/False | State of the Auto Play toggle |
Filter Explicit Content Changed
Fired when the Filter Explicit Content toggle is flipped or immediately after a Session Connected event.
Key | Value | Description |
---|---|---|
PLAYER_EVENT | filter_explicit_content_changed |
Name of the Event |
FILTER | True/False | State of the Filter Explicit Content toggle |
Track Changed
Fired when the Track changes.
Track Changed has Common Fields shared by both Tracks and Episodes and Fields unique to Tracks and Episodes.
Common Fields
Key | Value | Description |
---|---|---|
PLAYER_EVENT | track_changed |
Name of the Event |
ITEM_TYPE | Track / Episode |
Track or Episode |
TRACK_ID | Spotify Track ID | ID of the Track |
URI | URI | URI of the Track |
NAME | Name | Name of the Track |
DURATION_MS | Milliseconds | Duration in ms |
IS_EXPLICIT | True/False | If the Track is Explicit |
LANGUAGE | Languages | \n separated list of Languages |
COVERS | Cover urls | \n separated list of Cover urls from largest to smallest in size |
Track Item Type Specific Fields
Key | Value | Description |
---|---|---|
NUMBER | Track Number | Number of the Track as it appears on the album |
DISC_NUMBER | Disc Number | Disc Number of the Track as it appears on the album |
POPULARITY | Popularity | Popularity of the Track 0 - 100 |
ALBUM | Album | Album the Track appears on |
ARTISTS | Artists | \n separated list of the artists that appear on the Track |
ALBUM_ARTISTS | Album Artists | \n separated list of the artists of the album |
Episode Item Type Specific Fields
Key | Value | Description |
---|---|---|
SHOW_NAME | Show Name | Name of the Show |
PUBLISH_TIME | Unix Timestamp | Unix Timestamp of the Publish Time |
DESCRIPTION | Description | Description of the Show/Episode |
Playing / Paused / Seeked / Position Correction
Playing / Paused are fired when librespot
is in the respectively state.
Seeked / Position Correction are fired when the position changes due to a seek or an internal librespot
position correction. Track position can be assumed to be advancing at a normal rate otherwise.
These Events all share common fields, just PLAYER_EVENT
is different.
Key | Value | Description |
---|---|---|
PLAYER_EVENT | playing / paused / seeked / position_correction |
Name of the Event |
TRACK_ID | Spotify Track ID | ID of the Track |
POSITION_MS | Milliseconds | Position in ms |
Unavailable / End of Track / Preload Next / Preloading / Loading / Stopped
Unavailable / Preload Next / Preloading / Loading represent internal librespot
states and are useful mainly for debugging.
Stopped is fired when librespot
stops.
End of Track is fired at the end of a Track.
These Events all share common fields, just PLAYER_EVENT
is different.
Key | Value | Description |
---|---|---|
PLAYER_EVENT | unavailable / end_of_track / preload_next / preloading / loading / stopped |
Name of the Event |
TRACK_ID | Spotify Track ID | ID of the Track |
Event Flows
In this section we will focus on events that are the most useful to consumers.
A new user connects:
The Session Connected / Session Disconnected events contain a "username" field but it's not really suitable for display, it's really more of an ID. It will be something like 01234567.
Session Connected > Client Changed > Volume Changed > Auto Play Changed > Filter Explicit Content > Shuffle Changed > Repeat Changed
An example jsonified output when feed though the Example Event Handler Script:
{
"event_time": "2022-09-28 20:14:10.695987",
"event": "session_connected",
"user_name": "XXXXXXXX",
"connection_id": "XXXXXXXX"
}
{
"event_time": "2022-09-28 20:14:10.718546",
"event": "session_client_changed",
"client_id": "XXXXXXXX",
"client_name": "little-baby-war-machine",
"client_brand_name": "spotify",
"client_model_name": "PC desktop"
}
{
"event_time": "2022-09-28 20:14:10.736467",
"event": "volume_changed",
"volume": "65535"
}
{
"event_time": "2022-09-28 20:14:10.757426",
"event": "auto_play_changed",
"auto_play": "true"
}
{
"event_time": "2022-09-28 20:14:10.775620",
"event": "filter_explicit_content_changed",
"filter": "false"
}
{
"event_time": "2022-09-28 20:14:10.795816",
"event": "shuffle_changed",
"shuffle": "false"
}
{
"event_time": "2022-09-28 20:14:10.817437",
"event": "repeat_changed",
"repeat": "false"
}
A user disconnects:
All previous Event info is now invalid.
A consumer should not count on the session picking up where it left off. It is best not to guess, but to just wait for more events.
The user switches devices:
For example they started the session on the desktop client and then later used their phone as a control point.
A new Track starts:
Track Changed > Playing / Paused
An example jsonified output when feed though the Example Event Handler Script:
{
"event_time": "2022-09-28 20:15:28.674763",
"event": "track_changed",
"common_metadata_fields": {
"item_type": "Track",
"track_id": "10Nmj3JCNoMeBQ87uw5j8k",
"uri": "spotify:track:10Nmj3JCNoMeBQ87uw5j8k",
"name": "Dani California",
"duration_ms": "282160",
"is_explicit": "false",
"language": [
"en"
],
"covers": [
"https://i.scdn.co/image/ab67616d0000b27309fd83d32aee93dceba78517",
"https://i.scdn.co/image/ab67616d00001e0209fd83d32aee93dceba78517",
"https://i.scdn.co/image/ab67616d0000485109fd83d32aee93dceba78517"
]
},
"track_metadata_fields": {
"number": "1",
"disc_number": "1",
"popularity": "78",
"album": "Stadium Arcadium",
"artists": [
"Red Hot Chili Peppers"
],
"album_artists": [
"Red Hot Chili Peppers"
]
}
}
{
"event_time": "2022-09-28 20:15:28.694878",
"event": "playing",
"track_id": "10Nmj3JCNoMeBQ87uw5j8k",
"position_ms": "0"
}
A Track ends:
If there are tracks left in the playlist or Auto Play or Repeat is enabled End of Track will be immediately followed by Track Changed > Playing/Paused, otherwise Stopped.
Playback stops:
All previous playback Event info is now invalid.
A consumer should not count on the user picking up where they left off. It is best not to guess, but to just wait for more events.
librespot
corrects it's time:
The user seeks or These events can be treated as the same as far as the consumer is concerned.
The Track position has changed in some unpredictable way.
Keeping track of time
The Track's duration can be found in the Track Changed Event. As mentioned above Playing / Paused / Seeked / Position Correction all contain position fields when those Events are fired you should update your current time accordingly.
Blocking Events
Blocking Events and behavior have not changed and are identical to that of the legacy behaviour, documented below.
Example
An example non-blocking Event Python script can be found Here.
Refer to the below Example for Blocking Events.
Legacy; < 0.5.0
Non-blocking Events
Non-blocking events are non-blocking in every sense. They do not block librespot
threads in any way and the event script/program is run in it's own separate thread for each and every event. librespot
's event handler does not wait for event scripts/programs to exit before it fires the next event. This can lead to data race situations if the events take a variable amount of time for the script/program to process. It is up to the user to keep track of event sequencing. This has been fixed in dev.
Changed
Key | Value | Description |
---|---|---|
PLAYER_EVENT | changed |
Name of the Event |
OLD_TRACK_ID | Spotify Track ID | ID of the previous Track |
TRACK_ID | Spotify Track ID | ID of the new Track |
Started
Key | Value | Description |
---|---|---|
PLAYER_EVENT | started |
Name of the Event |
TRACK_ID | Spotify Track ID | ID of the Track |
Stopped
Key | Value | Description |
---|---|---|
PLAYER_EVENT | stopped |
Name of the Event |
TRACK_ID | Spotify Track ID | ID of the Track |
Playing
Key | Value | Description |
---|---|---|
PLAYER_EVENT | playing |
Name of the Event |
TRACK_ID | Spotify Track ID | ID of the Track |
DURATION_MS | Milliseconds | Duration in ms |
POSITION_MS | Milliseconds | Position in ms |
Paused
Key | Value | Description |
---|---|---|
PLAYER_EVENT | paused |
Name of the Event |
TRACK_ID | Spotify Track ID | ID of the Track |
DURATION_MS | Milliseconds | Duration in ms |
POSITION_MS | Milliseconds | Position in ms |
Preloading
Key | Value | Description |
---|---|---|
PLAYER_EVENT | preloading |
Name of the Event |
TRACK_ID | Spotify Track ID | ID of the Track |
Volume Set
Key | Value | Description |
---|---|---|
PLAYER_EVENT | volume_set |
Name of the Event |
VOLUME | volume | Volume 0 - 65535 |
Blocking Events
Blocking events are blocking in every sense. They block librespot
's player thread and also therefore block themselves. librespot
's player thread will not unblock until the event script/program exits. Blocking events will not be fired by default for that reason. Use blocking events with care.
Running
Key | Value | Description |
---|---|---|
PLAYER_EVENT | sink |
Name of the Event |
SINK_STATUS | running |
The sink is about to be opened |
Temporarily Closed (Transient State)
Key | Value | Description |
---|---|---|
PLAYER_EVENT | sink |
Name of the Event |
SINK_STATUS | temporarily_closed |
The sink has closed, but more than likely will be reopened very shortly |
Closed
Key | Value | Description |
---|---|---|
PLAYER_EVENT | sink |
Name of the Event |
SINK_STATUS | closed |
The sink has closed |
Example
Here is an example Python skeleton to further illustrate:
#!/usr/bin/python3
import os
# Non-blocking Events
if os.environ['PLAYER_EVENT'] == 'changed':
old_track_id = os.environ['OLD_TRACK_ID']
new_track_id = os.environ['TRACK_ID']
# do stuff
elif os.environ['PLAYER_EVENT'] == 'started':
track_id = os.environ['TRACK_ID']
# do suff
elif os.environ['PLAYER_EVENT'] == 'stopped':
track_id = os.environ['TRACK_ID']
# do stuff
elif os.environ['PLAYER_EVENT'] == 'playing':
track_id = os.environ['TRACK_ID']
track_duration_ms = os.environ['DURATION_MS']
track_position_ms = os.environ['POSITION_MS']
# do stuff
elif os.environ['PLAYER_EVENT'] == 'paused':
track_id = os.environ['TRACK_ID']
track_duration_ms = os.environ['DURATION_MS']
track_position_ms = os.environ['POSITION_MS']
# do stuff
elif os.environ['PLAYER_EVENT'] == 'preloading':
track_id = os.environ['TRACK_ID']
# do stuff
elif os.environ['PLAYER_EVENT'] == 'volume_set':
volume = os.environ['VOLUME']
# do stuff
# Blocking Events
elif os.environ['PLAYER_EVENT'] == 'sink':
status = os.environ['SINK_STATUS']
if status == 'running':
# do stuff
elif status == 'temporarily_closed':
# do stuff
elif status == 'closed':
# do stuff