mirror of
https://github.com/badaix/snapcast.git
synced 2025-05-01 11:17:36 +02:00
Make metadata part of the properties
This commit is contained in:
parent
5cebc64e15
commit
004ea21e3f
13 changed files with 134 additions and 153 deletions
|
@ -26,6 +26,8 @@
|
||||||
|
|
||||||
#include "common/aixlog.hpp"
|
#include "common/aixlog.hpp"
|
||||||
#include "common/json.hpp"
|
#include "common/json.hpp"
|
||||||
|
#include "common/metatags.hpp"
|
||||||
|
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
@ -152,6 +154,8 @@ public:
|
||||||
fromJson(j);
|
fromJson(j);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Meta data
|
||||||
|
std::optional<Metatags> metatags;
|
||||||
/// https://www.musicpd.org/doc/html/protocol.html#tags
|
/// https://www.musicpd.org/doc/html/protocol.html#tags
|
||||||
/// The current playback status
|
/// The current playback status
|
||||||
std::optional<PlaybackStatus> playback_status;
|
std::optional<PlaybackStatus> playback_status;
|
||||||
|
@ -202,6 +206,8 @@ public:
|
||||||
addTag(j, "canPause", can_pause);
|
addTag(j, "canPause", can_pause);
|
||||||
addTag(j, "canSeek", can_seek);
|
addTag(j, "canSeek", can_seek);
|
||||||
addTag(j, "canControl", can_control);
|
addTag(j, "canControl", can_control);
|
||||||
|
if (metatags.has_value())
|
||||||
|
addTag(j, "metadata", metatags->toJson());
|
||||||
return j;
|
return j;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,7 +215,7 @@ public:
|
||||||
{
|
{
|
||||||
static std::set<std::string> rw_props = {"loopStatus", "shuffle", "volume", "rate"};
|
static std::set<std::string> rw_props = {"loopStatus", "shuffle", "volume", "rate"};
|
||||||
static std::set<std::string> ro_props = {"playbackStatus", "loopStatus", "shuffle", "volume", "position", "minimumRate", "maximumRate",
|
static std::set<std::string> ro_props = {"playbackStatus", "loopStatus", "shuffle", "volume", "position", "minimumRate", "maximumRate",
|
||||||
"canGoNext", "canGoPrevious", "canPlay", "canPause", "canSeek", "canControl"};
|
"canGoNext", "canGoPrevious", "canPlay", "canPause", "canSeek", "canControl", "metadata"};
|
||||||
for (const auto& element : j.items())
|
for (const auto& element : j.items())
|
||||||
{
|
{
|
||||||
bool is_rw = (rw_props.find(element.key()) != rw_props.end());
|
bool is_rw = (rw_props.find(element.key()) != rw_props.end());
|
||||||
|
@ -243,12 +249,38 @@ public:
|
||||||
readTag(j, "canPause", can_pause, false);
|
readTag(j, "canPause", can_pause, false);
|
||||||
readTag(j, "canSeek", can_seek, false);
|
readTag(j, "canSeek", can_seek, false);
|
||||||
readTag(j, "canControl", can_control, false);
|
readTag(j, "canControl", can_control, false);
|
||||||
|
|
||||||
|
if (j.contains("metadata"))
|
||||||
|
{
|
||||||
|
Metatags m;
|
||||||
|
m.fromJson(j["metadata"]);
|
||||||
|
metatags = m;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
metatags = std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool operator==(const Properties& other) const
|
bool operator==(const Properties& other) const
|
||||||
{
|
{
|
||||||
// expensive, but not called ofetn and less typing
|
// expensive, but not called ofetn and less typing
|
||||||
return (toJson() == other.toJson());
|
return (toJson() == other.toJson());
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
// return (playback_status == other.playback_status &&
|
||||||
|
// loop_status == other.loop_status &&
|
||||||
|
// rate == other.rate &&
|
||||||
|
// shuffle == other.shuffle &&
|
||||||
|
// volume == other.volume &&
|
||||||
|
// position == other.position &&
|
||||||
|
// minimum_rate == other.minimum_rate &&
|
||||||
|
// maximum_rate == other.maximum_rate &&
|
||||||
|
// can_go_next == other.can_go_next &&
|
||||||
|
// can_go_previous == other.can_go_previous &&
|
||||||
|
// can_play == other.can_play &&
|
||||||
|
// can_pause == other.can_pause &&
|
||||||
|
// can_seek == other.can_seek &&
|
||||||
|
// can_control == other.can_control);
|
||||||
|
// clang-format on
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -376,7 +376,8 @@ class MPDWrapper(object):
|
||||||
success = True
|
success = True
|
||||||
command = request['params']['command']
|
command = request['params']['command']
|
||||||
params = request['params'].get('params', {})
|
params = request['params'].get('params', {})
|
||||||
logger.debug(f'Control command: {command}, params: {params}')
|
logger.debug(
|
||||||
|
f'Control command: {command}, params: {params}')
|
||||||
if command == 'Next':
|
if command == 'Next':
|
||||||
self.next()
|
self.next()
|
||||||
elif command == 'Previous':
|
elif command == 'Previous':
|
||||||
|
@ -495,8 +496,20 @@ class MPDWrapper(object):
|
||||||
def __track_key(self, snapmeta):
|
def __track_key(self, snapmeta):
|
||||||
return hash(snapmeta.get('artist', [''])[0] + snapmeta.get('album', snapmeta.get('title', '')))
|
return hash(snapmeta.get('artist', [''])[0] + snapmeta.get('album', snapmeta.get('title', '')))
|
||||||
|
|
||||||
def update_albumart(self, snapmeta):
|
def get_albumart(self, snapmeta, cached):
|
||||||
album_key = 'musicbrainzAlbumId'
|
album_key = 'musicbrainzAlbumId'
|
||||||
|
track_key = self.__track_key(snapmeta)
|
||||||
|
album_art = self._album_art_map.get(track_key)
|
||||||
|
if album_art is not None:
|
||||||
|
if album_art == '':
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return album_art
|
||||||
|
|
||||||
|
if cached:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self._album_art_map[track_key] = ''
|
||||||
try:
|
try:
|
||||||
if not album_key in snapmeta:
|
if not album_key in snapmeta:
|
||||||
mbartist = None
|
mbartist = None
|
||||||
|
@ -521,22 +534,21 @@ class MPDWrapper(object):
|
||||||
data = musicbrainzngs.get_image_list(snapmeta[album_key])
|
data = musicbrainzngs.get_image_list(snapmeta[album_key])
|
||||||
for image in data["images"]:
|
for image in data["images"]:
|
||||||
if "Front" in image["types"] and image["approved"]:
|
if "Front" in image["types"] and image["approved"]:
|
||||||
snapmeta['artUrl'] = image["thumbnails"]["small"]
|
album_art = image["thumbnails"]["small"]
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'{snapmeta["artUrl"]} is an approved front image')
|
f'{album_art} is an approved front image')
|
||||||
logger.info(f'Snapmeta: {snapmeta}')
|
self._album_art_map[track_key] = album_art
|
||||||
self._album_art_map[self.__track_key(
|
|
||||||
snapmeta)] = snapmeta['artUrl']
|
|
||||||
send(
|
|
||||||
{"jsonrpc": "2.0", "method": "Plugin.Stream.Player.Metadata", "params": snapmeta})
|
|
||||||
break
|
break
|
||||||
|
|
||||||
except musicbrainzngs.musicbrainz.ResponseError as e:
|
except musicbrainzngs.musicbrainz.ResponseError as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f'Error while getting cover for {snapmeta[album_key]}: {e}')
|
f'Error while getting cover for {snapmeta[album_key]}: {e}')
|
||||||
self._album_art_map[self.__track_key(snapmeta)] = ''
|
album_art = self._album_art_map[track_key]
|
||||||
|
if album_art == '':
|
||||||
|
return None
|
||||||
|
return album_art
|
||||||
|
|
||||||
def update_metadata(self):
|
def get_metadata(self):
|
||||||
"""
|
"""
|
||||||
Translate metadata returned by MPD to the MPRIS v2 syntax.
|
Translate metadata returned by MPD to the MPRIS v2 syntax.
|
||||||
http://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata
|
http://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata
|
||||||
|
@ -586,19 +598,12 @@ class MPDWrapper(object):
|
||||||
snapmeta['artist'] = [fields[0]]
|
snapmeta['artist'] = [fields[0]]
|
||||||
snapmeta['title'] = fields[1]
|
snapmeta['title'] = fields[1]
|
||||||
|
|
||||||
track_key = self.__track_key(snapmeta)
|
art_url = self.get_albumart(snapmeta, True)
|
||||||
if track_key in self._album_art_map:
|
if art_url is not None:
|
||||||
art_url = self._album_art_map[track_key]
|
|
||||||
logger.info(f'album art cache hit: "{art_url}"')
|
logger.info(f'album art cache hit: "{art_url}"')
|
||||||
if art_url != '':
|
|
||||||
snapmeta['artUrl'] = art_url
|
snapmeta['artUrl'] = art_url
|
||||||
|
|
||||||
send({"jsonrpc": "2.0",
|
return snapmeta
|
||||||
"method": "Plugin.Stream.Player.Metadata", "params": snapmeta})
|
|
||||||
if not track_key in self._album_art_map:
|
|
||||||
# t = threading.Thread(target=self.update_albumart, args=[snapmeta])
|
|
||||||
# t.start()
|
|
||||||
self.update_albumart(snapmeta)
|
|
||||||
|
|
||||||
def __diff_map(self, old_map, new_map):
|
def __diff_map(self, old_map, new_map):
|
||||||
diff = {}
|
diff = {}
|
||||||
|
@ -662,14 +667,11 @@ class MPDWrapper(object):
|
||||||
logger.debug('nothing to do')
|
logger.debug('nothing to do')
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info(f'new status: {new_status}')
|
logger.info(
|
||||||
logger.info(f'changed_status: {changed_status}')
|
f'new status: {new_status}, changed_status: {changed_status}, changed_song: {changed_song}')
|
||||||
logger.info(f'changed_song: {changed_song}')
|
|
||||||
self._time = new_time = int(time.time())
|
self._time = new_time = int(time.time())
|
||||||
|
|
||||||
snapstatus = self._get_properties(new_status)
|
snapstatus = self._get_properties(new_status)
|
||||||
send({"jsonrpc": "2.0", "method": "Plugin.Stream.Player.Properties",
|
|
||||||
"params": snapstatus})
|
|
||||||
|
|
||||||
if 'elapsed' in new_status:
|
if 'elapsed' in new_status:
|
||||||
new_position = float(new_status['elapsed'])
|
new_position = float(new_status['elapsed'])
|
||||||
|
@ -682,9 +684,9 @@ class MPDWrapper(object):
|
||||||
|
|
||||||
# "player" subsystem
|
# "player" subsystem
|
||||||
|
|
||||||
force = len(changed_song) > 0
|
new_song = len(changed_song) > 0
|
||||||
|
|
||||||
if not force:
|
if not new_song:
|
||||||
if new_status['state'] == 'play':
|
if new_status['state'] == 'play':
|
||||||
expected_position = old_position + (new_time - old_time)
|
expected_position = old_position + (new_time - old_time)
|
||||||
else:
|
else:
|
||||||
|
@ -698,7 +700,18 @@ class MPDWrapper(object):
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Update current song metadata
|
# Update current song metadata
|
||||||
self.update_metadata()
|
snapstatus["metadata"] = self.get_metadata()
|
||||||
|
|
||||||
|
send({"jsonrpc": "2.0", "method": "Plugin.Stream.Player.Properties",
|
||||||
|
"params": snapstatus})
|
||||||
|
|
||||||
|
if new_song:
|
||||||
|
if 'artUrl' not in snapstatus['metadata']:
|
||||||
|
album_art = self.get_albumart(snapstatus['metadata'], False)
|
||||||
|
if album_art is not None:
|
||||||
|
snapstatus['metadata']['artUrl'] = album_art
|
||||||
|
send(
|
||||||
|
{"jsonrpc": "2.0", "method": "Plugin.Stream.Player.Properties", "params": snapstatus})
|
||||||
|
|
||||||
# Compatibility functions
|
# Compatibility functions
|
||||||
|
|
||||||
|
|
|
@ -331,12 +331,12 @@ class SnapcastWrapper(object):
|
||||||
if not 'mpris:artUrl' in self._metadata:
|
if not 'mpris:artUrl' in self._metadata:
|
||||||
self._metadata['mpris:artUrl'] = f'http://{self._params["host"]}:{self._params["port"]}/snapcast-512.png'
|
self._metadata['mpris:artUrl'] = f'http://{self._params["host"]}:{self._params["port"]}/snapcast-512.png'
|
||||||
|
|
||||||
logger.info(f'mpris meta: {self._metadata}')
|
logger.debug(f'mpris meta: {self._metadata}')
|
||||||
|
|
||||||
self.notify_about_track(self._metadata)
|
self.notify_about_track(self._metadata)
|
||||||
new_meta = self._dbus_service.update_property('org.mpris.MediaPlayer2.Player',
|
new_meta = self._dbus_service.update_property('org.mpris.MediaPlayer2.Player',
|
||||||
'Metadata')
|
'Metadata')
|
||||||
logger.info(f'new meta {new_meta}')
|
logger.debug(f'new meta {new_meta}')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f'Error in update_metadata: {str(e)}')
|
logger.error(f'Error in update_metadata: {str(e)}')
|
||||||
|
|
||||||
|
@ -344,11 +344,10 @@ class SnapcastWrapper(object):
|
||||||
try:
|
try:
|
||||||
if props is None:
|
if props is None:
|
||||||
props = {}
|
props = {}
|
||||||
logger.info(f'Properties: "{props}"')
|
logger.debug(f'Properties: "{props}"')
|
||||||
# store the last receive time stamp for better position estimation
|
# store the last receive time stamp for better position estimation
|
||||||
if 'position' in props:
|
if 'position' in props:
|
||||||
props['_received'] = time.time()
|
props['_received'] = time.time()
|
||||||
|
|
||||||
# ignore "internal" properties, starting with "_"
|
# ignore "internal" properties, starting with "_"
|
||||||
changed_properties = {}
|
changed_properties = {}
|
||||||
for key, value in props.items():
|
for key, value in props.items():
|
||||||
|
@ -369,6 +368,8 @@ class SnapcastWrapper(object):
|
||||||
if key in property_mapping:
|
if key in property_mapping:
|
||||||
self._dbus_service.update_property(
|
self._dbus_service.update_property(
|
||||||
'org.mpris.MediaPlayer2.Player', property_mapping[key])
|
'org.mpris.MediaPlayer2.Player', property_mapping[key])
|
||||||
|
if 'metadata' in changed_properties:
|
||||||
|
self.__update_metadata(props.get('metadata', None))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f'Error in update_properties: {str(e)}')
|
logger.error(f'Error in update_properties: {str(e)}')
|
||||||
|
|
||||||
|
@ -388,8 +389,6 @@ class SnapcastWrapper(object):
|
||||||
logger.info(f'Stream id: {self._stream_id}')
|
logger.info(f'Stream id: {self._stream_id}')
|
||||||
for stream in jmsg['result']['server']['streams']:
|
for stream in jmsg['result']['server']['streams']:
|
||||||
if stream['id'] == self._stream_id:
|
if stream['id'] == self._stream_id:
|
||||||
if 'metadata' in stream:
|
|
||||||
self.__update_metadata(stream['metadata'])
|
|
||||||
if 'properties' in stream:
|
if 'properties' in stream:
|
||||||
self.__update_properties(stream['properties'])
|
self.__update_properties(stream['properties'])
|
||||||
break
|
break
|
||||||
|
@ -399,14 +398,6 @@ class SnapcastWrapper(object):
|
||||||
logger.info(f'Stream id: {self._stream_id}')
|
logger.info(f'Stream id: {self._stream_id}')
|
||||||
elif jmsg['method'] == "Group.OnStreamChanged":
|
elif jmsg['method'] == "Group.OnStreamChanged":
|
||||||
self.send_request("Server.GetStatus")
|
self.send_request("Server.GetStatus")
|
||||||
elif jmsg["method"] == "Stream.OnMetadata":
|
|
||||||
stream_id = jmsg["params"]["id"]
|
|
||||||
logger.info(f'Stream meta changed for "{stream_id}"')
|
|
||||||
if self._stream_id != stream_id:
|
|
||||||
return
|
|
||||||
meta = jmsg["params"]["metadata"]
|
|
||||||
self.__update_metadata(meta)
|
|
||||||
|
|
||||||
elif jmsg["method"] == "Stream.OnProperties":
|
elif jmsg["method"] == "Stream.OnProperties":
|
||||||
stream_id = jmsg["params"]["id"]
|
stream_id = jmsg["params"]["id"]
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
|
@ -4,11 +4,11 @@
|
||||||
A stream plugin is (this might change in future) an executable binary or script that is started by the server for a specific stream and offers playback control capabilities and provides information about the stream's state, as well as metadata for the currently playing track.
|
A stream plugin is (this might change in future) an executable binary or script that is started by the server for a specific stream and offers playback control capabilities and provides information about the stream's state, as well as metadata for the currently playing track.
|
||||||
The Snapcast server communicates via stdin/stdout with the plugin and sends newline delimited JSON-RPC commands and receives responses and notifications from the plugin, as described below. In upcoming releases shared library plugins might be supported as well.
|
The Snapcast server communicates via stdin/stdout with the plugin and sends newline delimited JSON-RPC commands and receives responses and notifications from the plugin, as described below. In upcoming releases shared library plugins might be supported as well.
|
||||||
|
|
||||||
## Requests:
|
## Requests
|
||||||
|
|
||||||
A Stream plugin must support and handle the following requests, sent by the Snapcast server
|
A Stream plugin must support and handle the following requests, sent by the Snapcast server
|
||||||
|
|
||||||
### Plugin.Stream.Player.Control:
|
### Plugin.Stream.Player.Control
|
||||||
|
|
||||||
Used to control the player
|
Used to control the player
|
||||||
|
|
||||||
|
@ -38,14 +38,14 @@ Supported `command`s:
|
||||||
- `Position`: [float] the new track position
|
- `Position`: [float] the new track position
|
||||||
- `TrackId`: [string] the optional currently playing track's identifier
|
- `TrackId`: [string] the optional currently playing track's identifier
|
||||||
|
|
||||||
#### Example:
|
#### Example
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"id": 1, "jsonrpc": "2.0", "method": "Plugin.Stream.Player.Control", "params": {"command": "SetPosition", "params": { "Position": 170966827, "TrackId": "/org/mpris/MediaPlayer2/Track/2"}}}
|
{"id": 1, "jsonrpc": "2.0", "method": "Plugin.Stream.Player.Control", "params": {"command": "SetPosition", "params": { "Position": 170966827, "TrackId": "/org/mpris/MediaPlayer2/Track/2"}}}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
#### Expected response:
|
#### Expected response
|
||||||
|
|
||||||
Success:
|
Success:
|
||||||
|
|
||||||
|
@ -59,13 +59,13 @@ Error:
|
||||||
todo
|
todo
|
||||||
```
|
```
|
||||||
|
|
||||||
### Plugin.Stream.Player.SetProperty:
|
### Plugin.Stream.Player.SetProperty
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"id": 1, "jsonrpc": "2.0", "method": "Plugin.Stream.Player.SetProperty", "params": {"<property>", <value>}}
|
{"id": 1, "jsonrpc": "2.0", "method": "Plugin.Stream.Player.SetProperty", "params": {"<property>", <value>}}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Supported `property`s:
|
#### Supported `property`s
|
||||||
|
|
||||||
- `loopStatus`: [string] the current repeat status, one of:
|
- `loopStatus`: [string] the current repeat status, one of:
|
||||||
- `none`: the playback will stop when there are no more tracks to play
|
- `none`: the playback will stop when there are no more tracks to play
|
||||||
|
@ -75,7 +75,7 @@ todo
|
||||||
- `volume`: [int] voume in percent, valid range [0..100]
|
- `volume`: [int] voume in percent, valid range [0..100]
|
||||||
- `rate`: [float] The current playback rate, valid range (0..)
|
- `rate`: [float] The current playback rate, valid range (0..)
|
||||||
|
|
||||||
#### Expected response:
|
#### Expected response
|
||||||
|
|
||||||
Success:
|
Success:
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ todo
|
||||||
{"id": 1, "jsonrpc": "2.0", "method": "Plugin.Stream.Player.GetProperties"}
|
{"id": 1, "jsonrpc": "2.0", "method": "Plugin.Stream.Player.GetProperties"}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Expected response:
|
#### Expected response
|
||||||
|
|
||||||
Success:
|
Success:
|
||||||
|
|
||||||
|
@ -132,11 +132,13 @@ todo
|
||||||
|
|
||||||
### Plugin.Stream.Player.GetMetadata
|
### Plugin.Stream.Player.GetMetadata
|
||||||
|
|
||||||
|
TODO: Metadata are part of the properties
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"id": 1, "jsonrpc": "2.0", "method": "Plugin.Stream.Player.GetMetadata"}
|
{"id": 1, "jsonrpc": "2.0", "method": "Plugin.Stream.Player.GetMetadata"}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Expected response:
|
#### Expected response
|
||||||
|
|
||||||
Success:
|
Success:
|
||||||
|
|
||||||
|
@ -144,10 +146,12 @@ Success:
|
||||||
{"id": 1, "jsonrpc": "2.0", "result": {"artist":["Travis Scott & HVME"],"file":"http://wdr-1live-live.icecast.wdr.de/wdr/1live/live/mp3/128/stream.mp3","name":"1Live, Westdeutscher Rundfunk Koeln","title":"Goosebumps (Remix)","trackId":"3"}}
|
{"id": 1, "jsonrpc": "2.0", "result": {"artist":["Travis Scott & HVME"],"file":"http://wdr-1live-live.icecast.wdr.de/wdr/1live/live/mp3/128/stream.mp3","name":"1Live, Westdeutscher Rundfunk Koeln","title":"Goosebumps (Remix)","trackId":"3"}}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notifications:
|
## Notifications
|
||||||
|
|
||||||
### Plugin.Stream.Player.Metadata
|
### Plugin.Stream.Player.Metadata
|
||||||
|
|
||||||
|
TODO: Metadata are part of the properties
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"jsonrpc": "2.0", "method": "Plugin.Stream.Player.Metadata", "params": {"artist":["Travis Scott & HVME"],"file":"http://wdr-1live-live.icecast.wdr.de/wdr/1live/live/mp3/128/stream.mp3","name":"1Live, Westdeutscher Rundfunk Koeln","title":"Goosebumps (Remix)","trackId":"3"}}
|
{"jsonrpc": "2.0", "method": "Plugin.Stream.Player.Metadata", "params": {"artist":["Travis Scott & HVME"],"file":"http://wdr-1live-live.icecast.wdr.de/wdr/1live/live/mp3/128/stream.mp3","name":"1Live, Westdeutscher Rundfunk Koeln","title":"Goosebumps (Remix)","trackId":"3"}}
|
||||||
```
|
```
|
||||||
|
@ -199,7 +203,6 @@ Success:
|
||||||
- `spotifyArtistId`: [string] The [Spotify Artist ID](https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids)
|
- `spotifyArtistId`: [string] The [Spotify Artist ID](https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids)
|
||||||
- `spotifyTrackId`: [string] The [Spotify Track ID](https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids)
|
- `spotifyTrackId`: [string] The [Spotify Track ID](https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids)
|
||||||
|
|
||||||
|
|
||||||
### Plugin.Stream.Player.Properties
|
### Plugin.Stream.Player.Properties
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
@ -234,12 +237,11 @@ The plugin shall send this notification when it's up and ready to receive comman
|
||||||
{"jsonrpc": "2.0", "method": "Plugin.Stream.Ready"}
|
{"jsonrpc": "2.0", "method": "Plugin.Stream.Ready"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Server
|
||||||
# Server:
|
|
||||||
|
|
||||||
TODO: this belongs to doc/json_rpc_api/v2_0_0.md
|
TODO: this belongs to doc/json_rpc_api/v2_0_0.md
|
||||||
|
|
||||||
## Requests:
|
## Requests
|
||||||
|
|
||||||
To control the stream state, the following commands can be sent to the Snapcast server and will be forwarded to the respective stream:
|
To control the stream state, the following commands can be sent to the Snapcast server and will be forwarded to the respective stream:
|
||||||
|
|
||||||
|
@ -251,7 +253,7 @@ To control the stream state, the following commands can be sent to the Snapcast
|
||||||
{"id": 1, "jsonrpc": "2.0", "method": "Stream.SetProperty", "params": {"id": "Pipe", "property": property, "value": value}}
|
{"id": 1, "jsonrpc": "2.0", "method": "Stream.SetProperty", "params": {"id": "Pipe", "property": property, "value": value}}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notifications:
|
## Notifications
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"jsonrpc": "2.0", "method": "Stream.OnMetadata", "params": {"id": "Pipe", "metadata": {}}}
|
{"jsonrpc": "2.0", "method": "Stream.OnMetadata", "params": {"id": "Pipe", "metadata": {}}}
|
||||||
|
|
|
@ -49,23 +49,6 @@ void Server::onNewSession(std::shared_ptr<StreamSession> session)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void Server::onMetadataChanged(const PcmStream* pcmStream, const Metatags& metadata)
|
|
||||||
{
|
|
||||||
// clang-format off
|
|
||||||
// Notification: {"jsonrpc":"2.0","method":"Stream.OnMetadata","params":{"id":"stream 1", "metadata": {"album": "some album", "artist": "some artist", "track": "some track"...}}}
|
|
||||||
// clang-format on
|
|
||||||
|
|
||||||
LOG(DEBUG, LOG_TAG) << "Metadata changed, stream: " << pcmStream->getName() << ", meta: " << metadata.toJson().dump(3) << "\n";
|
|
||||||
|
|
||||||
// streamServer_->onMetadataChanged(pcmStream, meta);
|
|
||||||
|
|
||||||
// Send meta to all connected clients
|
|
||||||
json notification = jsonrpcpp::Notification("Stream.OnMetadata", jsonrpcpp::Parameter("id", pcmStream->getId(), "metadata", metadata.toJson())).to_json();
|
|
||||||
controlServer_->send(notification.dump(), nullptr);
|
|
||||||
// cout << "Notification: " << notification.dump() << "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void Server::onPropertiesChanged(const PcmStream* pcmStream, const Properties& properties)
|
void Server::onPropertiesChanged(const PcmStream* pcmStream, const Properties& properties)
|
||||||
{
|
{
|
||||||
LOG(DEBUG, LOG_TAG) << "Properties changed, stream: " << pcmStream->getName() << ", properties: " << properties.toJson().dump(3) << "\n";
|
LOG(DEBUG, LOG_TAG) << "Properties changed, stream: " << pcmStream->getName() << ", properties: " << properties.toJson().dump(3) << "\n";
|
||||||
|
|
|
@ -78,7 +78,6 @@ private:
|
||||||
void onNewSession(std::shared_ptr<StreamSession> session) override;
|
void onNewSession(std::shared_ptr<StreamSession> session) override;
|
||||||
|
|
||||||
/// Implementation of PcmListener
|
/// Implementation of PcmListener
|
||||||
void onMetadataChanged(const PcmStream* pcmStream, const Metatags& metadata) override;
|
|
||||||
void onPropertiesChanged(const PcmStream* pcmStream, const Properties& properties) override;
|
void onPropertiesChanged(const PcmStream* pcmStream, const Properties& properties) override;
|
||||||
void onStateChanged(const PcmStream* pcmStream, ReaderState state) override;
|
void onStateChanged(const PcmStream* pcmStream, ReaderState state) override;
|
||||||
void onChunkRead(const PcmStream* pcmStream, const msg::PcmChunk& chunk) override;
|
void onChunkRead(const PcmStream* pcmStream, const msg::PcmChunk& chunk) override;
|
||||||
|
|
|
@ -183,7 +183,7 @@ void AirplayStream::push()
|
||||||
// mden = metadata end, pcen == picture end
|
// mden = metadata end, pcen == picture end
|
||||||
if (metadata_dirty_ && entry_->type == "ssnc" && (entry_->code == "mden" || entry_->code == "pcen"))
|
if (metadata_dirty_ && entry_->type == "ssnc" && (entry_->code == "mden" || entry_->code == "pcen"))
|
||||||
{
|
{
|
||||||
setMetadata(metadata_);
|
// setMetadata(metadata_);
|
||||||
metadata_dirty_ = false;
|
metadata_dirty_ = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -161,7 +161,7 @@ void LibrespotStream::onStderrMsg(const std::string& line)
|
||||||
Metatags meta;
|
Metatags meta;
|
||||||
meta.artist = std::vector<std::string>{j["ARTIST"].get<std::string>()};
|
meta.artist = std::vector<std::string>{j["ARTIST"].get<std::string>()};
|
||||||
meta.title = j["TITLE"].get<std::string>();
|
meta.title = j["TITLE"].get<std::string>();
|
||||||
setMetadata(meta);
|
// TODO setMetadata(meta);
|
||||||
}
|
}
|
||||||
else if (regex_search(line, m, re_track_loaded))
|
else if (regex_search(line, m, re_track_loaded))
|
||||||
{
|
{
|
||||||
|
@ -169,7 +169,7 @@ void LibrespotStream::onStderrMsg(const std::string& line)
|
||||||
Metatags meta;
|
Metatags meta;
|
||||||
meta.title = string(m[1]);
|
meta.title = string(m[1]);
|
||||||
meta.duration = cpt::stod(m[2]) / 1000.;
|
meta.duration = cpt::stod(m[2]) / 1000.;
|
||||||
setMetadata(meta);
|
// TODO setMetadata(meta);
|
||||||
Properties properties;
|
Properties properties;
|
||||||
// properties.can_seek = true;
|
// properties.can_seek = true;
|
||||||
// properties.can_control = true;
|
// properties.can_control = true;
|
||||||
|
|
|
@ -84,16 +84,6 @@ void MetaStream::stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void MetaStream::onMetadataChanged(const PcmStream* pcmStream, const Metatags& metadata)
|
|
||||||
{
|
|
||||||
LOG(DEBUG, LOG_TAG) << "onMetadataChanged: " << pcmStream->getName() << "\n";
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
||||||
if (pcmStream != active_stream_.get())
|
|
||||||
return;
|
|
||||||
setMetadata(metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void MetaStream::onPropertiesChanged(const PcmStream* pcmStream, const Properties& properties)
|
void MetaStream::onPropertiesChanged(const PcmStream* pcmStream, const Properties& properties)
|
||||||
{
|
{
|
||||||
LOG(DEBUG, LOG_TAG) << "onPropertiesChanged: " << pcmStream->getName() << "\n";
|
LOG(DEBUG, LOG_TAG) << "onPropertiesChanged: " << pcmStream->getName() << "\n";
|
||||||
|
@ -118,7 +108,6 @@ void MetaStream::onStateChanged(const PcmStream* pcmStream, ReaderState state)
|
||||||
LOG(INFO, LOG_TAG) << "Stream: " << name_ << ", switching active stream: " << (active_stream_ ? active_stream_->getName() : "<null>") << " => "
|
LOG(INFO, LOG_TAG) << "Stream: " << name_ << ", switching active stream: " << (active_stream_ ? active_stream_->getName() : "<null>") << " => "
|
||||||
<< new_stream->getName() << "\n";
|
<< new_stream->getName() << "\n";
|
||||||
active_stream_ = new_stream;
|
active_stream_ = new_stream;
|
||||||
setMetadata(active_stream_->getMetadata());
|
|
||||||
setProperties(active_stream_->getProperties());
|
setProperties(active_stream_->getProperties());
|
||||||
resampler_ = make_unique<Resampler>(active_stream_->getSampleFormat(), sampleFormat_);
|
resampler_ = make_unique<Resampler>(active_stream_->getSampleFormat(), sampleFormat_);
|
||||||
};
|
};
|
||||||
|
|
|
@ -62,7 +62,6 @@ public:
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/// Implementation of PcmListener
|
/// Implementation of PcmListener
|
||||||
void onMetadataChanged(const PcmStream* pcmStream, const Metatags& metadata) override;
|
|
||||||
void onPropertiesChanged(const PcmStream* pcmStream, const Properties& properties) override;
|
void onPropertiesChanged(const PcmStream* pcmStream, const Properties& properties) override;
|
||||||
void onStateChanged(const PcmStream* pcmStream, ReaderState state) override;
|
void onStateChanged(const PcmStream* pcmStream, ReaderState state) override;
|
||||||
void onChunkRead(const PcmStream* pcmStream, const msg::PcmChunk& chunk) override;
|
void onChunkRead(const PcmStream* pcmStream, const msg::PcmChunk& chunk) override;
|
||||||
|
|
|
@ -137,12 +137,7 @@ void PcmStream::onControlNotification(const jsonrpcpp::Notification& notificatio
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
LOG(INFO, LOG_TAG) << "Notification method: " << notification.method() << ", params: " << notification.params().to_json() << "\n";
|
LOG(INFO, LOG_TAG) << "Notification method: " << notification.method() << ", params: " << notification.params().to_json() << "\n";
|
||||||
if (notification.method() == "Plugin.Stream.Player.Metadata")
|
if (notification.method() == "Plugin.Stream.Player.Properties")
|
||||||
{
|
|
||||||
LOG(DEBUG, LOG_TAG) << "Received metadata notification\n";
|
|
||||||
setMetadata(notification.params().to_json());
|
|
||||||
}
|
|
||||||
else if (notification.method() == "Plugin.Stream.Player.Properties")
|
|
||||||
{
|
{
|
||||||
LOG(DEBUG, LOG_TAG) << "Received properties notification\n";
|
LOG(DEBUG, LOG_TAG) << "Received properties notification\n";
|
||||||
setProperties(notification.params().to_json());
|
setProperties(notification.params().to_json());
|
||||||
|
@ -155,15 +150,10 @@ void PcmStream::onControlNotification(const jsonrpcpp::Notification& notificatio
|
||||||
if (response.error().code() == 0)
|
if (response.error().code() == 0)
|
||||||
setProperties(response.result());
|
setProperties(response.result());
|
||||||
});
|
});
|
||||||
stream_ctrl_->command({++req_id_, "Plugin.Stream.Player.GetMetadata"}, [this](const jsonrpcpp::Response& response) {
|
|
||||||
LOG(INFO, LOG_TAG) << "Response for Plugin.Stream.Player.GetMetadata: " << response.to_json() << "\n";
|
|
||||||
if (response.error().code() == 0)
|
|
||||||
setMetadata(response.result());
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Add capabilities or settings?
|
// TODO: Add capabilities or settings?
|
||||||
// {"jsonrpc": "2.0", "method": "Plugin.Stream.Ready", "params": {"pollProperties": 10, "responseTimeout": 5}}
|
// {"jsonrpc": "2.0", "method": "Plugin.Stream.Ready", "params": {"pollProperties": 10, "responseTimeout": 5}}
|
||||||
pollProperties();
|
// pollProperties();
|
||||||
}
|
}
|
||||||
else if (notification.method() == "Plugin.Stream.Log")
|
else if (notification.method() == "Plugin.Stream.Log")
|
||||||
{
|
{
|
||||||
|
@ -452,44 +442,31 @@ void PcmStream::play(ResultHandler handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void PcmStream::setMetadata(const Metatags& metadata)
|
|
||||||
{
|
|
||||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
||||||
if (metadata == metadata_)
|
|
||||||
{
|
|
||||||
LOG(DEBUG, LOG_TAG) << "setMetadata: Metadata did not change\n";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata_ = metadata;
|
|
||||||
LOG(INFO, LOG_TAG) << "setMetadata, stream: " << getId() << ", metadata: " << metadata_.toJson() << "\n";
|
|
||||||
|
|
||||||
// Trigger a stream update
|
|
||||||
for (auto* listener : pcmListeners_)
|
|
||||||
{
|
|
||||||
if (listener != nullptr)
|
|
||||||
listener->onMetadataChanged(this, metadata_);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void PcmStream::setProperties(const Properties& properties)
|
void PcmStream::setProperties(const Properties& properties)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
||||||
if (properties == properties_)
|
|
||||||
|
Properties props = properties;
|
||||||
|
// Missing metadata means, they didn't change, so
|
||||||
|
// enrich the new properites with old metadata
|
||||||
|
if (!props.metatags.has_value() && properties_.metatags.has_value())
|
||||||
|
props.metatags = properties_.metatags;
|
||||||
|
|
||||||
|
if (props == properties_)
|
||||||
{
|
{
|
||||||
LOG(DEBUG, LOG_TAG) << "setProperties: Properties did not change\n";
|
LOG(DEBUG, LOG_TAG) << "setProperties: Properties did not change\n";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
properties_ = properties;
|
properties_ = std::move(props);
|
||||||
|
|
||||||
LOG(INFO, LOG_TAG) << "setProperties, stream: " << getId() << ", properties: " << properties_.toJson() << "\n";
|
LOG(INFO, LOG_TAG) << "setProperties, stream: " << getId() << ", properties: " << properties_.toJson() << "\n";
|
||||||
|
|
||||||
// Trigger a stream update
|
// Trigger a stream update
|
||||||
for (auto* listener : pcmListeners_)
|
for (auto* listener : pcmListeners_)
|
||||||
{
|
{
|
||||||
if (listener != nullptr)
|
if (listener != nullptr)
|
||||||
listener->onPropertiesChanged(this, properties);
|
listener->onPropertiesChanged(this, properties_);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
|
|
||||||
#include "common/error_code.hpp"
|
#include "common/error_code.hpp"
|
||||||
#include "common/json.hpp"
|
#include "common/json.hpp"
|
||||||
#include "common/metatags.hpp"
|
|
||||||
#include "common/properties.hpp"
|
#include "common/properties.hpp"
|
||||||
#include "common/sample_format.hpp"
|
#include "common/sample_format.hpp"
|
||||||
#include "encoder/encoder.hpp"
|
#include "encoder/encoder.hpp"
|
||||||
|
@ -98,7 +97,6 @@ static constexpr auto kControlScript = "controlscript";
|
||||||
class PcmListener
|
class PcmListener
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
virtual void onMetadataChanged(const PcmStream* pcmStream, const Metatags& metadata) = 0;
|
|
||||||
virtual void onPropertiesChanged(const PcmStream* pcmStream, const Properties& properties) = 0;
|
virtual void onPropertiesChanged(const PcmStream* pcmStream, const Properties& properties) = 0;
|
||||||
virtual void onStateChanged(const PcmStream* pcmStream, ReaderState state) = 0;
|
virtual void onStateChanged(const PcmStream* pcmStream, ReaderState state) = 0;
|
||||||
virtual void onChunkRead(const PcmStream* pcmStream, const msg::PcmChunk& chunk) = 0;
|
virtual void onChunkRead(const PcmStream* pcmStream, const msg::PcmChunk& chunk) = 0;
|
||||||
|
@ -166,7 +164,6 @@ protected:
|
||||||
void resync(const std::chrono::nanoseconds& duration);
|
void resync(const std::chrono::nanoseconds& duration);
|
||||||
void chunkEncoded(const encoder::Encoder& encoder, std::shared_ptr<msg::PcmChunk> chunk, double duration);
|
void chunkEncoded(const encoder::Encoder& encoder, std::shared_ptr<msg::PcmChunk> chunk, double duration);
|
||||||
|
|
||||||
void setMetadata(const Metatags& metadata);
|
|
||||||
void setProperties(const Properties& properties);
|
void setProperties(const Properties& properties);
|
||||||
|
|
||||||
void pollProperties();
|
void pollProperties();
|
||||||
|
|
|
@ -22,7 +22,6 @@
|
||||||
#include <regex>
|
#include <regex>
|
||||||
|
|
||||||
#include "common/aixlog.hpp"
|
#include "common/aixlog.hpp"
|
||||||
#include "common/metatags.hpp"
|
|
||||||
#include "common/properties.hpp"
|
#include "common/properties.hpp"
|
||||||
#include "common/utils/string_utils.hpp"
|
#include "common/utils/string_utils.hpp"
|
||||||
#include "server/streamreader/control_error.hpp"
|
#include "server/streamreader/control_error.hpp"
|
||||||
|
|
Loading…
Add table
Reference in a new issue