Add RPC "Stream.Control" command

This commit is contained in:
badaix 2021-05-24 22:36:16 +02:00
parent 09cde776b1
commit 91ea368121
5 changed files with 163 additions and 90 deletions

View file

@ -584,6 +584,7 @@ Usage: %(progname)s [OPTION]...
--snapcast-host=ADDR Set the mpd server address --snapcast-host=ADDR Set the mpd server address
--snapcast-port=PORT Set the TCP port --snapcast-port=PORT Set the TCP port
--stream=ID Set the stream id --stream=ID Set the stream id
--command=CMD Issue a command to MPD and exit
-d, --debug Run in debug mode -d, --debug Run in debug mode
-j, --use-journal Log to systemd journal instead of stderr -j, --use-journal Log to systemd journal instead of stderr
@ -606,7 +607,7 @@ if __name__ == '__main__':
# Parse command line # Parse command line
try: try:
(opts, args) = getopt.getopt(sys.argv[1:], 'hdjv', (opts, args) = getopt.getopt(sys.argv[1:], 'hdjv',
['help', 'mpd-host=', 'mpd-port=', 'snapcast-host=', 'snapcast-port=', 'stream=', 'debug', 'use-journal', 'version']) ['help', 'mpd-host=', 'mpd-port=', 'snapcast-host=', 'snapcast-port=', 'stream=', 'command=', 'debug', 'use-journal', 'version'])
except getopt.GetoptError as ex: except getopt.GetoptError as ex:
(msg, opt) = ex.args (msg, opt) = ex.args
print("%s: %s" % (sys.argv[0], msg), file=sys.stderr) print("%s: %s" % (sys.argv[0], msg), file=sys.stderr)
@ -628,6 +629,8 @@ if __name__ == '__main__':
params['snapcast-port'] = int(arg) params['snapcast-port'] = int(arg)
elif opt in ['--stream']: elif opt in ['--stream']:
params['stream'] = arg params['stream'] = arg
elif opt in ['--command']:
params['command'] = arg
elif opt in ['-d', '--debug']: elif opt in ['-d', '--debug']:
log_level = logging.DEBUG log_level = logging.DEBUG
elif opt in ['-j', '--use-journal']: elif opt in ['-j', '--use-journal']:
@ -674,6 +677,41 @@ if __name__ == '__main__':
logger.debug(f'Parameters: {params}') logger.debug(f'Parameters: {params}')
if 'command' in params:
try:
cmd = params['command']
if cmd not in ['next', 'previous', 'play', 'pause', 'playpause', 'stop']:
logger.error(f'Command not supported: {cmd}')
sys.exit(1)
client = mpd.MPDClient()
client.connect(params['mpd-host'], params['mpd-port'])
if params['mpd-password']:
client.password(params['mpd-password'])
if cmd == 'next':
client.next()
elif cmd == 'previous':
client.previous()
elif cmd == 'play':
client.play()
elif cmd == 'pause':
client.pause(1)
elif cmd == 'playpause':
if client.status()['state'] == 'play':
client.pause(1)
else:
client.play()
elif cmd == 'stop':
client.stop()
client.close()
client.disconnect()
except mpd.CommandError as e:
logger.error(e)
sys.exit(1)
sys.exit(0)
# Set up the main loop # Set up the main loop
if using_gi_glib: if using_gi_glib:
logger.debug('Using GObject-Introspection main loop.') logger.debug('Using GObject-Introspection main loop.')

View file

