-
Notifications
You must be signed in to change notification settings - Fork 638
Events
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.
PLAYER_EVENT
will contain the name of the Event. Additional environment variables will follow depending on the Event.
The mapping is as follows:
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.
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 |
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 |
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 |
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 |
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 |
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 |
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 |
Fired when the Track changes.
Track Changed has Common Fields shared by both Tracks and Episodes and Fields unique to Tracks and Episodes.
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 |
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 |
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 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 / 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 |
In this section we will focus on events that are the most useful to consumers.
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"
}
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.
For example they started the session on the desktop client and then later used their phone as a control point.
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"
}
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.
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.
These events can be treated as the same as far as the consumer is concerned.
The Track position has changed in some unpredictable way.
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 and behavior have not changed and are identical to that of the legacy behaviour, documented below.
An example non-blocking Event Python script can be found Here.
Refer to the below Example for 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.
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 |
Key | Value | Description |
---|---|---|
PLAYER_EVENT | started |
Name of the Event |
TRACK_ID | Spotify Track ID | ID of the Track |
Key | Value | Description |
---|---|---|
PLAYER_EVENT | stopped |
Name of the Event |
TRACK_ID | Spotify Track ID | ID of the Track |
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 |
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 |
Key | Value | Description |
---|---|---|
PLAYER_EVENT | preloading |
Name of the Event |
TRACK_ID | Spotify Track ID | ID of the Track |
Key | Value | Description |
---|---|---|
PLAYER_EVENT | volume_set |
Name of the Event |
VOLUME | volume | Volume 0 - 65535 |
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.
Key | Value | Description |
---|---|---|
PLAYER_EVENT | sink |
Name of the Event |
SINK_STATUS | running |
The sink is about to be opened |
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 |
Key | Value | Description |
---|---|---|
PLAYER_EVENT | sink |
Name of the Event |
SINK_STATUS | closed |
The sink has closed |
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