mirror of
https://github.com/badaix/snapcast.git
synced 2025-05-19 03:56:14 +02:00
Add support for Player.Properties
This commit is contained in:
parent
283c3d2c9b
commit
7c11cb7559
11 changed files with 577 additions and 154 deletions
|
@ -268,7 +268,10 @@ private:
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!source.has_value())
|
if (!source.has_value())
|
||||||
|
{
|
||||||
|
if (j.contains(tag))
|
||||||
j.erase(tag);
|
j.erase(tag);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
j[tag] = source.value();
|
j[tag] = source.value();
|
||||||
}
|
}
|
||||||
|
|
232
common/properties.hpp
Normal file
232
common/properties.hpp
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
/***
|
||||||
|
This file is part of snapcast
|
||||||
|
Copyright (C) 2014-2021 Johannes Pohl
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
***/
|
||||||
|
|
||||||
|
#ifndef PROPERTIES_HPP
|
||||||
|
#define PROPERTIES_HPP
|
||||||
|
|
||||||
|
#include <boost/optional.hpp>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "common/aixlog.hpp"
|
||||||
|
#include "common/json.hpp"
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
enum class PlaybackStatus
|
||||||
|
{
|
||||||
|
kPlaying = 0,
|
||||||
|
kPaused = 1,
|
||||||
|
kStopped = 2,
|
||||||
|
kUnknown = 3
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
static std::string to_string(PlaybackStatus playback_status)
|
||||||
|
{
|
||||||
|
switch (playback_status)
|
||||||
|
{
|
||||||
|
case PlaybackStatus::kPlaying:
|
||||||
|
return "playing";
|
||||||
|
case PlaybackStatus::kPaused:
|
||||||
|
return "paused";
|
||||||
|
case PlaybackStatus::kStopped:
|
||||||
|
return "stopped";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static std::ostream& operator<<(std::ostream& os, PlaybackStatus playback_status)
|
||||||
|
{
|
||||||
|
os << to_string(playback_status);
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum class LoopStatus
|
||||||
|
{
|
||||||
|
kNone = 0,
|
||||||
|
kTrack = 1,
|
||||||
|
kPlaylist = 2,
|
||||||
|
kUnknown = 3
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
static std::string to_string(LoopStatus loop_status)
|
||||||
|
{
|
||||||
|
switch (loop_status)
|
||||||
|
{
|
||||||
|
case LoopStatus::kNone:
|
||||||
|
return "none";
|
||||||
|
case LoopStatus::kTrack:
|
||||||
|
return "track";
|
||||||
|
case LoopStatus::kPlaylist:
|
||||||
|
return "playlist";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static std::ostream& operator<<(std::ostream& os, LoopStatus loop_status)
|
||||||
|
{
|
||||||
|
os << to_string(loop_status);
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Properties
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Properties() = default;
|
||||||
|
Properties(const json& j)
|
||||||
|
{
|
||||||
|
fromJson(j);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// https://www.musicpd.org/doc/html/protocol.html#tags
|
||||||
|
/// The current playback status
|
||||||
|
boost::optional<PlaybackStatus> playback_status;
|
||||||
|
/// The current loop / repeat status
|
||||||
|
boost::optional<LoopStatus> loop_status;
|
||||||
|
/// A value of false indicates that playback is progressing linearly through a playlist, while true means playback is progressing through a playlist in some
|
||||||
|
/// other order.
|
||||||
|
boost::optional<bool> shuffle;
|
||||||
|
/// The volume level between 0-100
|
||||||
|
boost::optional<int> volume;
|
||||||
|
/// The current track position in seconds
|
||||||
|
boost::optional<float> position;
|
||||||
|
/// The current track duration in seconds
|
||||||
|
boost::optional<float> duration;
|
||||||
|
/// Whether the client can call the Next method on this interface and expect the current track to change
|
||||||
|
boost::optional<bool> can_go_next;
|
||||||
|
/// Whether the client can call the Previous method on this interface and expect the current track to change
|
||||||
|
boost::optional<bool> can_go_previous;
|
||||||
|
/// Whether playback can be started using "play" or "playPause"
|
||||||
|
boost::optional<bool> can_play;
|
||||||
|
/// Whether playback can be paused using "pause" or "playPause"
|
||||||
|
boost::optional<bool> can_pause;
|
||||||
|
/// Whether the client can control the playback position using "seek" and "setPosition". This may be different for different tracks
|
||||||
|
boost::optional<bool> can_seek;
|
||||||
|
/// Whether the media player may be controlled over this interface
|
||||||
|
boost::optional<bool> can_control;
|
||||||
|
|
||||||
|
json toJson() const
|
||||||
|
{
|
||||||
|
json j;
|
||||||
|
if (playback_status.has_value())
|
||||||
|
addTag(j, "playbackStatus", boost::optional<std::string>(to_string(playback_status.value())));
|
||||||
|
if (loop_status.has_value())
|
||||||
|
addTag(j, "loopStatus", boost::optional<std::string>(to_string(loop_status.value())));
|
||||||
|
addTag(j, "shuffle", shuffle);
|
||||||
|
addTag(j, "volume", volume);
|
||||||
|
addTag(j, "position", position);
|
||||||
|
addTag(j, "duration", duration);
|
||||||
|
addTag(j, "canGoNext", can_go_next);
|
||||||
|
addTag(j, "canGoPrevious", can_go_previous);
|
||||||
|
addTag(j, "canPlay", can_play);
|
||||||
|
addTag(j, "canPause", can_pause);
|
||||||
|
addTag(j, "canSeek", can_seek);
|
||||||
|
addTag(j, "canControl", can_control);
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fromJson(const json& j)
|
||||||
|
{
|
||||||
|
boost::optional<std::string> opt;
|
||||||
|
readTag(j, "playbackStatus", opt);
|
||||||
|
if (opt.has_value())
|
||||||
|
{
|
||||||
|
if (*opt == "playing")
|
||||||
|
playback_status = PlaybackStatus::kPlaying;
|
||||||
|
else if (*opt == "paused")
|
||||||
|
playback_status = PlaybackStatus::kPaused;
|
||||||
|
else if (*opt == "stopped")
|
||||||
|
playback_status = PlaybackStatus::kStopped;
|
||||||
|
else
|
||||||
|
playback_status = PlaybackStatus::kUnknown;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
playback_status = boost::none;
|
||||||
|
readTag(j, "loopStatus", opt);
|
||||||
|
if (opt.has_value())
|
||||||
|
{
|
||||||
|
if (*opt == "none")
|
||||||
|
loop_status = LoopStatus::kNone;
|
||||||
|
else if (*opt == "track")
|
||||||
|
loop_status = LoopStatus::kTrack;
|
||||||
|
else if (*opt == "playlist")
|
||||||
|
loop_status = LoopStatus::kPlaylist;
|
||||||
|
else
|
||||||
|
loop_status = LoopStatus::kUnknown;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
loop_status = boost::none;
|
||||||
|
readTag(j, "shuffle", shuffle);
|
||||||
|
readTag(j, "volume", volume);
|
||||||
|
readTag(j, "position", position);
|
||||||
|
readTag(j, "duration", duration);
|
||||||
|
readTag(j, "canGoNext", can_go_next);
|
||||||
|
readTag(j, "canGoPrevious", can_go_previous);
|
||||||
|
readTag(j, "canPlay", can_play);
|
||||||
|
readTag(j, "canPause", can_pause);
|
||||||
|
readTag(j, "canSeek", can_seek);
|
||||||
|
readTag(j, "canControl", can_control);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
template <typename T>
|
||||||
|
void readTag(const json& j, const std::string& tag, boost::optional<T>& dest) const
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!j.contains(tag))
|
||||||
|
dest = boost::none;
|
||||||
|
else
|
||||||
|
dest = j[tag].get<T>();
|
||||||
|
}
|
||||||
|
catch (const std::exception& e)
|
||||||
|
{
|
||||||
|
LOG(ERROR) << "failed to read tag: '" << tag << "': " << e.what() << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void addTag(json& j, const std::string& tag, const boost::optional<T>& source) const
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!source.has_value())
|
||||||
|
{
|
||||||
|
if (j.contains(tag))
|
||||||
|
j.erase(tag);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
j[tag] = source.value();
|
||||||
|
}
|
||||||
|
catch (const std::exception& e)
|
||||||
|
{
|
||||||
|
LOG(ERROR) << "failed to add tag: '" << tag << "': " << e.what() << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
|
@ -82,10 +82,13 @@ defaults = {
|
||||||
# Player.Status
|
# Player.Status
|
||||||
status_mapping = {
|
status_mapping = {
|
||||||
# https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#properties
|
# https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#properties
|
||||||
'state': ['playbackStatus', lambda val: {'play': 'playing', 'pause': 'paused', 'stop': 'stopped'}[val]], # R/O - play => playing, pause => paused, stop => stopped
|
# R/O - play => playing, pause => paused, stop => stopped
|
||||||
'repeat': ['loopStatus', lambda val: {'0': 'none', '1': 'track', '2': 'playlist'}[val]], # R/W - 0 => none, 1 => track, n/a => playlist
|
'state': ['playbackStatus', lambda val: {'play': 'playing', 'pause': 'paused', 'stop': 'stopped'}[val]],
|
||||||
|
# R/W - 0 => none, 1 => track, n/a => playlist
|
||||||
|
'repeat': ['loopStatus', lambda val: {'0': 'none', '1': 'track', '2': 'playlist'}[val]],
|
||||||
# 'Rate d (Playback_Rate) R/W
|
# 'Rate d (Playback_Rate) R/W
|
||||||
'random': ['shuffle', lambda val: {'0': False, '1': True}[val]], # R/W - 0 => false, 1 => true
|
# R/W - 0 => false, 1 => true
|
||||||
|
'random': ['shuffle', lambda val: {'0': False, '1': True}[val]],
|
||||||
# 'Metadata a{sv} (Metadata_Map) Read only
|
# 'Metadata a{sv} (Metadata_Map) Read only
|
||||||
'volume': ['volume', int], # R/W - 0-100 => 0-100
|
'volume': ['volume', int], # R/W - 0-100 => 0-100
|
||||||
'elapsed': ['position', float], # R/O - seconds? ms?
|
'elapsed': ['position', float], # R/O - seconds? ms?
|
||||||
|
@ -109,7 +112,8 @@ status_mapping = {
|
||||||
# nextsong 2: playlist song number of the next song to be played
|
# nextsong 2: playlist song number of the next song to be played
|
||||||
# nextsongid 2: playlist songid of the next song to be played
|
# nextsongid 2: playlist songid of the next song to be played
|
||||||
# time: total time elapsed (of current playing/paused song) in seconds (deprecated, use elapsed instead)
|
# time: total time elapsed (of current playing/paused song) in seconds (deprecated, use elapsed instead)
|
||||||
'duration': ['duration', float], # duration 5: Duration of the current song in seconds.
|
# duration 5: Duration of the current song in seconds.
|
||||||
|
'duration': ['duration', float],
|
||||||
# bitrate: instantaneous bitrate in kbps
|
# bitrate: instantaneous bitrate in kbps
|
||||||
# xfade: crossfade in seconds
|
# xfade: crossfade in seconds
|
||||||
# mixrampdb: mixramp threshold in dB
|
# mixrampdb: mixramp threshold in dB
|
||||||
|
@ -119,7 +123,8 @@ status_mapping = {
|
||||||
# error: if there is an error, returns message here
|
# error: if there is an error, returns message here
|
||||||
|
|
||||||
# Snapcast
|
# Snapcast
|
||||||
'mute': ['mute', lambda val: {'0': False, '1': True}[val]] # R/W true/false
|
# R/W true/false
|
||||||
|
'mute': ['mute', lambda val: {'0': False, '1': True}[val]]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Player.Metadata
|
# Player.Metadata
|
||||||
|
@ -207,6 +212,7 @@ class MPDWrapper(object):
|
||||||
self._temp_cover = None
|
self._temp_cover = None
|
||||||
self._position = 0
|
self._position = 0
|
||||||
self._time = 0
|
self._time = 0
|
||||||
|
self._currentsong = None
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""
|
"""
|
||||||
|
@ -370,8 +376,9 @@ class MPDWrapper(object):
|
||||||
def control(self, cmd):
|
def control(self, cmd):
|
||||||
try:
|
try:
|
||||||
request = json.loads(cmd)
|
request = json.loads(cmd)
|
||||||
cmd = request['method']
|
|
||||||
id = request['id']
|
id = request['id']
|
||||||
|
[interface, cmd] = request['method'].split('.', 1)
|
||||||
|
if interface == 'Player':
|
||||||
success = True
|
success = True
|
||||||
if cmd == 'Next':
|
if cmd == 'Next':
|
||||||
self.next()
|
self.next()
|
||||||
|
@ -401,6 +408,29 @@ class MPDWrapper(object):
|
||||||
if offset >= 0:
|
if offset >= 0:
|
||||||
strOffset = "+" + strOffset
|
strOffset = "+" + strOffset
|
||||||
self.seekcur(strOffset)
|
self.seekcur(strOffset)
|
||||||
|
elif cmd == 'SetProperties':
|
||||||
|
properties = request['params']
|
||||||
|
logger.info(f'SetProperties: {properties}')
|
||||||
|
if 'shuffle' in properties:
|
||||||
|
self.random(int(properties['shuffle']))
|
||||||
|
if 'loopStatus' in properties:
|
||||||
|
value = properties['loopStatus']
|
||||||
|
if value == "playlist":
|
||||||
|
self.repeat(1)
|
||||||
|
if self._can_single:
|
||||||
|
self.single(0)
|
||||||
|
elif value == "track":
|
||||||
|
if self._can_single:
|
||||||
|
self.repeat(1)
|
||||||
|
self.single(1)
|
||||||
|
elif value == "none":
|
||||||
|
self.repeat(0)
|
||||||
|
if self._can_single:
|
||||||
|
self.single(0)
|
||||||
|
else:
|
||||||
|
send({"jsonrpc": "2.0", "error": {"code": -32601,
|
||||||
|
"message": "Method not found"}, "id": id})
|
||||||
|
success = False
|
||||||
else:
|
else:
|
||||||
send({"jsonrpc": "2.0", "error": {"code": -32601,
|
send({"jsonrpc": "2.0", "error": {"code": -32601,
|
||||||
"message": "Method not found"}, "id": id})
|
"message": "Method not found"}, "id": id})
|
||||||
|
@ -446,7 +476,8 @@ class MPDWrapper(object):
|
||||||
if subsystem in ("player", "mixer", "options", "playlist"):
|
if subsystem in ("player", "mixer", "options", "playlist"):
|
||||||
if not updated:
|
if not updated:
|
||||||
logger.info(f'Subsystem: {subsystem}')
|
logger.info(f'Subsystem: {subsystem}')
|
||||||
self._update_properties(force=True)
|
self._update_properties(
|
||||||
|
force=subsystem == 'player')
|
||||||
updated = True
|
updated = True
|
||||||
self.idle_enter()
|
self.idle_enter()
|
||||||
return True
|
return True
|
||||||
|
@ -455,6 +486,44 @@ class MPDWrapper(object):
|
||||||
self.reconnect()
|
self.reconnect()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def update_albumart(self, snapmeta):
|
||||||
|
album_key = 'musicbrainzAlbumId'
|
||||||
|
try:
|
||||||
|
if not album_key in snapmeta:
|
||||||
|
mbartist = None
|
||||||
|
mbrelease = None
|
||||||
|
if 'artist' in snapmeta:
|
||||||
|
mbartist = snapmeta['artist'][0]
|
||||||
|
if 'album' in snapmeta:
|
||||||
|
mbrelease = snapmeta['album']
|
||||||
|
else:
|
||||||
|
if 'title' in snapmeta:
|
||||||
|
mbrelease = snapmeta['title']
|
||||||
|
|
||||||
|
if mbartist is not None and mbrelease is not None:
|
||||||
|
logger.info(
|
||||||
|
f'Querying album art for artist "{mbartist}", release: "{mbrelease}"')
|
||||||
|
result = musicbrainzngs.search_releases(artist=mbartist, release=mbrelease,
|
||||||
|
limit=1)
|
||||||
|
if result['release-list']:
|
||||||
|
snapmeta[album_key] = result['release-list'][0]['id']
|
||||||
|
|
||||||
|
if album_key in snapmeta:
|
||||||
|
data = musicbrainzngs.get_image_list(snapmeta[album_key])
|
||||||
|
for image in data["images"]:
|
||||||
|
if "Front" in image["types"] and image["approved"]:
|
||||||
|
snapmeta['artUrl'] = image["thumbnails"]["small"]
|
||||||
|
logger.debug(
|
||||||
|
f'{snapmeta["artUrl"]} is an approved front image')
|
||||||
|
logger.info(f'Snapmeta: {snapmeta}')
|
||||||
|
send(
|
||||||
|
{"jsonrpc": "2.0", "method": "Player.Metadata", "params": snapmeta})
|
||||||
|
break
|
||||||
|
|
||||||
|
except musicbrainzngs.musicbrainz.ResponseError as e:
|
||||||
|
logger.error(
|
||||||
|
f'Error while getting cover for {snapmeta[album_key]}: {e}')
|
||||||
|
|
||||||
def update_metadata(self):
|
def update_metadata(self):
|
||||||
"""
|
"""
|
||||||
Translate metadata returned by MPD to the MPRIS v2 syntax.
|
Translate metadata returned by MPD to the MPRIS v2 syntax.
|
||||||
|
@ -505,49 +574,16 @@ class MPDWrapper(object):
|
||||||
snapmeta['title'] = fields[1]
|
snapmeta['title'] = fields[1]
|
||||||
|
|
||||||
send({"jsonrpc": "2.0", "method": "Player.Metadata", "params": snapmeta})
|
send({"jsonrpc": "2.0", "method": "Player.Metadata", "params": snapmeta})
|
||||||
|
self.update_albumart(snapmeta)
|
||||||
album_key = 'musicbrainzAlbumId'
|
|
||||||
try:
|
|
||||||
if not album_key in snapmeta:
|
|
||||||
mbartist = None
|
|
||||||
mbrelease = None
|
|
||||||
if 'artist' in snapmeta:
|
|
||||||
mbartist = snapmeta['artist'][0]
|
|
||||||
if 'album' in snapmeta:
|
|
||||||
mbrelease = snapmeta['album']
|
|
||||||
else:
|
|
||||||
if 'title' in snapmeta:
|
|
||||||
mbrelease = snapmeta['title']
|
|
||||||
|
|
||||||
if mbartist is not None and mbrelease is not None:
|
|
||||||
logger.info(
|
|
||||||
f'Querying album art for artist "{mbartist}", release: "{mbrelease}"')
|
|
||||||
result = musicbrainzngs.search_releases(artist=mbartist, release=mbrelease,
|
|
||||||
limit=1)
|
|
||||||
if result['release-list']:
|
|
||||||
snapmeta[album_key] = result['release-list'][0]['id']
|
|
||||||
|
|
||||||
if album_key in snapmeta:
|
|
||||||
data = musicbrainzngs.get_image_list(snapmeta[album_key])
|
|
||||||
for image in data["images"]:
|
|
||||||
if "Front" in image["types"] and image["approved"]:
|
|
||||||
snapmeta['artUrl'] = image["thumbnails"]["small"]
|
|
||||||
logger.debug(
|
|
||||||
f'{snapmeta["artUrl"]} is an approved front image')
|
|
||||||
break
|
|
||||||
|
|
||||||
except musicbrainzngs.musicbrainz.ResponseError as e:
|
|
||||||
logger.error(
|
|
||||||
f'Error while getting cover for {snapmeta[album_key]}: {e}')
|
|
||||||
|
|
||||||
logger.info(f'Snapmeta: {snapmeta}')
|
|
||||||
send({"jsonrpc": "2.0", "method": "Player.Metadata", "params": snapmeta})
|
|
||||||
|
|
||||||
def _update_properties(self, force=False):
|
def _update_properties(self, force=False):
|
||||||
old_status = self._status
|
old_status = self._status
|
||||||
old_position = self._position
|
old_position = self._position
|
||||||
old_time = self._time
|
old_time = self._time
|
||||||
self._currentsong = self.client.currentsong()
|
currentsong = self.client.currentsong()
|
||||||
|
if self._currentsong != currentsong:
|
||||||
|
self._currentsong = currentsong
|
||||||
|
force = True
|
||||||
new_status = self.client.status()
|
new_status = self.client.status()
|
||||||
logger.info(f'new status: {new_status}')
|
logger.info(f'new status: {new_status}')
|
||||||
self._time = new_time = int(time.time())
|
self._time = new_time = int(time.time())
|
||||||
|
@ -566,7 +602,13 @@ class MPDWrapper(object):
|
||||||
logger.warning("Can't cast value %r to %s" %
|
logger.warning("Can't cast value %r to %s" %
|
||||||
(value, status_mapping[key][1]))
|
(value, status_mapping[key][1]))
|
||||||
|
|
||||||
send({"jsonrpc": "2.0", "method": "Player.Status", "params": snapstatus})
|
snapstatus['canGoNext'] = True
|
||||||
|
snapstatus['canGoPrevious'] = True
|
||||||
|
snapstatus['canPlay'] = True
|
||||||
|
snapstatus['canPause'] = True
|
||||||
|
snapstatus['canSeek'] = True
|
||||||
|
snapstatus['canControl'] = True
|
||||||
|
send({"jsonrpc": "2.0", "method": "Player.Properties", "params": snapstatus})
|
||||||
|
|
||||||
if not new_status:
|
if not new_status:
|
||||||
logger.debug("_update_properties: failed to get new status")
|
logger.debug("_update_properties: failed to get new status")
|
||||||
|
|
|
@ -32,6 +32,7 @@ import sys
|
||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
import getopt
|
import getopt
|
||||||
|
import time
|
||||||
import socket
|
import socket
|
||||||
import dbus
|
import dbus
|
||||||
import dbus.service
|
import dbus.service
|
||||||
|
@ -306,7 +307,7 @@ class MPDWrapper(object):
|
||||||
'repeat': None,
|
'repeat': None,
|
||||||
}
|
}
|
||||||
self._metadata = {}
|
self._metadata = {}
|
||||||
self._position = 0
|
self._properties = {}
|
||||||
self._time = 0
|
self._time = 0
|
||||||
self._req_id = 0
|
self._req_id = 0
|
||||||
|
|
||||||
|
@ -366,6 +367,30 @@ class MPDWrapper(object):
|
||||||
'Metadata')
|
'Metadata')
|
||||||
logger.info(f'new meta {new_meta}')
|
logger.info(f'new meta {new_meta}')
|
||||||
|
|
||||||
|
elif jmsg["method"] == "Stream.OnProperties":
|
||||||
|
logger.info(
|
||||||
|
f'Stream properties changed for "{jmsg["params"]["id"]}"')
|
||||||
|
props = jmsg["params"]["properties"]
|
||||||
|
logger.info(f'Properties: "{props}"')
|
||||||
|
props['received'] = time.time()
|
||||||
|
changed_properties = self.update_properties(props)
|
||||||
|
logger.info(f'Changed properties: "{changed_properties}"')
|
||||||
|
if 'playbackStatus' in changed_properties:
|
||||||
|
self._dbus_service.update_property(
|
||||||
|
'org.mpris.MediaPlayer2.Player', 'PlaybackStatus')
|
||||||
|
if 'loopStatus' in changed_properties:
|
||||||
|
self._dbus_service.update_property(
|
||||||
|
'org.mpris.MediaPlayer2.Player', 'LoopStatus')
|
||||||
|
if 'shuffle' in changed_properties:
|
||||||
|
self._dbus_service.update_property(
|
||||||
|
'org.mpris.MediaPlayer2.Player', 'Shuffle')
|
||||||
|
if 'volume' in changed_properties:
|
||||||
|
self._dbus_service.update_property(
|
||||||
|
'org.mpris.MediaPlayer2.Player', 'Volume')
|
||||||
|
if 'canGoNext' in changed_properties:
|
||||||
|
self._dbus_service.update_property(
|
||||||
|
'org.mpris.MediaPlayer2.Player', 'CanGoNext')
|
||||||
|
|
||||||
def on_ws_error(self, ws, error):
|
def on_ws_error(self, ws, error):
|
||||||
logger.error("Snapcast RPC websocket error")
|
logger.error("Snapcast RPC websocket error")
|
||||||
logger.error(error)
|
logger.error(error)
|
||||||
|
@ -382,8 +407,7 @@ class MPDWrapper(object):
|
||||||
# '/org/mpris/MediaPlayer2')
|
# '/org/mpris/MediaPlayer2')
|
||||||
self._dbus_service.acquire_name()
|
self._dbus_service.acquire_name()
|
||||||
|
|
||||||
self.websocket.send(json.dumps(
|
self.send({"id": 1, "jsonrpc": "2.0", "method": "Server.GetStatus"})
|
||||||
{"id": 1, "jsonrpc": "2.0", "method": "Server.GetStatus"}))
|
|
||||||
|
|
||||||
def on_ws_close(self, ws):
|
def on_ws_close(self, ws):
|
||||||
logger.info("Snapcast RPC websocket closed")
|
logger.info("Snapcast RPC websocket closed")
|
||||||
|
@ -391,6 +415,9 @@ class MPDWrapper(object):
|
||||||
self._dbus_service.release_name()
|
self._dbus_service.release_name()
|
||||||
# self._dbus_service.remove_from_connection()
|
# self._dbus_service.remove_from_connection()
|
||||||
|
|
||||||
|
def send(self, json_msg):
|
||||||
|
self.websocket.send(json.dumps(json_msg))
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.websocket.keep_running = False
|
self.websocket.keep_running = False
|
||||||
logger.info("Waiting for websocket thread to exit")
|
logger.info("Waiting for websocket thread to exit")
|
||||||
|
@ -469,12 +496,54 @@ class MPDWrapper(object):
|
||||||
url = f'http://{self._params["host"]}:{self._params["port"]}/jsonrpc'
|
url = f'http://{self._params["host"]}:{self._params["port"]}/jsonrpc'
|
||||||
logger.info(f'url: {url}')
|
logger.info(f'url: {url}')
|
||||||
self._req_id += 1
|
self._req_id += 1
|
||||||
self.websocket.send(json.dumps(j))
|
self.send(j)
|
||||||
|
|
||||||
|
def set_property(self, property, value):
|
||||||
|
properties = {}
|
||||||
|
properties[property] = value
|
||||||
|
logger.info(f'set_properties {properties}')
|
||||||
|
j = {"id": self._req_id, "jsonrpc": "2.0", "method": "Stream.SetProperties",
|
||||||
|
"params": {"id": "Pipe", "properties": properties}}
|
||||||
|
logger.info(f'Set properties: {properties}, json: {j}')
|
||||||
|
url = f'http://{self._params["host"]}:{self._params["port"]}/jsonrpc'
|
||||||
|
logger.info(f'url: {url}')
|
||||||
|
self._req_id += 1
|
||||||
|
self.send(j)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def metadata(self):
|
def metadata(self):
|
||||||
return self._metadata
|
return self._metadata
|
||||||
|
|
||||||
|
@property
|
||||||
|
def properties(self):
|
||||||
|
return self._properties
|
||||||
|
|
||||||
|
def update_properties(self, new_properties):
|
||||||
|
changed_properties = {}
|
||||||
|
for key, value in new_properties.items():
|
||||||
|
if not key in self._properties:
|
||||||
|
changed_properties[key] = [None, value]
|
||||||
|
elif value != self._properties[key]:
|
||||||
|
changed_properties[key] = [self._properties[key], value]
|
||||||
|
for key, value in self._properties.items():
|
||||||
|
if not key in self._properties:
|
||||||
|
changed_properties[key] = [value, None]
|
||||||
|
self._properties = new_properties
|
||||||
|
return changed_properties
|
||||||
|
|
||||||
|
def position(self):
|
||||||
|
if not 'position' in self._properties:
|
||||||
|
return 0
|
||||||
|
if not 'duration' in self._properties:
|
||||||
|
return 0
|
||||||
|
position = self._properties['position']
|
||||||
|
if 'received' in self._properties:
|
||||||
|
position += (time.time() - self._properties['received'])
|
||||||
|
return position * 1000000
|
||||||
|
|
||||||
|
def property(self, name, default):
|
||||||
|
return self._properties.get(name, default)
|
||||||
|
|
||||||
def notify_about_track(self, meta, state='play'):
|
def notify_about_track(self, meta, state='play'):
|
||||||
uri = 'sound'
|
uri = 'sound'
|
||||||
# if 'mpris:artUrl' in meta:
|
# if 'mpris:artUrl' in meta:
|
||||||
|
@ -763,82 +832,53 @@ class MPRISInterface(dbus.service.Object):
|
||||||
}
|
}
|
||||||
|
|
||||||
def __get_playback_status():
|
def __get_playback_status():
|
||||||
# status = mpd_wrapper.last_status()
|
status = snapcast_wrapper.property('playbackStatus', 'stopped')
|
||||||
# return {'play': 'Playing', 'pause': 'Paused', 'stop': 'Stopped'}[status['state']]
|
logger.debug(f'get_playback_status "{status}"')
|
||||||
return 'Playing'
|
return {'playing': 'Playing', 'paused': 'Paused', 'stopped': 'Stopped'}[status]
|
||||||
|
|
||||||
def __set_loop_status(value):
|
def __set_loop_status(value):
|
||||||
logger.debug(f'set_loop_status "{value}"')
|
logger.debug(f'set_loop_status "{value}"')
|
||||||
# if value == "Playlist":
|
snapcast_wrapper.set_property(
|
||||||
# mpd_wrapper.repeat(1)
|
'loopStatus', {'None': 'none', 'Track': 'track', 'Playlist': 'playlist'}[value])
|
||||||
# if mpd_wrapper._can_single:
|
|
||||||
# mpd_wrapper.single(0)
|
|
||||||
# elif value == "Track":
|
|
||||||
# if mpd_wrapper._can_single:
|
|
||||||
# mpd_wrapper.repeat(1)
|
|
||||||
# mpd_wrapper.single(1)
|
|
||||||
# elif value == "None":
|
|
||||||
# mpd_wrapper.repeat(0)
|
|
||||||
# if mpd_wrapper._can_single:
|
|
||||||
# mpd_wrapper.single(0)
|
|
||||||
# else:
|
|
||||||
# raise dbus.exceptions.DBusException("Loop mode %r not supported" %
|
|
||||||
# value)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def __get_loop_status():
|
def __get_loop_status():
|
||||||
logger.debug(f'get_loop_status')
|
status = snapcast_wrapper.property('loopStatus', 'none')
|
||||||
# status = mpd_wrapper.last_status()
|
logger.debug(f'get_loop_status "{status}"')
|
||||||
# if int(status['repeat']) == 1:
|
return {'none': 'None', 'track': 'Track', 'playlist': 'Playlist'}[status]
|
||||||
# if int(status.get('single', 0)) == 1:
|
|
||||||
# return "Track"
|
|
||||||
# else:
|
|
||||||
# return "Playlist"
|
|
||||||
# else:
|
|
||||||
# return "None"
|
|
||||||
return "None"
|
|
||||||
|
|
||||||
def __set_shuffle(value):
|
def __set_shuffle(value):
|
||||||
logger.debug(f'set_shuffle "{value}"')
|
logger.debug(f'set_shuffle "{value}"')
|
||||||
# mpd_wrapper.random(value)
|
snapcast_wrapper.set_property('shuffle', bool(value))
|
||||||
return
|
return
|
||||||
|
|
||||||
def __get_shuffle():
|
def __get_shuffle():
|
||||||
logger.debug(f'get_shuffle')
|
shuffle = snapcast_wrapper.property('shuffle', False)
|
||||||
# if int(mpd_wrapper.last_status()['random']) == 1:
|
logger.debug(f'get_shuffle "{shuffle}"')
|
||||||
# return True
|
return shuffle
|
||||||
# else:
|
|
||||||
# return False
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __get_metadata():
|
def __get_metadata():
|
||||||
logger.debug(f'get_metadata: {snapcast_wrapper.metadata}')
|
logger.debug(f'get_metadata: {snapcast_wrapper.metadata}')
|
||||||
return dbus.Dictionary(snapcast_wrapper.metadata, signature='sv')
|
return dbus.Dictionary(snapcast_wrapper.metadata, signature='sv')
|
||||||
|
|
||||||
def __get_volume():
|
def __get_volume():
|
||||||
logger.debug(f'get_volume')
|
volume = snapcast_wrapper.property('volume', 100)
|
||||||
# vol = float(mpd_wrapper.last_status().get('volume', 0))
|
logger.debug(f'get_volume "{volume}"')
|
||||||
# if vol > 0:
|
if volume > 0:
|
||||||
# return vol / 100.0
|
return volume / 100.0
|
||||||
# else:
|
else:
|
||||||
# return 0.0
|
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
def __set_volume(value):
|
def __set_volume(value):
|
||||||
logger.debug(f'set_voume: {value}')
|
logger.debug(f'set_voume: {value}')
|
||||||
# if value >= 0 and value <= 1:
|
if value >= 0 and value <= 1:
|
||||||
# mpd_wrapper.setvol(int(value * 100))
|
snapcast_wrapper.set_property('volume', int(value * 100))
|
||||||
return
|
return
|
||||||
|
|
||||||
def __get_position():
|
def __get_position():
|
||||||
logger.debug(f'get_position')
|
position = snapcast_wrapper.position()
|
||||||
# status = mpd_wrapper.last_status()
|
logger.debug(f'get_position: {position}')
|
||||||
# if 'time' in status:
|
return dbus.Int64(position)
|
||||||
# current, end = status['time'].split(':')
|
|
||||||
# return dbus.Int64((int(current) * 1000000))
|
|
||||||
# else:
|
|
||||||
# return dbus.Int64(0)
|
|
||||||
return dbus.Int64(0)
|
|
||||||
|
|
||||||
__player_interface = "org.mpris.MediaPlayer2.Player"
|
__player_interface = "org.mpris.MediaPlayer2.Player"
|
||||||
__player_props = {
|
__player_props = {
|
||||||
|
@ -995,7 +1035,8 @@ class MPRISInterface(dbus.service.Object):
|
||||||
# Player signals
|
# Player signals
|
||||||
@ dbus.service.signal(__player_interface, signature='x')
|
@ dbus.service.signal(__player_interface, signature='x')
|
||||||
def Seeked(self, position):
|
def Seeked(self, position):
|
||||||
logger.debug("Seeked to %i" % position)
|
logger.debug(f'Seeked to {position}')
|
||||||
|
snapcast_wrapper.properties['position'] = float(position) / 1000000
|
||||||
return float(position)
|
return float(position)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -68,6 +68,17 @@ void Server::onMetaChanged(const PcmStream* pcmStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Server::onPropertiesChanged(const PcmStream* pcmStream)
|
||||||
|
{
|
||||||
|
LOG(INFO, LOG_TAG) << "onPropertiesChanged (" << pcmStream->getName() << ")\n";
|
||||||
|
const auto props = pcmStream->getProperties();
|
||||||
|
|
||||||
|
// Send propeties to all connected control clients
|
||||||
|
json notification = jsonrpcpp::Notification("Stream.OnProperties", jsonrpcpp::Parameter("id", pcmStream->getId(), "properties", props->toJson())).to_json();
|
||||||
|
controlServer_->send(notification.dump(), nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void Server::onStateChanged(const PcmStream* pcmStream, ReaderState state)
|
void Server::onStateChanged(const PcmStream* pcmStream, ReaderState state)
|
||||||
{
|
{
|
||||||
// clang-format off
|
// clang-format off
|
||||||
|
@ -452,6 +463,28 @@ void Server::processRequest(const jsonrpcpp::request_ptr request, jsonrpcpp::ent
|
||||||
// Setup response
|
// Setup response
|
||||||
result["id"] = streamId;
|
result["id"] = streamId;
|
||||||
}
|
}
|
||||||
|
else if (request->method().find("Stream.SetProperties") == 0)
|
||||||
|
{
|
||||||
|
// clang-format off
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
LOG(INFO, LOG_TAG) << "Stream.SetProperties id: " << request->params().get<std::string>("id")
|
||||||
|
<< ", properties: " << request->params().get("properties") << "\n";
|
||||||
|
|
||||||
|
// Find stream
|
||||||
|
string streamId = request->params().get<std::string>("id");
|
||||||
|
PcmStreamPtr stream = streamManager_->getStream(streamId);
|
||||||
|
if (stream == nullptr)
|
||||||
|
throw jsonrpcpp::InternalErrorException("Stream not found", request->id());
|
||||||
|
|
||||||
|
Properties props(request->params().get("properties"));
|
||||||
|
|
||||||
|
// Set metadata from request
|
||||||
|
stream->setProperties(props);
|
||||||
|
|
||||||
|
// Setup response
|
||||||
|
result["id"] = streamId;
|
||||||
|
}
|
||||||
else if (request->method() == "Stream.AddStream")
|
else if (request->method() == "Stream.AddStream")
|
||||||
{
|
{
|
||||||
// clang-format off
|
// clang-format off
|
||||||
|
|
|
@ -76,6 +76,7 @@ private:
|
||||||
|
|
||||||
/// Implementation of PcmListener
|
/// Implementation of PcmListener
|
||||||
void onMetaChanged(const PcmStream* pcmStream) override;
|
void onMetaChanged(const PcmStream* pcmStream) override;
|
||||||
|
void onPropertiesChanged(const PcmStream* pcmStream) 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;
|
||||||
void onChunkEncoded(const PcmStream* pcmStream, std::shared_ptr<msg::PcmChunk> chunk, double duration) override;
|
void onChunkEncoded(const PcmStream* pcmStream, std::shared_ptr<msg::PcmChunk> chunk, double duration) override;
|
||||||
|
|
|
@ -93,6 +93,12 @@ void MetaStream::onMetaChanged(const PcmStream* pcmStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void MetaStream::onPropertiesChanged(const PcmStream* pcmStream)
|
||||||
|
{
|
||||||
|
LOG(DEBUG, LOG_TAG) << "onPropertiesChanged: " << pcmStream->getName() << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void MetaStream::onStateChanged(const PcmStream* pcmStream, ReaderState state)
|
void MetaStream::onStateChanged(const PcmStream* pcmStream, ReaderState state)
|
||||||
{
|
{
|
||||||
LOG(DEBUG, LOG_TAG) << "onStateChanged: " << pcmStream->getName() << ", state: " << state << "\n";
|
LOG(DEBUG, LOG_TAG) << "onStateChanged: " << pcmStream->getName() << ", state: " << state << "\n";
|
||||||
|
|
|
@ -47,6 +47,7 @@ public:
|
||||||
protected:
|
protected:
|
||||||
/// Implementation of PcmListener
|
/// Implementation of PcmListener
|
||||||
void onMetaChanged(const PcmStream* pcmStream) override;
|
void onMetaChanged(const PcmStream* pcmStream) override;
|
||||||
|
void onPropertiesChanged(const PcmStream* pcmStream) 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;
|
||||||
void onChunkEncoded(const PcmStream* pcmStream, std::shared_ptr<msg::PcmChunk> chunk, double duration) override;
|
void onChunkEncoded(const PcmStream* pcmStream, std::shared_ptr<msg::PcmChunk> chunk, double duration) override;
|
||||||
|
|
|
@ -257,38 +257,33 @@ void PcmStream::onControlMsg(const std::string& msg)
|
||||||
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() == "Player.Metadata")
|
if (notification->method() == "Player.Metadata")
|
||||||
{
|
{
|
||||||
|
LOG(DEBUG, LOG_TAG) << "Received metadata notification\n";
|
||||||
setMeta(notification->params().to_json());
|
setMeta(notification->params().to_json());
|
||||||
}
|
}
|
||||||
|
else if (notification->method() == "Player.Properties")
|
||||||
|
{
|
||||||
|
LOG(DEBUG, LOG_TAG) << "Received properties notification\n";
|
||||||
|
properties_ = std::make_shared<Properties>(notification->params().to_json());
|
||||||
|
// Trigger a stream update
|
||||||
|
for (auto* listener : pcmListeners_)
|
||||||
|
{
|
||||||
|
if (listener != nullptr)
|
||||||
|
listener->onPropertiesChanged(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
LOG(WARNING, LOG_TAG) << "Received unknown notification method: '" << notification->method() << "'\n";
|
||||||
}
|
}
|
||||||
else if (entity->is_request())
|
else if (entity->is_request())
|
||||||
{
|
{
|
||||||
LOG(INFO, LOG_TAG) << "Request\n";
|
jsonrpcpp::request_ptr request = dynamic_pointer_cast<jsonrpcpp::Request>(entity);
|
||||||
// jsonrpcpp::entity_ptr response(nullptr);
|
LOG(INFO, LOG_TAG) << "Request: " << request->method() << ", id: " << request->id() << ", params: " << request->params().to_json() << "\n";
|
||||||
// jsonrpcpp::notification_ptr notification(nullptr);
|
|
||||||
// jsonrpcpp::request_ptr request = dynamic_pointer_cast<jsonrpcpp::Request>(entity);
|
|
||||||
// processRequest(request, response, notification);
|
|
||||||
// saveConfig();
|
|
||||||
// ////cout << "Request: " << request->to_json().dump() << "\n";
|
|
||||||
// if (notification)
|
|
||||||
// {
|
|
||||||
// ////cout << "Notification: " << notification->to_json().dump() << "\n";
|
|
||||||
// controlServer_->send(notification->to_json().dump(), controlSession);
|
|
||||||
// }
|
|
||||||
// if (response)
|
|
||||||
// {
|
|
||||||
// ////cout << "Response: " << response->to_json().dump() << "\n";
|
|
||||||
// return response->to_json().dump();
|
|
||||||
// }
|
|
||||||
// return "";
|
|
||||||
}
|
}
|
||||||
else if (entity->is_response())
|
else if (entity->is_response())
|
||||||
{
|
{
|
||||||
jsonrpcpp::response_ptr response = dynamic_pointer_cast<jsonrpcpp::Response>(entity);
|
jsonrpcpp::response_ptr response = dynamic_pointer_cast<jsonrpcpp::Response>(entity);
|
||||||
LOG(INFO, LOG_TAG) << "Response: " << response->result().dump() << ", id: " << response->id() << "\n";
|
LOG(INFO, LOG_TAG) << "Response: " << response->result().dump() << ", id: " << response->id() << "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
// json j = json::parse(msg);
|
|
||||||
// setMeta(j["params"]["meta"]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -403,6 +398,24 @@ std::shared_ptr<msg::StreamTags> PcmStream::getMeta() const
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::shared_ptr<Properties> PcmStream::getProperties() const
|
||||||
|
{
|
||||||
|
return properties_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void PcmStream::setProperties(const Properties& props)
|
||||||
|
{
|
||||||
|
LOG(INFO, LOG_TAG) << "Stream '" << getId() << "' set properties: " << props.toJson() << "\n";
|
||||||
|
// TODO: queue commands, send next on timeout or after reception of the last command's response
|
||||||
|
if (ctrl_script_)
|
||||||
|
{
|
||||||
|
jsonrpcpp::Request request(++req_id_, "Player.SetProperties", props.toJson());
|
||||||
|
ctrl_script_->send(request.to_json().dump() + "\n"); //, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void PcmStream::control(const std::string& command, const json& params)
|
void PcmStream::control(const std::string& command, const json& params)
|
||||||
{
|
{
|
||||||
LOG(INFO, LOG_TAG) << "Stream '" << getId() << "' received command: '" << command << "', params: '" << params << "'\n";
|
LOG(INFO, LOG_TAG) << "Stream '" << getId() << "' received command: '" << command << "', params: '" << params << "'\n";
|
||||||
|
@ -410,9 +423,12 @@ void PcmStream::control(const std::string& command, const json& params)
|
||||||
{
|
{
|
||||||
LOG(INFO, LOG_TAG) << "Stream " << getId() << " key: '" << it.key() << "', param: '" << it.value() << "'\n";
|
LOG(INFO, LOG_TAG) << "Stream " << getId() << " key: '" << it.key() << "', param: '" << it.value() << "'\n";
|
||||||
}
|
}
|
||||||
jsonrpcpp::Request request(++req_id_, command, params);
|
// TODO: queue commands, send next on timeout or after reception of the last command's response
|
||||||
if (ctrl_script_)
|
if (ctrl_script_)
|
||||||
|
{
|
||||||
|
jsonrpcpp::Request request(++req_id_, "Player." + command, params);
|
||||||
ctrl_script_->send(request.to_json().dump() + "\n"); //, params);
|
ctrl_script_->send(request.to_json().dump() + "\n"); //, params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
#include <boost/optional.hpp>
|
#include <boost/optional.hpp>
|
||||||
|
|
||||||
#include "common/json.hpp"
|
#include "common/json.hpp"
|
||||||
|
#include "common/properties.hpp"
|
||||||
#include "common/sample_format.hpp"
|
#include "common/sample_format.hpp"
|
||||||
#include "encoder/encoder.hpp"
|
#include "encoder/encoder.hpp"
|
||||||
#include "message/codec_header.hpp"
|
#include "message/codec_header.hpp"
|
||||||
|
@ -102,6 +103,7 @@ class PcmListener
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
virtual void onMetaChanged(const PcmStream* pcmStream) = 0;
|
virtual void onMetaChanged(const PcmStream* pcmStream) = 0;
|
||||||
|
virtual void onPropertiesChanged(const PcmStream* pcmStream) = 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;
|
||||||
virtual void onChunkEncoded(const PcmStream* pcmStream, std::shared_ptr<msg::PcmChunk> chunk, double duration) = 0;
|
virtual void onChunkEncoded(const PcmStream* pcmStream, std::shared_ptr<msg::PcmChunk> chunk, double duration) = 0;
|
||||||
|
@ -169,6 +171,9 @@ public:
|
||||||
std::shared_ptr<msg::StreamTags> getMeta() const;
|
std::shared_ptr<msg::StreamTags> getMeta() const;
|
||||||
void setMeta(const json& j);
|
void setMeta(const json& j);
|
||||||
|
|
||||||
|
std::shared_ptr<Properties> getProperties() const;
|
||||||
|
void setProperties(const Properties& props);
|
||||||
|
|
||||||
virtual void control(const std::string& command, const json& params);
|
virtual void control(const std::string& command, const json& params);
|
||||||
|
|
||||||
virtual ReaderState getState() const;
|
virtual ReaderState getState() const;
|
||||||
|
@ -194,6 +199,7 @@ protected:
|
||||||
std::string name_;
|
std::string name_;
|
||||||
ReaderState state_;
|
ReaderState state_;
|
||||||
std::shared_ptr<msg::StreamTags> meta_;
|
std::shared_ptr<msg::StreamTags> meta_;
|
||||||
|
std::shared_ptr<Properties> properties_;
|
||||||
boost::asio::io_context& ioc_;
|
boost::asio::io_context& ioc_;
|
||||||
ServerSettings server_settings_;
|
ServerSettings server_settings_;
|
||||||
std::unique_ptr<CtrlScript> ctrl_script_;
|
std::unique_ptr<CtrlScript> ctrl_script_;
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
#include "catch.hpp"
|
#include "catch.hpp"
|
||||||
#include "common/aixlog.hpp"
|
#include "common/aixlog.hpp"
|
||||||
#include "common/metatags.hpp"
|
#include "common/metatags.hpp"
|
||||||
|
#include "common/properties.hpp"
|
||||||
#include "common/utils/string_utils.hpp"
|
#include "common/utils/string_utils.hpp"
|
||||||
#include "server/streamreader/stream_uri.hpp"
|
#include "server/streamreader/stream_uri.hpp"
|
||||||
|
|
||||||
|
@ -153,3 +154,44 @@ TEST_CASE("Metatags")
|
||||||
std::cout << out_json.dump(4) << "\n";
|
std::cout << out_json.dump(4) << "\n";
|
||||||
REQUIRE(in_json == out_json);
|
REQUIRE(in_json == out_json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST_CASE("Properties")
|
||||||
|
{
|
||||||
|
REQUIRE(to_string(PlaybackStatus::kPaused) == "paused");
|
||||||
|
auto in_json = json::parse(R"(
|
||||||
|
{
|
||||||
|
"playbackStatus": "playing",
|
||||||
|
"loopStatus": "track",
|
||||||
|
"shuffle": false,
|
||||||
|
"volume": 42,
|
||||||
|
"elapsed": 23.0
|
||||||
|
}
|
||||||
|
)");
|
||||||
|
std::cout << in_json.dump(4) << "\n";
|
||||||
|
|
||||||
|
Properties props(in_json);
|
||||||
|
std::cout << props.toJson().dump(4) << "\n";
|
||||||
|
|
||||||
|
REQUIRE(props.playback_status.has_value());
|
||||||
|
|
||||||
|
auto out_json = props.toJson();
|
||||||
|
std::cout << out_json.dump(4) << "\n";
|
||||||
|
REQUIRE(in_json == out_json);
|
||||||
|
|
||||||
|
in_json = json::parse(R"(
|
||||||
|
{
|
||||||
|
"volume": 42
|
||||||
|
}
|
||||||
|
)");
|
||||||
|
std::cout << in_json.dump(4) << "\n";
|
||||||
|
|
||||||
|
props.fromJson(in_json);
|
||||||
|
std::cout << props.toJson().dump(4) << "\n";
|
||||||
|
|
||||||
|
REQUIRE(!props.playback_status.has_value());
|
||||||
|
|
||||||
|
out_json = props.toJson();
|
||||||
|
std::cout << out_json.dump(4) << "\n";
|
||||||
|
REQUIRE(in_json == out_json);
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue