-
Notifications
You must be signed in to change notification settings - Fork 297
remote api documentation
while musikcube
is a rather complete frontend for musikcore
, it only appeals to a very niche audience. on the desktop most users prefer point-and-click, drag-and-drop user interfaces. also in recent years, there has been a significant paradigm shift from thick desktop clients to thin web clients and native mobile apps.
the musikcore
backend library is written in c++
, making it somewhat difficult to integrate with other programming languages and frameworks. it does, however, have an extensible plugin architecture. this plugin architecture was utilized to expose a small language-agnostic, platform-agnostic, "connected" api layer that uses websockets for realtime, bidirectional client/server communication, and http for streaming audio data.
the purpose of this api is to be something small and maintainable that serves the 95% use case. it's not meant to be an end-all-be-all solution that exposes every single bit of functionality musikcore
provides.
it's supposed to be something that's easy to tinker with, and can be used to build and prototype custom music players easily and quickly.
the api is new and subject to change over time. here be dragons.
musikdroid
is an android app that implements most of the functionality described in this document. the code can be found here.
it's important to understand that, out of the box, the remote api should NOT be considered safe for use outside of a local network. the websockets service only supports a simple password challenge, and the audio http server just handles Basic
authorization. it does not provide ssl or tls.
the server also stores the password in plain text in a settings file on the local machine.
you can fix some of this using a reverse proxy to provide ssl termination. details in the ssl-server-setup section. while this improves things, you should exercise caution exposing these services over the internet.
messages sent between the websocket client and server are simple json
structs with the following format:
{
"name": "<name_of_message>",
"id": "<unique_message_id>",
"device_id": "<unique_device_id>", /* request/broadcast only */
"type": "<request | response | broadcast>",
"options" {
/* map of arguments */
}
}
note that there are three types of messages: requests
, responses
and broadcasts
. if one end of the connection sends a request
, the other side of the server must send a response
with the originating message's request name
and id
. broadcast
messages are fire-and-forget and should not be responded to.
the device_id
property is currently optional, but highly recommended for all request
and broadcast
messages originating from a client. when present, the server can cache context-sensitive data on behalf of the client, including snapshots of the play queue and other information. if your device doesn't have a unique id, that's fine! just generate a guid and use that.
the rest of the message structure should be straight forward.
authenticating with the websocket and http servers are straight forward, although relatively insecure.
immediately after connecting to the websocket server, the client must send an authenticate
message as follows:
{
"name": "authenticate",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"password": "<password>"
}
}
upon successful authentication the server will respond as follows:
{
"name": "authenticate",
"type": "response",
"id": "<original_request_id>",
"device_id": "<unique_device_id>",
"options": {
"authenticated": true
}
}
if an incorrect password is supplied by the client, the server will terminate the connection immediately.
authentication against the streaming http server uses basic http authentication. that means every request against the server requires a header like the following:
Authorization: Basic <credentials>
, where <credentials>
is the base64 encoded string username:password
. in the case of the streaming audio server, the username
is always default
and the password is the same password used in the websocket service.
for example, if the password is set to mypassword
, <credentials>
will be the base64 encoded value of default:mypassword
, which is ZGVmYXVsdDpteXBhc3N3b3Jk
. so the entire auth header will be:
Authorization: Basic ZGVmYXVsdDpteXBhc3N3b3Jk
if the auth header is not present, or the username or password are incorrect, the server will respond with an http 401 (unauthorized)
.
it's not uncommon for users to have very large collections of music -- in some cases 100,000+ tracks. therefore, certain optimizations exist in a subset of queries that can be used to limit and page through the amount of data returned. queries that may return large amounts of data all follow a common pattern and accept the same optional inputs that should be used to optimize performance.
the optimization is a two-step process, as follows:
-
run the query first with
"count_only" : true
specified as anoption
. the backend will run the query, but only return the number of results (but not the results themselves). clients should use this information to estimate the dimensions of their views. -
run the query again without
count_only
, and this time specify anoffset
and alimit
to only retrieve metadata for the information currently in view -- plus maybe a couple pages before and after. -
as the user continues to scroll through the list, request subsequent sets of data using
offset
andlimit
.
this section describes a few common resource types that are used across multiple messages.
{
"id": <int64>,
"external_id": "<external id>",
"title": "<title>",
"track_num": <int32>,
"album": "<album name>",
"album_id": <int64>,
"album_artist": "<album artist name>",
"album_artist_id": <int64>,
"artist": "<artist name>",
"artist_id": <int64>,
"genre": "<genre name>",
"genre_id": <int64>
}
{
"id": <int64>,
"title": "<album title>",
"album_artist": "<album artist name>",
"album_artist_id": <int64>
}
{
"id": <int64>,
"value": "<category (artist/genre/etc) value>",
}
{
"state": "<stopped | playing | paused>",
"repeat_mode": "<none | track | list>",
"volume": <0.0 to 1.0>,
"shuffled": <true | false>,
"muted": <true | false>,
"play_queue_count": 10, /* total */
"play_queue_position": 2, /* current */
"playing_duration": 300.0, /* seconds */
"playing_current_time": 10.0, /* seconds */
"playing_track": {
/* track resource */
}
}
broadcasted whenever the play queue has changed (new list enqueued, rearranged, etc)
{
"name": "play_queue_changed",
"type": "broadcast",
"id": "<unique_id>",
"options": { }
}
sent from the server to the client whenever the playback state changes.
{
"name": "get_playback_overview",
"type": "broadcast",
"id": "<unique_id>",
"options": {
/* playback overview resource */
}
}
none!
none!
note: many responses are a generic success/failure
, which have following format:
{
"name": "<name_from_request>",
"type: "response",
"id": "<id_from_request>",
"options" {
"success": <true | false>
}
}
returns a playback overview, suitable for updating transport controls on the client to match server playback state:
request:
{
"name": "get_playback_overview",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": { }
}
response:
{
"name": "get_playback_overview",
"type": "response",
"id": "<request_id>",
"options": {
/* playback overview resource */
}
}
remote controlling playback is easy. most messages follow the same basic request/response format, so they have been consolidated into this section.
request:
{
"name": "<pause_or_resume | stop | previous | next | toggle_shuffle | toggle_repeat | toggle_mute>",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": { }
}
response: generic success/failure response.
important: all of these messages will implicitly trigger a playback_overview_changed
broadcast from the server to the client immediately after internal state has been processed and updated.
set the playback volume to the specified value
request:
{
"name": "set_volume",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"volume": <0.0 to 1.0>
}
}
response: generic success/failure response.
set the volume relative to the current volume
{
"name": "set_volume",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"relative":" <up | down | -1.0 to 1.0>"
}
}
if relative
is up
, it will increase the volume by one unit (matching whatever the server does by default). if set to down
it will decrease by one unit. if a floating point number is specified it will be treated as a delta and applied to the current volume. negative deltas are allowed.
response: generic success/failure response.
seek to an absolute position in the current track
request:
{
"name": "seek_to",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"position": 10.0 /* in seconds */
}
}
response: generic success/failure response.
seek to a position relative to the current position (i.e. fast-forward or rewind)
request:
{
"name": "seek_relative",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"position": 10.0 /* in seconds. can be negative for rewind */
}
}
response: generic success/failure response.
play the track at the specified index in the current play queue
{
"name": "play_at_index",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"index": <int32>, /* optional */
"time": <double> /* in seconds. optional */
}
}
response: generic success/failure response.
replace the current play queue with all tracks (filtered by optional keywords), and start play back at the specified index (defaults to the first track)
{
"name": "play_all_tracks",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"index": <int32>, /* optional */
"time": <double>, /* in seconds. optional */
"filter": "<filter>" /* optional */
}
}
response: generic success/failure response.
replaces the current play queue with the specified array of tracks, and starts playback at the specified index (defaults to the first track)
{
"name": "play_tracks",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"ids": [ /* array of int64 ids */],
"index": <int32>, /* optional */
"time": <double> /* in seconds. optional */
}
}
response: generic success/failure response.
play all tracks for the specified category (e.g. all tracks by artist "foo" or all tracks with genre "bar")
{
"name": "play_tracks_by_category",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"category": "<album | artist | album_artist | genre | playlist>",
"id": <int64>, /* id for the selected category */
"filter": "<filter_string>" /* optional */
}
}
response: generic success/failure response.
query all tracks, optionally filtered by a string with an offset and a limit.
{
"name": "query_tracks",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"filter": "<filter string>", /* optional */
"count_only": <true | false>, /* optional; default false */
"limit": <integer>, /* optional */
"offset": <integer> /* optional */
}
}
response for count_only
query:
{
"name": "query_tracks",
"type": "response",
"id": "<request_id>",
"options": {
"data": [ ], /* empty array */
"count": <integer>
}
}
response for limit
and offset
query:
{
"name": "query_tracks",
"type": "response",
"id": "<request_id>",
"options": {
"count": <integer>,
"limit": <request_limit>,
"offset": <request_offset>,
"data": [
/* array of track resources */
]
}
}
note: see the query optimization
section for more information.
get metadata for all tracks with the specified external_ids
{
"name": "query_tracks_by_external_ids",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"external_ids": [<external_ids>]
}
}
response:
{
"name": "query_tracks_by_external_ids",
"type": "response",
"id": "<request_id>",
"options": {
"data": {
{ "<external_id>": { <track_resource> },
...
}
}
}
note: returned tracks will not be in order; rather, they will be in an object that maps the external_id to the track resource object.
query all tracks for the specified category (e.g. all tracks by artist "foo" or all tracks with genre "bar")
{
"name": "query_tracks_by_category",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"category": "<album | artist | album_artist | genre | playlist>"
"id": <int64>, /* id for the selected category */
"filter": "<filter string>", /* optional */
"count_only": <true | false>, /* optional; default false */
"limit": <integer>, /* optional */
"offset": <integer>, /* optional */
"predicates": [ /* optional */
{
"category": "<album | artist | album_artist | genre | etc>",
"id": <int64>
},
...
]
}
}
response for count_only
query:
{
"name": "query_tracks_by_category",
"type": "response",
"id": "<request_id>",
"options": {
"data": [ ], /* empty array */
"count": <integer>
}
}
response for limit
and offset
query:
{
"name": "query_tracks_by_category",
"type": "response",
"id": "<request_id>",
"device_id": "<unique_device_id>",
"options": {
"count": <integer>,
"limit": <request_limit>,
"offset": <request_offset>,
"data": [
/* array of track resources */
]
}
}
note 1: see the query optimization
section for more information.
note 2: the results of the query can be further filtered by a list of predicates that contain category type and corresponding category id. currently, all specified predicates will be joined via AND
. for example: you can get all albums
with genre=foo AND year=bar
. note that predicates are not supported for playlist requests.
used to retrieve a list of all metadata categories that may be used for subsequent queries
request:
{
"name": "list_categories",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": { /* none */ }
}
response:
{
"name": "list_categories",
"type": "response",
"id": "<request_id>",
"options": {
"data": ["artist", "album", ...]
}
}
retrieve a list of albums/artists/tracks/genres/playlists
note 1: this query does not currently support limit
and offset
, but likely will in the future.
note 2: the results of the query can be further filtered by a list of predicates that contain category type and corresponding category id. currently, all specified predicates will be joined via AND
. for example: you can get all albums
with genre=foo AND year=bar
. note that predicates are not supported for playlist requests.
request:
{
"name": "query_category",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"category": "<album | artist | album_artist | genre | playlist>",
"filter": "<filter string>", /* optional */
"predicates": [ /* optional */
{
"category": "<album | artist | album_artist | genre | etc>",
"id": <int64>
},
...
]
}
}
response:
{
"name": "query_category",
"type": "response",
"id": "<request_id>",
"options": {
"category": "<album | artist | album_artist | genre | playlist>"
"data": [ /* array of category value resources */ ]
}
}
very similar to query_category
, but returns album
resources with additional metadata. it can also be used to retrieve all albums for a specified artist
or genre
.
note: in the future this method will likely be removed/deprecated, and query_category
will just return album resources if the user asks for albums.
request:
{
"name": "query_albums",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"category": "<artist | album_artist | genre>", /* optional */
"category_id", <int64>, /* optional */
"filter": "<filter string>" /* optional */
}
}
response:
{
"name": "query_albums",
"type": "response",
"id": "<request_id>",
"options": {
"category": "album",
"data": [ /* array of album resources */ ]
}
}
query tracks from the current play queue.
the caller can request "live" data (what's currently in the play queue), or "snapshot" data, which was a snapshot of the play queue at some point in the past. snapshots can be taken using the snapshot_play_queue
message.
{
"name": "query_play_queue_tracks",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"count_only": <true | false>, /* optional; default false */
"type": "<live | snapshot>", /* optional; default "live" */
"limit": <integer>, /* optional */
"offset": <integer> /* optional */
}
}
response for count_only
query:
{
"name": "query_play_queue_tracks",
"type": "response",
"id": "<request_id>",
"options": {
"data": [ ], /* empty array */
"count": <integer>
}
}
response for limit
and offset
query:
{
"name": "query_play_queue_tracks",
"type": "response",
"id": "<request_id>",
"options": {
"count": <integer>,
"limit": <request_limit>,
"offset": <request_offset>,
"data": [
/* array of track resources */
]
}
}
{
"name": "rename_playlist",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"playlist_id": <int64>,
"playlist_name": "<new_playlist_name>"
}
}
response: generic success/failure response.
{
"name": "delete_playlist",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"playlist_id": <int64>
}
}
response: generic success/failure response.
playlists can be saved with either an list of track external_ids
, or by using a query_tracks_by_category
subquery (e.g. albums by "foo").
using external_ids:
{
"name": "save_playlist",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"playlist_id": <int64>, /* optional. if specified, overrides this
playlist. otherwise, a new playlist will be
created */
"playlist_name": "<new_playlist_name>",
"external_ids": [ <list_of_external_ids> ]
}
}
using a query_tracks_by_category
subquery:
{
"name": "save_playlist",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"playlist_id": <int64>, /* optional. if specified, overrides this
playlist. otherwise, a new playlist will be
created */
"playlist_name": "<new_playlist_name>",
"subquery": {
/* options from query_tracks_by_category. see documentation
for this request */
}
}
}
response:
{
"name": "save_playlist",
"type": "response",
"id": "<request_id>",
"options": {
"playlist_id": <int64> /* id of the playlist created/updated */
}
}
this query is used to add one or more tracks to the specified playlist, at the optionally specified offset. tracks can either be added explicitly with an array of external_ids
, or by using a query_tracks_by_category
, similar to save_playlist
:
using external_ids:
{
"name": "append_to_playlist",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"playlist_id": <int64>,
"offset": <integer>, /* optional */
"external_ids": [ <list_of_external_ids> ]
}
}
using a query_tracks_by_category
subquery:
{
"name": "append_to_playlist",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"playlist_id": <int64>,
"offset": "<integer>", /* optional */
"subquery": {
/* options from query_tracks_by_category. see documentation
for this request */
}
}
}
response: generic success/failure response.
use this to remove tracks from a playlist.
the input to this query is two arrays:
- a list of external ids
- the corresponding indices of these external ids in the playlist
this complexity exists because it's completely possible (and actually quite common) for the same song to exist in a playlist multiple times. if indices are not specified, it would not be possible for the server to figure out which to remove.
{
"name": "remove_tracks_from_playlist",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"playlist_id": <int64>
"external_ids": [ <list_of_external_ids> ],
"sort_orders": [ <list_of_indices> ] /* 0-based offsets */
}
}
response:
{
"name": "remove_tracks_from_playlist",
"type": "response",
"id": "<request_id>",
"options": {
"count": <integer> /* number of elements removed */
}
}
used to remotely start a rescan of the user's metadata. the caller may choose between reindex
(which will only scan files that have been updated since the last scan), or rebuild
, which will rescan all files regardless of update time.
request:
{
"name": "run_indexer",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"type": "<reindex | rebuild>"
}
}
response: generic success/failure response.
returns a list of available output drivers and their respective devices. the response also includes the currently selected driver and device.
request:
{
"name": "list_output_drivers",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": { /* none */ }
}
response:
{
"name": "list_output_drivers",
"type": "response",
"id": "<request_id>",
"options": {
"selected": {
"driver_name": "<string>",
"device_id": "<string>"
},
"all": [
{
"driver_name": "<string>",
"devices": [
{
"device_name": "<string>",
"device_id": "<string>"
},
...
],
...
},
...
]
}
}
used to set the playback system's default driver and device. this call will automatically re-route playback to the newly specified device, immediately.
request:
{
"name": "set_default_output_driver",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"driver_name": "<string>",
"device_id": "<string>" /* optional. */
}
}
response: generic success/failure response.
retrieves preamp and replaygain settings
request:
{
"name": "get_gain_settings",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": { /* none */ }
}
response:
{
"name": "get_gain_settings",
"type": "response",
"id": "<request_id>",
"options": {
"replaygain_mode": "<disabled | album | track>",
"preamp_gain": <float32 -20.0 to 20.0>
}
}
request:
{
"name": "update_gain_settings",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"replaygain_mode": "<disabled | album | track>",
"preamp_gain": <float32 -20.0 to 20.0>
}
}
response: generic success/failure response.
returns the currently selected transport type ("gapless" or "crossfade")
request:
{
"name": "get_transport_type",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": { /* none */ }
}
response:
{
"name": "get_transport_type",
"type": "response",
"id": "<request_id>",
"options": {
"type": "<gapless| crossfade>"
}
}
updates the currently selected transport type ("gapless" or "crossfade")
request:
{
"name": "set_transport_type",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"type": "<gapless| crossfade>"
}
}
response: generic success/failure response.
takes a snapshot of the current play queue, and associates it with the specified device_id
. callers may then call query_play_queue_tracks
and specify "snapshot"
in the type
field to query this data.
snapshots are considered stale if not accessed for 6 hours, and will be automatically purged by the server.
request:
{
"name": "snapshot_play_queue",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": { /* none */ }
}
response: generic success/failure response.
as described in the snapshot_play_queue
documentation, snapshots not accessed for 6 hours are considered invalid and purged automatically. however, a well-behaved client can send an invalidate_play_queue_snapshot
message as soon as it knows the snapshot is invalid, and the associated resources will be freed immediately.
request:
{
"name": "invalidate_play_queue_snapshot",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": { /* none */ }
}
response: generic success/failure response.
replaces the play queue with the snapshot for the specified device_id
, and starts playback at the specified index and time.
request:
{
"name": "play_snapshot_tracks ",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"index": <int32>, /* optional */
"time": <double> /* in seconds. optional */
}
}
response: generic success/failure response.
returns current equalizer settings
request:
{
"name": "get_equalizer_settings",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": { }
}
response:
note: band values range between -20.0 and 20.0 dB. the number of bands and their associated frequencies are not defined in the api layer, and are determined by the server.
{
"name": "get_equalizer_settings",
"type": "response",
"id": "<request_id>",
"options": {
"enabled": <true|false>,
"bands": [
{"<freq_hz>": <double>},
...
]
}
}
updates equalizer settings on the server.
note: band values range between -20.0 and 20.0 dB. the number of bands and their associated frequencies are not defined in the api layer, and are determined by the server; you should always call get_equalizer_settings
first to build the frequency table on the client.
request:
{
"name": "set_equalizer_settings",
"type": "request",
"id": "<unique_id>",
"device_id": "<unique_device_id>",
"options": {
"enabled": <true|false>, /* optional */
"bands": [ <double>, ... ] /* optional */
}
}
response:
response: generic success/failure response.
{
"name": "get_transport_type",
"type": "response",
"id": "<request_id>",
"options": {
"enabled": <true|false>,
"bands": [
{"<freq_hz>": <double>},
...
]
}
}
if you're writing a streaming audio client (not just a playback remote), you can request audio data from the server. there are two ways to request audio data:
- source audio data: this will stream the file as-is from your library. that is, a flac file will be sent as flac, an mp3 file will be sent as mp3, ogg as ogg, etc. no transformation or downsampling will be performed.
- downsampled audio data: source audio will be transcoded, on demand, to mp3 with the specified bitrate.
important: the first time an transcoded audio file is requested, it is downsampled in real-time, therefore cannot be seeked. that means that any requests with Range
headers will be rejected with http 416
. as soon as the initial transcode has completed, the result will be cached to disk, and subsequent requests can be seeked.
the format of the url is as follows:
http://host:port/audio/external_id/<external_id>?bitrate=xyz
note 1: the value for the track's external_id
can be obtained in the websocket metadata queries described above.
note 2: the transcoder will only run if the bitrate
parameter is present and valid. otherwise, no downsampling will be performed, and the requested file will be returned without transformation.
note 3: make sure you url encode the <external_id>
in the path! library plugins can format external ids however they wish, and may include characters that need to be encoded!
similar to the audio data requests, album artwork can be obtained using the following url:
http://host:port/thumbnail/<thumbnail_id>
the thumbnail_id
value can be found in both album and track resources.