@ -458,6 +458,11 @@ class MPDWrapper(object):
# def last_currentsong(self): # def last_currentsong(self):
# return self._currentsong.copy() # return self._currentsong.copy()
def control(self, command): # , param = ""):
logger.info(f'Control: {command}')
requests.post(f'http://{params["host"]}:{params["port"]}/jsonrpc', json={
"id": 1, "jsonrpc": "2.0", "method": "Stream.Control", "params": {"id": "Pipe", "command": command}})
@property @property
def metadata(self): def metadata(self):
return self._metadata return self._metadata
@ -840,8 +845,6 @@ class MPRISInterface(dbus.service.Object):
"CanControl": (True, None), "CanControl": (True, None),
} }
__tracklist_interface = "org.mpris.MediaPlayer2.TrackList"
__prop_mapping = { __prop_mapping = {
__player_interface: __player_props, __player_interface: __player_props,
__root_interface: __root_props, __root_interface: __root_props,
@ -906,47 +909,32 @@ class MPRISInterface(dbus.service.Object):
# Player methods # Player methods
@ dbus.service.method(__player_interface, in_signature='', out_signature='') @ dbus.service.method(__player_interface, in_signature='', out_signature='')
def Next(self): def Next(self):
logger.info('Next') snapcast_wrapper.control("next")
# mpd_wrapper.next()
return return
@ dbus.service.method(__player_interface, in_signature='', out_signature='') @ dbus.service.method(__player_interface, in_signature='', out_signature='')
def Previous(self): def Previous(self):
logger.info('Previous') snapcast_wrapper.control("previous")
# mpd_wrapper.previous()
return return
@ dbus.service.method(__player_interface, in_signature='', out_signature='') @ dbus.service.method(__player_interface, in_signature='', out_signature='')
def Pause(self): def Pause(self):
logger.info('Pause') snapcast_wrapper.control("pause")
# mpd_wrapper.pause(1)
# mpd_wrapper.notify_about_state('pause')
return return
@ dbus.service.method(__player_interface, in_signature='', out_signature='') @ dbus.service.method(__player_interface, in_signature='', out_signature='')
def PlayPause(self): def PlayPause(self):
logger.info('PlayPause') snapcast_wrapper.control("playpause")
# status = mpd_wrapper.status()
# if status['state'] == 'play':
# mpd_wrapper.pause(1)
# mpd_wrapper.notify_about_state('pause')
# else:
# mpd_wrapper.play()
# mpd_wrapper.notify_about_state('play')
return return
@ dbus.service.method(__player_interface, in_signature='', out_signature='') @ dbus.service.method(__player_interface, in_signature='', out_signature='')
def Stop(self): def Stop(self):
logger.info('Stop') snapcast_wrapper.control("stop")
# mpd_wrapper.stop()
# mpd_wrapper.notify_about_state('stop')
return return
@ dbus.service.method(__player_interface, in_signature='', out_signature='') @ dbus.service.method(__player_interface, in_signature='', out_signature='')
def Play(self): def Play(self):
logger.info('Play') snapcast_wrapper.control("play")
# mpd_wrapper.play()
# mpd_wrapper.notify_about_state('play')
return return
@ dbus.service.method(__player_interface, in_signature='x', out_signature='') @ dbus.service.method(__player_interface, in_signature='x', out_signature='')
@ -979,17 +967,18 @@ class MPRISInterface(dbus.service.Object):
# self.Seeked(position * 1000000) # self.Seeked(position * 1000000)
return return
@dbus.service.signal(__player_interface, signature='x')
def Seeked(self, position):
logger.debug("Seeked to %i" % position)
return float(position)
@ dbus.service.method(__player_interface, in_signature='', out_signature='') @ dbus.service.method(__player_interface, in_signature='', out_signature='')
def OpenUri(self): def OpenUri(self):
logger.info('OpenUri') logger.info('OpenUri')
# TODO # TODO
return return
# Player signals
@ dbus.service.signal(__player_interface, signature='x')
def Seeked(self, position):
logger.debug("Seeked to %i" % position)
return float(position)
def __get_client_from_server_status(status): def __get_client_from_server_status(status):
client = None client = None
@ -998,7 +987,8 @@ def __get_client_from_server_status(status):
for client in group['clients']: for client in group['clients']:
if client['host']['name'] == hostname: if client['host']['name'] == hostname:
active = client["connected"] active = client["connected"]
logger.info(f'Client with id "{client["id"]}" active: {active}') logger.info(
f'Client with id "{client["id"]}" active: {active}')
client = client['id'] client = client['id']
if active: if active:
return client return client
@ -1006,6 +996,7 @@ def __get_client_from_server_status(status):
logger.error('Failed to parse server status') logger.error('Failed to parse server status')
return client return client
def usage(params): def usage(params):
print("""\ print("""\
Usage: %(progname)s [OPTION]... Usage: %(progname)s [OPTION]...
@ -1130,10 +1121,13 @@ if __name__ == '__main__':
if params['client'] is None: if params['client'] is None:
hostname = socket.gethostname() hostname = socket.gethostname()
logger.info(f'No client id specified, trying to find a client running on host "{hostname}"') logger.info(
resp = requests.post(f'http://{params["host"]}:{params["port"]}/jsonrpc', json={"id":1,"jsonrpc":"2.0","method":"Server.GetStatus"}) f'No client id specified, trying to find a client running on host "{hostname}"')
resp = requests.post(f'http://{params["host"]}:{params["port"]}/jsonrpc', json={
"id": 1, "jsonrpc": "2.0", "method": "Server.GetStatus"})
if resp.ok: if resp.ok:
params['client'] = __get_client_from_server_status(json.loads(resp.text)) params['client'] = __get_client_from_server_status(
json.loads(resp.text))
if params['client'] is None: if params['client'] is None:
logger.error('Client not found or not configured') logger.error('Client not found or not configured')

View file

@ -410,10 +410,10 @@ void Server::processRequest(const jsonrpcpp::request_ptr request, jsonrpcpp::ent
{ {
// clang-format off // clang-format off
// Request: {"id":4,"jsonrpc":"2.0","method":"Stream.SetMeta","params":{"id":"Spotify", "meta": {"album": "some album", "artist": "some artist", "track": "some track"...}}} // Request: {"id":4,"jsonrpc":"2.0","method":"Stream.SetMeta","params":{"id":"Spotify", "meta": {"album": "some album", "artist": "some artist", "track": "some track"...}}}
// Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"Spotify"}} // Response: {"id":4,"jsonrpc":"2.0","result":{"id":"Spotify"}}
// clang-format on // clang-format on
LOG(INFO, LOG_TAG) << "Stream.SetMeta(" << request->params().get<std::string>("id") << ")" << request->params().get("meta") << "\n"; LOG(INFO, LOG_TAG) << "Stream.SetMeta id: " << request->params().get<std::string>("id") << ", meta: " << request->params().get("meta") << "\n";
// Find stream // Find stream
string streamId = request->params().get<std::string>("id"); string streamId = request->params().get<std::string>("id");
@ -427,11 +427,36 @@ 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.Control") == 0)
{
// clang-format off
// Request: {"id":4,"jsonrpc":"2.0","method":"Stream.Control","params":{"id":"Spotify", "command": "next", params: {}}}
// Response: {"id":4,"jsonrpc":"2.0","result":{"id":"Spotify"}}
//
// Request: {"id":4,"jsonrpc":"2.0","method":"Stream.Control","params":{"id":"Spotify", "command": "seek", "param": "60000"}}
// Response: {"id":4,"jsonrpc":"2.0","result":{"id":"Spotify"}}
// clang-format on
LOG(INFO, LOG_TAG) << "Stream.Control id: " << request->params().get<std::string>("id") << ", command: " << request->params().get("command")
<< "\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());
// Set metadata from request
stream->control(request->params().get("command"), request->params().has("param") ? request->params().get("param") : "");
// Setup response
result["id"] = streamId;
}
else if (request->method() == "Stream.AddStream") else if (request->method() == "Stream.AddStream")
{ {
// clang-format off // clang-format off
// Request: {"id":4,"jsonrpc":"2.0","method":"Stream.AddStream","params":{"streamUri":"uri"}} // Request: {"id":4,"jsonrpc":"2.0","method":"Stream.AddStream","params":{"streamUri":"uri"}}
// Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"Spotify"}} // Response: {"id":4,"jsonrpc":"2.0","result":{"id":"Spotify"}}
// clang-format on // clang-format on
LOG(INFO, LOG_TAG) << "Stream.AddStream(" << request->params().get("streamUri") << ")" LOG(INFO, LOG_TAG) << "Stream.AddStream(" << request->params().get("streamUri") << ")"
@ -450,7 +475,7 @@ void Server::processRequest(const jsonrpcpp::request_ptr request, jsonrpcpp::ent
{ {
// clang-format off // clang-format off
// Request: {"id":4,"jsonrpc":"2.0","method":"Stream.RemoveStream","params":{"id":"Spotify"}} // Request: {"id":4,"jsonrpc":"2.0","method":"Stream.RemoveStream","params":{"id":"Spotify"}}
// Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"Spotify"}} // Response: {"id":4,"jsonrpc":"2.0","result":{"id":"Spotify"}}
// clang-format on // clang-format on
LOG(INFO, LOG_TAG) << "Stream.RemoveStream(" << request->params().get("id") << ")" LOG(INFO, LOG_TAG) << "Stream.RemoveStream(" << request->params().get("id") << ")"

View file

@ -48,7 +48,7 @@ CtrlScript::~CtrlScript()
} }
void CtrlScript::start(const std::string& stream_id, const ServerSettings& server_setttings) void CtrlScript::start(const std::string& stream_id, const ServerSettings& server_setttings, const std::string& command, const std::string& param)
{ {
pipe_stderr_ = bp::pipe(); pipe_stderr_ = bp::pipe();
pipe_stdout_ = bp::pipe(); pipe_stdout_ = bp::pipe();
@ -56,7 +56,15 @@ void CtrlScript::start(const std::string& stream_id, const ServerSettings& serve
params << " \"--stream=" + stream_id + "\""; params << " \"--stream=" + stream_id + "\"";
if (server_setttings.http.enabled) if (server_setttings.http.enabled)
params << " --snapcast-port=" << server_setttings.http.port; params << " --snapcast-port=" << server_setttings.http.port;
process_ = bp::child(script_ + params.str(), bp::std_out > pipe_stdout_, bp::std_err > pipe_stderr_); if (!command.empty())
params << " --command=" << command;
if (!param.empty())
params << " --param=" << param;
process_ = bp::child(
script_ + params.str(), bp::std_out > pipe_stdout_, bp::std_err > pipe_stderr_,
bp::on_exit = [](int exit,
const std::error_code& ec_in) { LOG(INFO, SCRIPT_LOG_TAG) << "Exit code: " << exit << ", message: " << ec_in.message() << "\n"; },
ioc_);
stream_stdout_ = make_unique<boost::asio::posix::stream_descriptor>(ioc_, pipe_stdout_.native_source()); stream_stdout_ = make_unique<boost::asio::posix::stream_descriptor>(ioc_, pipe_stdout_.native_source());
stream_stderr_ = make_unique<boost::asio::posix::stream_descriptor>(ioc_, pipe_stderr_.native_source()); stream_stderr_ = make_unique<boost::asio::posix::stream_descriptor>(ioc_, pipe_stderr_.native_source());
stderrReadLine(); stderrReadLine();
@ -93,10 +101,7 @@ void CtrlScript::logScript(const std::string& source, std::string line)
void CtrlScript::stderrReadLine() void CtrlScript::stderrReadLine()
{ {
const std::string delimiter = "\n"; const std::string delimiter = "\n";
boost::asio::async_read_until( boost::asio::async_read_until(*stream_stderr_, streambuf_stderr_, delimiter, [this, delimiter](const std::error_code& ec, std::size_t bytes_transferred) {
*stream_stderr_, streambuf_stderr_, delimiter,
[this, delimiter](const std::error_code& ec, std::size_t bytes_transferred)
{
if (ec) if (ec)
{ {
LOG(ERROR, LOG_TAG) << "Error while reading from stderr: " << ec.message() << "\n"; LOG(ERROR, LOG_TAG) << "Error while reading from stderr: " << ec.message() << "\n";
@ -115,10 +120,7 @@ void CtrlScript::stderrReadLine()
void CtrlScript::stdoutReadLine() void CtrlScript::stdoutReadLine()
{ {
const std::string delimiter = "\n"; const std::string delimiter = "\n";
boost::asio::async_read_until( boost::asio::async_read_until(*stream_stdout_, streambuf_stdout_, delimiter, [this, delimiter](const std::error_code& ec, std::size_t bytes_transferred) {
*stream_stdout_, streambuf_stdout_, delimiter,
[this, delimiter](const std::error_code& ec, std::size_t bytes_transferred)
{
if (ec) if (ec)
{ {
LOG(ERROR, LOG_TAG) << "Error while reading from stdout: " << ec.message() << "\n"; LOG(ERROR, LOG_TAG) << "Error while reading from stdout: " << ec.message() << "\n";
@ -162,7 +164,10 @@ PcmStream::PcmStream(PcmListener* pcmListener, boost::asio::io_context& ioc, con
LOG(INFO, LOG_TAG) << "PcmStream: " << name_ << ", sampleFormat: " << sampleFormat_.toString() << "\n"; LOG(INFO, LOG_TAG) << "PcmStream: " << name_ << ", sampleFormat: " << sampleFormat_.toString() << "\n";
if (uri_.query.find(kControlScript) != uri_.query.end()) if (uri_.query.find(kControlScript) != uri_.query.end())
{
ctrl_script_ = std::make_unique<CtrlScript>(ioc, uri_.query[kControlScript]); ctrl_script_ = std::make_unique<CtrlScript>(ioc, uri_.query[kControlScript]);
command_script_ = std::make_unique<CtrlScript>(ioc, uri_.query[kControlScript]);
}
if (uri_.query.find(kUriChunkMs) != uri_.query.end()) if (uri_.query.find(kUriChunkMs) != uri_.query.end())
chunk_ms_ = cpt::stoul(uri_.query[kUriChunkMs]); chunk_ms_ = cpt::stoul(uri_.query[kUriChunkMs]);
@ -324,6 +329,14 @@ std::shared_ptr<msg::StreamTags> PcmStream::getMeta() const
} }
void PcmStream::control(const std::string& command, const std::string& param)
{
LOG(INFO, LOG_TAG) << "Stream " << getId() << " control: '" << command << "', param: '" << param << "'\n";
if (command_script_)
command_script_->start(getId(), server_settings_, command, param);
}
void PcmStream::setMeta(const json& jtag) void PcmStream::setMeta(const json& jtag)
{ {
meta_.reset(new msg::StreamTags(jtag)); meta_.reset(new msg::StreamTags(jtag));

View file

@ -114,7 +114,7 @@ public:
CtrlScript(boost::asio::io_context& ioc, const std::string& script); CtrlScript(boost::asio::io_context& ioc, const std::string& script);
virtual ~CtrlScript(); virtual ~CtrlScript();
void start(const std::string& stream_id, const ServerSettings& server_setttings); void start(const std::string& stream_id, const ServerSettings& server_setttings, const std::string& command = "", const std::string& param = "");
void stop(); void stop();
private: private:
@ -162,6 +162,8 @@ 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);
void control(const std::string& command, const std::string& param);
virtual ReaderState getState() const; virtual ReaderState getState() const;
virtual json toJson() const; virtual json toJson() const;
@ -187,6 +189,7 @@ protected:
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_;
std::unique_ptr<CtrlScript> command_script_;
}; };
} // namespace streamreader } // namespace streamreader