From 2856879600b9d8002116495dddc4781559343b48 Mon Sep 17 00:00:00 2001 From: John Patrick Poet Date: Sat, 7 Sep 2024 08:50:38 -0700 Subject: [PATCH] ExternalRecorder: Add a JSON control option (APIv3) ExternalChannel: with APIv3, provide inputid, sourceid, chanid, freqid, atsc_major, atsc_minor, mplexid and recordid to external recorder. --- .../libmythtv/recorders/ExternalChannel.cpp | 80 +++++- .../recorders/ExternalStreamHandler.cpp | 174 ++++++++++- .../recorders/ExternalStreamHandler.h | 11 +- .../mythexternrecorder/MythExternControl.cpp | 269 +++++++++--------- .../mythexternrecorder/MythExternControl.h | 21 +- .../mythexternrecorder/MythExternRecApp.cpp | 178 ++++++++---- .../mythexternrecorder/MythExternRecApp.h | 17 +- 7 files changed, 536 insertions(+), 214 deletions(-) diff --git a/mythtv/libs/libmythtv/recorders/ExternalChannel.cpp b/mythtv/libs/libmythtv/recorders/ExternalChannel.cpp index 98c420ed9ef..661f005351a 100644 --- a/mythtv/libs/libmythtv/recorders/ExternalChannel.cpp +++ b/mythtv/libs/libmythtv/recorders/ExternalChannel.cpp @@ -117,16 +117,84 @@ bool ExternalChannel::Tune(const QString &channum) LOG(VB_CHANNEL, LOG_INFO, LOC + "Tuning to " + channum); - if (!m_streamHandler->ProcessCommand("TuneChannel:" + channum, - result, m_tuneTimeout)) + if (m_streamHandler->APIVersion() < 3) { - LOG(VB_CHANNEL, LOG_ERR, LOC + QString - ("Failed to Tune %1: %2").arg(channum, result)); - return false; + if (!m_streamHandler->ProcessCommand("TuneChannel:" + channum, + result, m_tuneTimeout)) + { + LOG(VB_CHANNEL, LOG_ERR, LOC + QString + ("Failed to Tune %1: %2").arg(channum, result)); + return false; + } + m_backgroundTuning = result.startsWith("OK:InProgress"); + } + else + { + QVariantMap cmd, vresult; + QByteArray response; + + cmd["command"] = "TuneChannel"; + cmd["channum"] = channum; + cmd["inputid"] = GetInputID(); + cmd["sourceid"] = m_sourceId; + + if (m_pParent) + { + ProgramInfo* prog = m_pParent->GetRecording(); + if (prog) + { + uint recordid = prog->GetRecordingRuleID(); + cmd["recordid"] = recordid; + } + } + + uint chanid = 0; + QString tvformat; + QString modulation; + QString freqtable; + QString freqid; + int finetune = 0; + uint64_t frequency = 0; + QString dtv_si_std; + int mpeg_prog_num = 0; + uint atsc_major = 0; + uint atsc_minor = 0; + uint dvb_transportid = 0; + uint dvb_networkid = 0; + uint mplexid = 0; + bool commfree = false; + + if (!ChannelUtil::GetChannelData(m_sourceId, chanid, channum, + tvformat, modulation, freqtable, freqid, + finetune, frequency, dtv_si_std, + mpeg_prog_num, atsc_major, atsc_minor, + dvb_transportid, dvb_networkid, + mplexid, commfree)) + { + LOG(VB_GENERAL, LOG_ERR, LOC + " " + + QString("Failed to find channel in DB on input '%1' ") + .arg(m_inputId)); + } + else + { + cmd["chanid"] = chanid; + cmd["freqid"] = freqid; + cmd["atsc_major"] = atsc_major; + cmd["atsc_minor"] = atsc_minor; + cmd["mplexid"] = mplexid; + } + + if (!m_streamHandler->ProcessJson(cmd, vresult, response)) + { + LOG(VB_CHANNEL, LOG_ERR, LOC + QString + ("Failed to Tune %1: %2").arg(channum, QString(response))); + return false; + } + m_backgroundTuning = vresult["message"] + .toString().startsWith("InProgress"); } UpdateDescription(); - m_backgroundTuning = result.startsWith("OK:InProgress"); return true; } diff --git a/mythtv/libs/libmythtv/recorders/ExternalStreamHandler.cpp b/mythtv/libs/libmythtv/recorders/ExternalStreamHandler.cpp index 74b5b006d1c..2d14a9e243b 100644 --- a/mythtv/libs/libmythtv/recorders/ExternalStreamHandler.cpp +++ b/mythtv/libs/libmythtv/recorders/ExternalStreamHandler.cpp @@ -19,6 +19,8 @@ // Qt headers #include #include +#include +#include // MythTV headers #include "config.h" @@ -168,7 +170,7 @@ int ExternIO::Read(QByteArray & buffer, int maxlen, std::chrono::milliseconds ti return len; } -QString ExternIO::GetStatus(std::chrono::milliseconds timeout) +QByteArray ExternIO::GetStatus(std::chrono::milliseconds timeout) { if (Error()) { @@ -194,7 +196,7 @@ QString ExternIO::GetStatus(std::chrono::milliseconds timeout) LOG(VB_RECORD, LOG_DEBUG, QString("ExternIO::GetStatus '%1'") .arg(msg)); - return msg; + return msg.toUtf8(); } int ExternIO::Write(const QByteArray & buffer) @@ -876,7 +878,7 @@ bool ExternalStreamHandler::SetAPIVersion(void) { LOG(VB_RECORD, LOG_ERR, LOC + QString("Bad response to 'APIVersion?' - '%1'. " - "Expecting 1 or 2").arg(result)); + "Expecting 1, 2 or 3").arg(result)); m_apiVersion = 1; } @@ -1234,6 +1236,23 @@ bool ExternalStreamHandler::ProcessCommand(const QString & cmd, { QMutexLocker locker(&m_processLock); + if (m_apiVersion == 3) + { + QVariantMap vcmd, vresult; + QByteArray response; + QStringList tokens = cmd.split(':'); + vcmd["command"] = tokens[0]; + if (tokens.size() > 1) + vcmd["value"] = tokens[1]; + + LOG(VB_RECORD, LOG_DEBUG, LOC + + QString("Arguments: %1").arg(tokens.join("\n"))); + + bool r = ProcessJson(vcmd, vresult, response, timeout, retry_cnt); + result = QString("%1:%2").arg(vresult["status"].toString()) + .arg(vresult["message"].toString()); + return r; + } if (m_apiVersion == 2) return ProcessVer2(cmd, result, timeout, retry_cnt); if (m_apiVersion == 1) @@ -1492,6 +1511,155 @@ bool ExternalStreamHandler::ProcessVer2(const QString & command, return false; } +bool ExternalStreamHandler::ProcessJson(const QVariantMap & vmsg, + QVariantMap & elements, + QByteArray & response, + std::chrono::milliseconds timeout, + uint retry_cnt) +{ + for (uint cnt = 0; cnt < retry_cnt; ++cnt) + { + QVariantMap query(vmsg); + + uint serial = ++m_serialNo; + query["serial"] = serial; + QString cmd = query["command"].toString(); + + QJsonDocument qdoc; + qdoc = QJsonDocument::fromVariant(query); + QByteArray cmdbuf = qdoc.toJson(QJsonDocument::Compact); + + LOG(VB_RECORD, LOG_DEBUG, LOC + + QString("ProcessJson: %1").arg(QString(cmdbuf))); + + if (m_io->Error()) + { + LOG(VB_GENERAL, LOG_ERR, LOC + "External Recorder in bad state: " + + m_io->ErrorString()); + return false; + } + + /* Send query */ + m_io->Write(cmdbuf); + m_io->Write("\n"); + + MythTimer timer(MythTimer::kStartRunning); + while (timer.elapsed() < timeout) + { + response = m_io->GetStatus(timeout); + if (m_io->Error()) + { + LOG(VB_GENERAL, LOG_ERR, LOC + + "Failed to read from External Recorder: " + + m_io->ErrorString()); + m_bError = true; + return false; + } + + if (!response.isEmpty()) + { + QJsonParseError parseError; + QJsonDocument doc; + + doc = QJsonDocument::fromJson(response, &parseError); + + if (parseError.error != QJsonParseError::NoError) + { + LOG(VB_GENERAL, LOG_ERR, LOC + + QString("ExternalRecorder returned invalid JSON message: %1: %2\n%3\nfor\n%4") + .arg(parseError.offset).arg(parseError.errorString()) + .arg(QString(response)) + .arg(QString(cmdbuf))); + } + else + { + elements = doc.toVariant().toMap(); + if (elements.find("serial") == elements.end()) + continue; + + serial = elements["serial"].toInt(); + if (serial >= m_serialNo) + break; + + if (elements.find("status") != elements.end() && + elements["status"] != "OK") + { + LOG(VB_RECORD, LOG_WARNING, LOC + QString("%1: %2") + .arg(elements["status"].toString()) + .arg(elements["message"].toString())); + } + } + } + } + + if (timer.elapsed() >= timeout) + { + LOG(VB_RECORD, LOG_ERR, LOC + + QString("ProcessJson: Giving up waiting for response for " + "command '%2'").arg(QString(cmdbuf))); + + } + + if (serial > m_serialNo) + { + LOG(VB_RECORD, LOG_ERR, LOC + + QString("ProcessJson: Looking for serial no %1, " + "but received %2 for command '%2'") + .arg(QString::number(m_serialNo)) + .arg(serial) + .arg(QString(cmdbuf))); + } + else if (elements.find("status") == elements.end()) + { + LOG(VB_RECORD, LOG_ERR, LOC + + QString("ProcessJson: ExternalRecorder 'status' not found in %1") + .arg(QString(response))); + } + else + { + QString status = elements["status"].toString(); + bool okay = (status == "OK"); + if (okay || status == "WARN" || status == "ERR") + { + LogLevel_t level = LOG_INFO; + + m_ioErrCnt = 0; + if (!okay) + level = LOG_WARNING; + else if (cmd == "SendBytes" || + (cmd == "TuneStatus" && + elements["message"] == "InProgress")) + level = LOG_DEBUG; + + LOG(VB_RECORD, level, + LOC + QString("ProcessJson('%1') = %2:%3:%4 took %5ms %6") + .arg(QString(cmdbuf)) + .arg(elements["serial"].toInt()) + .arg(elements["status"].toString()) + .arg(elements["message"].toString()) + .arg(QString::number(timer.elapsed().count())) + .arg(okay ? "" : "<-- NOTE") + ); + + return okay; + } + LOG(VB_GENERAL, LOG_WARNING, LOC + + QString("External Recorder invalid response to '%1': '%2'") + .arg(QString(cmdbuf)) + .arg(QString(response))); + } + + if (++m_ioErrCnt > 10) + { + LOG(VB_GENERAL, LOG_ERR, LOC + "Too many I/O errors."); + m_bError = true; + break; + } + } + + return false; +} + bool ExternalStreamHandler::CheckForError(void) { QString result; diff --git a/mythtv/libs/libmythtv/recorders/ExternalStreamHandler.h b/mythtv/libs/libmythtv/recorders/ExternalStreamHandler.h index d4a879fba9e..61bd31dd365 100644 --- a/mythtv/libs/libmythtv/recorders/ExternalStreamHandler.h +++ b/mythtv/libs/libmythtv/recorders/ExternalStreamHandler.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -35,7 +36,7 @@ class ExternIO bool Ready(int fd, std::chrono::milliseconds timeout, const QString & what); int Read(QByteArray & buffer, int maxlen, std::chrono::milliseconds timeout = 2500ms); - QString GetStatus(std::chrono::milliseconds timeout = 2500ms); + QByteArray GetStatus(std::chrono::milliseconds timeout = 2500ms); int Write(const QByteArray & buffer); bool Run(void); bool Error(void) const { return !m_error.isEmpty(); } @@ -67,7 +68,7 @@ class ExternIO class ExternalStreamHandler : public StreamHandler { - enum constants { MAX_API_VERSION = 2, + enum constants { MAX_API_VERSION = 3, TS_PACKET_SIZE = 188, PACKET_SIZE = TS_PACKET_SIZE * 8192, TOO_FAST_SIZE = TS_PACKET_SIZE * 32768 }; @@ -112,6 +113,12 @@ class ExternalStreamHandler : public StreamHandler std::chrono::milliseconds timeout, uint retry_cnt); bool ProcessVer2(const QString & command, QString & result, std::chrono::milliseconds timeout, uint retry_cnt); + bool ProcessJson(const QVariantMap & vmsg, + QVariantMap & result, + QByteArray & response, + std::chrono::milliseconds timeout = 4s, + uint retry_cnt = 3); + int APIVersion(void) const { return m_apiVersion; } private: int StreamingCount(void) const; diff --git a/mythtv/programs/mythexternrecorder/MythExternControl.cpp b/mythtv/programs/mythexternrecorder/MythExternControl.cpp index 44e06e93e78..2e41459019e 100644 --- a/mythtv/programs/mythexternrecorder/MythExternControl.cpp +++ b/mythtv/programs/mythexternrecorder/MythExternControl.cpp @@ -27,12 +27,14 @@ #include #include +#include +#include #include "libmythbase/mythlogging.h" using namespace std::chrono_literals; -const QString VERSION = "1.0"; +const QString VERSION = "2.0"; #define LOC Desc() @@ -110,12 +112,13 @@ void MythExternControl::Fatal(const QString & msg) Terminate(); } -Q_SLOT void MythExternControl::SendMessage(const QString & cmd, +Q_SLOT void MythExternControl::SendMessage(const QString & command, const QString & serial, - const QString & msg) + const QString & message, + const QString & status) { std::unique_lock lk(m_msgMutex); - m_commands.SendStatus(cmd, serial, msg); + m_commands.SendStatus(command, status, serial, message); } Q_SLOT void MythExternControl::ErrorMessage(const QString & msg) @@ -169,9 +172,9 @@ void Commands::SetBlockSize(const QString & serial, int blksz) emit m_parent->SetBlockSize(serial, blksz); } -void Commands::TuneChannel(const QString & serial, const QString & channum) +void Commands::TuneChannel(const QString & serial, const QVariantMap & args) { - emit m_parent->TuneChannel(serial, channum); + emit m_parent->TuneChannel(serial, args); } void Commands::TuneStatus(const QString & serial) @@ -199,234 +202,232 @@ void Commands::Cleanup(void) emit m_parent->Cleanup(); } -bool Commands::SendStatus(const QString & command, const QString & status) +bool Commands::SendStatus(const QString & command, + const QString & status, + const QString & serial, + const QString & response) { - int len = write(2, status.toUtf8().constData(), status.size()); + QJsonObject query; + if (!serial.isEmpty()) + query["serial"] = serial; + query["command"] = command; + query["status"] = status; + if (!response.isEmpty()) + query["message"] = response; + + QByteArray msgbuf = QJsonDocument(query).toJson(QJsonDocument::Compact); + int len = write(2, msgbuf.constData(), msgbuf.size()); len += write(2, "\n", 1); - if (len != status.size() + 1) + if (len != msgbuf.size() + 1) { LOG(VB_RECORD, LOG_ERR, LOC + QString("%1: Only wrote %2 of %3 bytes of message '%4'.") - .arg(command).arg(len).arg(status.size()).arg(status)); - return false; - } - - LOG(VB_RECORD, LOG_INFO, LOC + QString("Processing '%1' --> '%2'") - .arg(command, status)); - - m_parent->ClearError(); - return true; -} - -bool Commands::SendStatus(const QString & command, const QString & serial, - const QString & status) -{ - QString msg = QString("%1:%2").arg(serial, status); - - int len = write(2, msg.toUtf8().constData(), msg.size()); - len += write(2, "\n", 1); - - if (len != msg.size() + 1) - { - LOG(VB_RECORD, LOG_ERR, LOC + - QString("%1: Only wrote %2 of %3 bytes of message '%4'.") - .arg(command).arg(len).arg(msg.size()).arg(msg)); + .arg(command).arg(len).arg(msgbuf.size()).arg(QString(msgbuf))); return false; } if (!command.isEmpty()) { - LOG(VB_RECORD, LOG_INFO, LOC + QString("Processing '%1' --> '%2'") - .arg(command, msg)); + if (command == m_prev_cmd) + { + if (++m_rep_cmd_cnt % 25 == 0) + { + LOG(VB_RECORD, LOG_INFO, LOC + + QString("Processing '%1' --> '%2' (Repeated 25 times)") + .arg(command, QString(msgbuf))); + } + } + else + { + if (m_rep_cmd_cnt) + { + LOG(VB_RECORD, LOG_INFO, + LOC + QString("Processing '%1' (Repeated %2 times)") + .arg(m_prev_cmd).arg(m_rep_cmd_cnt % 25)); + m_rep_cmd_cnt = 0; + } + LOG(VB_RECORD, LOG_INFO, LOC + + QString("Processing '%1' --> '%2'") + .arg(command, QString(msgbuf))); + } + m_prev_cmd = command; } -#if 0 else - LOG(VB_RECORD, LOG_INFO, LOC + QString("%1").arg(msg)); -#endif + { + m_prev_cmd.clear(); + m_rep_cmd_cnt = 0; + } m_parent->ClearError(); return true; } -bool Commands::ProcessCommand(const QString & cmd) +bool Commands::ProcessCommand(const QString & query) { - LOG(VB_RECORD, LOG_DEBUG, LOC + QString("Processing '%1'").arg(cmd)); + LOG(VB_RECORD, LOG_DEBUG, LOC + QString("Processing '%1'").arg(query)); std::unique_lock lk1(m_parent->m_msgMutex); - if (cmd.startsWith("APIVersion?")) + if (query.startsWith("APIVersion?")) { - if (m_parent->m_fatal) - SendStatus(cmd, "ERR:" + m_parent->ErrorString()); - else - SendStatus(cmd, "OK:2"); + write(2, "OK:3\n", 5); return true; } - QStringList tokens = cmd.split(':', Qt::SkipEmptyParts); - if (tokens.size() < 2) + QJsonParseError parseError; + QJsonDocument doc; + QJsonObject jObj; + QString cmd; + QString serial; + QVariantMap elements; + QByteArray cmdbuf = query.toUtf8(); + + jObj = doc.object(); + doc = QJsonDocument::fromJson(cmdbuf, &parseError); + elements = doc.toVariant().toMap(); + + cmd = elements["command"].toString(); + serial = elements["serial"].toString(); + + if (parseError.error != QJsonParseError::NoError) { - SendStatus(cmd, "0", - QString("0:ERR:Version 2 API expects serial_no:msg format. " - "Saw '%1' instead").arg(cmd)); - return true; + SendStatus(query, "ERR", serial, + QString("ExternalRecorder sent invalid JSON message: %1: %2") + .arg(parseError.offset).arg(parseError.errorString())); + return false; } - - if (tokens[1].startsWith("APIVersion?")) + if (m_parent->m_fatal) { - if (m_parent->m_fatal) - SendStatus(cmd, tokens[0], "ERR:" + m_parent->ErrorString()); - else - SendStatus(cmd, tokens[0], "OK:2"); + SendStatus(query, "ERR", serial, m_parent->ErrorString()); + return false; } - else if (tokens[1].startsWith("APIVersion")) + + if (elements["command"] == "APIVersion") { - if (tokens.size() > 1) - { - m_apiVersion = tokens[2].toInt(); - SendStatus(cmd, tokens[0], QString("OK:%1").arg(m_apiVersion)); - } - else - SendStatus(cmd, tokens[0], "ERR:Missing API Version number"); + m_apiVersion = elements["value"].toInt(); + SendStatus(cmd, "OK", serial, QString::number(m_apiVersion)); } - else if (tokens[1].startsWith("Version?")) + else if (cmd == "Version?") { - if (m_parent->m_fatal) - SendStatus(cmd, tokens[0], "ERR:" + m_parent->ErrorString()); - else - SendStatus(cmd, tokens[0], "OK:" + VERSION); + SendStatus(cmd, "OK", serial, VERSION); } - else if (tokens[1].startsWith("Description?")) + else if (cmd == "Description?") { - if (m_parent->m_fatal) - SendStatus(cmd, tokens[0], "ERR:" + m_parent->ErrorString()); - else if (m_parent->m_desc.trimmed().isEmpty()) - SendStatus(cmd, tokens[0], "WARN:Not set"); + if (m_parent->m_desc.trimmed().isEmpty()) + SendStatus(cmd, "WARN", serial, "Not set"); else - SendStatus(cmd, tokens[0], "OK:" + m_parent->m_desc.trimmed()); + SendStatus(cmd, "OK", serial, m_parent->m_desc.trimmed()); } - else if (tokens[1].startsWith("HasLock?")) + else if (cmd == "HasLock?") { - if (m_parent->m_ready) - SendStatus(cmd, tokens[0], "OK:Yes"); - else - SendStatus(cmd, tokens[0], "OK:No"); + SendStatus(cmd, "OK", serial, m_parent->m_ready ? "Yes" : "No"); } - else if (tokens[1].startsWith("SignalStrengthPercent")) + else if (cmd == "SignalStrengthPercent?") { - if (m_parent->m_ready) - SendStatus(cmd, tokens[0], "OK:100"); - else - SendStatus(cmd, tokens[0], "OK:20"); + SendStatus(cmd, "OK", serial, m_parent->m_ready ? "100" : "20"); } - else if (tokens[1].startsWith("LockTimeout?")) + else if (cmd == "LockTimeout?") { - LockTimeout(tokens[0]); + LockTimeout(serial); } - else if (tokens[1].startsWith("HasTuner")) + else if (cmd == "HasTuner?") { - HasTuner(tokens[0]); + HasTuner(serial); } - else if (tokens[1].startsWith("HasPictureAttributes")) + else if (cmd == "HasPictureAttributes?") { - HasPictureAttributes(tokens[0]); + HasPictureAttributes(serial); } - else if (tokens[1].startsWith("SendBytes")) + else if (cmd == "SendBytes") { // Used when FlowControl is Polling - SendStatus(cmd, tokens[0], "ERR:Not supported"); + SendStatus(cmd, "ERR", serial, "Not supported"); } - else if (tokens[1].startsWith("XON")) + else if (cmd == "XON") { // Used when FlowControl is XON/XOFF if (m_parent->m_streaming) { - SendStatus(cmd, tokens[0], "OK"); + SendStatus(cmd, "OK", serial, "Started Streaming"); m_parent->m_xon = true; m_parent->m_flowCond.notify_all(); } else - SendStatus(cmd, tokens[0], "WARN:Not streaming"); + SendStatus(cmd, "Warn", serial, "Not Streaming"); } - else if (tokens[1].startsWith("XOFF")) + else if (cmd == "XOFF") { if (m_parent->m_streaming) { - SendStatus(cmd, tokens[0], "OK"); + SendStatus(cmd, "OK", serial, "Stopped Streaming"); // Used when FlowControl is XON/XOFF m_parent->m_xon = false; m_parent->m_flowCond.notify_all(); } else - SendStatus(cmd, tokens[0], "WARN:Not streaming"); + SendStatus(cmd, "Warn", serial, "Not Streaming"); } - else if (tokens[1].startsWith("TuneChannel")) + else if (cmd == "TuneChannel") { - if (tokens.size() > 2) - TuneChannel(tokens[0], tokens[2]); - else - SendStatus(cmd, tokens[0], "ERR:Missing channum"); + TuneChannel(serial, elements); } - else if (tokens[1].startsWith("TuneStatus?")) + else if (cmd == "TuneStatus?") { - TuneStatus(tokens[0]); + TuneStatus(serial); } - else if (tokens[1].startsWith("LoadChannels")) + else if (cmd == "LoadChannels") { - LoadChannels(tokens[0]); + LoadChannels(serial); } - else if (tokens[1].startsWith("FirstChannel")) + else if (cmd == "FirstChannel") { - FirstChannel(tokens[0]); + FirstChannel(serial); } - else if (tokens[1].startsWith("NextChannel")) + else if (cmd == "NextChannel") { - NextChannel(tokens[0]); + NextChannel(serial); } - else if (tokens[1].startsWith("IsOpen?")) + else if (cmd == "IsOpen?") { std::unique_lock lk2(m_parent->m_runMutex); - if (m_parent->m_fatal) - SendStatus(cmd, tokens[0], "ERR:" + m_parent->ErrorString()); - else if (m_parent->m_ready) - SendStatus(cmd, tokens[0], "OK:Open"); + if (m_parent->m_ready) + SendStatus(cmd, "OK", serial, "Open"); else - SendStatus(cmd, tokens[0], "WARN:Not Open yet"); + SendStatus(cmd, "WARN", serial, "Not Open yet"); } - else if (tokens[1].startsWith("CloseRecorder")) + else if (cmd == "CloseRecorder") { if (m_parent->m_streaming) - StopStreaming(tokens[0], true); + StopStreaming(serial, true); m_parent->Terminate(); - SendStatus(cmd, tokens[0], "OK:Terminating"); + SendStatus(cmd, "OK", serial, "Terminating"); Cleanup(); } - else if (tokens[1].startsWith("FlowControl?")) + else if (cmd == "FlowControl?") { - SendStatus(cmd, tokens[0], "OK:XON/XOFF"); + SendStatus(cmd, "OK", serial, "XON/XOFF"); } - else if (tokens[1].startsWith("BlockSize")) + else if (cmd == "BlockSize") { - if (tokens.size() > 1) - SetBlockSize(tokens[0], tokens[2].toUInt()); + if (elements.find("value") == elements.end()) + SendStatus(cmd, "ERR", serial, "Missing block size value"); else - SendStatus(cmd, tokens[0], "ERR:Missing block size"); + SetBlockSize(serial, elements["value"].toUInt()); } - else if (tokens[1].startsWith("StartStreaming")) + else if (cmd == "StartStreaming") { - StartStreaming(tokens[0]); + StartStreaming(serial); } - else if (tokens[1].startsWith("StopStreaming")) + else if (cmd == "StopStreaming") { /* This does not close the stream! When Myth is done with * this 'recording' ExternalChannel::EnterPowerSavingMode() * will be called, which invokes CloseRecorder() */ - StopStreaming(tokens[0], false); + StopStreaming(serial, false); } else - SendStatus(cmd, tokens[0], - QString("ERR:Unrecognized command '%1'").arg(tokens[1])); + SendStatus(cmd, "ERR", serial, QString("Unrecognized command '%1'").arg(query)); return true; } diff --git a/mythtv/programs/mythexternrecorder/MythExternControl.h b/mythtv/programs/mythexternrecorder/MythExternControl.h index 387bc73a6f3..6a2f64247ef 100644 --- a/mythtv/programs/mythexternrecorder/MythExternControl.h +++ b/mythtv/programs/mythexternrecorder/MythExternControl.h @@ -87,9 +87,10 @@ class Commands : public QObject m_thread.join(); } - bool SendStatus(const QString & command, const QString & status); - bool SendStatus(const QString & command, const QString & serial, - const QString & status); + bool SendStatus(const QString & command, + const QString & status, + const QString & serial, + const QString & response = ""); bool ProcessCommand(const QString & cmd); protected: @@ -102,7 +103,7 @@ class Commands : public QObject void HasTuner(const QString & serial) const; void HasPictureAttributes(const QString & serial) const; void SetBlockSize(const QString & serial, int blksz); - void TuneChannel(const QString & serial, const QString & channum); + void TuneChannel(const QString & serial, const QVariantMap & args); void TuneStatus(const QString & serial); void LoadChannels(const QString & serial); void FirstChannel(const QString & serial); @@ -112,6 +113,9 @@ class Commands : public QObject private: std::thread m_thread; + size_t m_rep_cmd_cnt { 0 }; + QString m_prev_cmd; + MythExternControl* m_parent { nullptr }; int m_apiVersion { -1 }; }; @@ -146,7 +150,7 @@ class MythExternControl : public QObject void HasTuner(const QString & serial); void HasPictureAttributes(const QString & serial); void SetBlockSize(const QString & serial, int blksz); - void TuneChannel(const QString & serial, const QString & channum); + void TuneChannel(const QString & serial, const QVariantMap & args); void TuneStatus(const QString & serial); void LoadChannels(const QString & serial); void FirstChannel(const QString & serial); @@ -156,8 +160,11 @@ class MythExternControl : public QObject public slots: void SetDescription(const QString & desc) { m_desc = desc; } - void SendMessage(const QString & cmd, const QString & serial, - const QString & msg); + void SendMessage(const QString & command, + const QString & serial, + const QString & message, + const QString & status = ""); + void ErrorMessage(const QString & msg); void Opened(void); void Done(void); diff --git a/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp b/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp index 04d5ee34a0d..4255de80941 100644 --- a/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp +++ b/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp @@ -68,6 +68,58 @@ MythExternRecApp::~MythExternRecApp(void) Close(); } +/* Remove any non-replaced variables along with any dependant strings. + Dependant strings are wrapped in {} */ +QString MythExternRecApp::sanitize_var(const QString & var) const +{ + qsizetype p1, p2; + QString cleaned = var; + + while ((p1 = cleaned.indexOf('{')) != -1) + { + p2 = cleaned.indexOf('}', p1); + if (cleaned.mid(p1, p2 - p1).indexOf('%') == -1) + { + // Just remove the '{' and '}' + cleaned = cleaned.remove(p2, 1); + cleaned = cleaned.remove(p1, 1); + } + else + { + // Remove the contents of { ... } + cleaned = cleaned.remove(p1, p2 - p1 + 1); + } + } + + LOG(VB_CHANNEL, LOG_DEBUG, QString("Sanitized: '%1' -> '%2'") + .arg(var).arg(cleaned)); + + return cleaned; +} + +QString MythExternRecApp::replace_extra_args(const QString & var, + const QVariantMap & extra_args) +{ + QString result = var; + /* + Replace any information provided in JSON message + */ + for (auto it = extra_args.keyValueBegin(); + it != extra_args.keyValueEnd(); ++it) + { + if (it->first == "command") + continue; + result.replace(QString("\%%1\%").arg(it->first.toUpper()), + it->second.toString()); + LOG(VB_CHANNEL, LOG_DEBUG, LOC + + QString("Replaced '%1' with '%2'") + .arg(it->first.toUpper()).arg(it->second.toString())); + } + result = sanitize_var(result); + + return result; +} + void MythExternRecApp::ReplaceVariables(QString & cmd) const { QMap::const_iterator Ivar; @@ -130,9 +182,27 @@ bool MythExternRecApp::config(void) .arg(var, settings.value(var).toString())); } settings.endGroup(); + + /* Replace defined VARs in the subsequently defined VARs */ + QMap::iterator Ivar, Ivar2; + for (Ivar = m_settingVars.begin(); + Ivar != m_settingVars.end(); ++Ivar) + { + QString repl = "%" + Ivar.key() + "%"; + Ivar2 = Ivar; + for (++Ivar2; Ivar2 != m_settingVars.end(); ++Ivar2) + { + if ((*Ivar2).indexOf(repl) >= 0) + { + LOG(VB_CHANNEL, LOG_DEBUG, QString("Replacing '%1' with '%2'") + .arg(repl, Ivar.value())); + (*Ivar2).replace(repl, Ivar.value()); + } + } + } } else - LOG(VB_CHANNEL, LOG_INFO, "No VARIABLES section"); + LOG(VB_CHANNEL, LOG_DEBUG, "No VARIABLES section"); if (!settings.contains("RECORDER/command")) { @@ -194,14 +264,14 @@ bool MythExternRecApp::Open(void) { if (m_fatal) { - emit SendMessage("Open", "0", m_fatalMsg); + emit SendMessage("Open", "0", m_fatalMsg, "ERR"); return false; } if (m_command.isEmpty()) { LOG(VB_RECORD, LOG_ERR, LOC + ": No recorder provided."); - emit SendMessage("Open", "0", "ERR:No recorder provided."); + emit SendMessage("Open", "0", "No recorder provided.", "ERR"); return false; } @@ -346,6 +416,8 @@ Q_SLOT void MythExternRecApp::Cleanup(void) LOG(VB_RECORD, LOG_WARNING, LOC + QString(" Beginning cleanup: '%1'").arg(cmd)); + cmd = replace_extra_args(cmd, m_chaninfo); + QProcess cleanup; cleanup.start(cmd, args); if (!cleanup.waitForStarted()) @@ -393,8 +465,7 @@ Q_SLOT void MythExternRecApp::DataStarted(void) if (startcmd.isEmpty()) return; - - startcmd.replace("%CHANNUM%", m_tunedChannel); + startcmd = replace_extra_args(startcmd, m_chaninfo); bool background = false; int pos = startcmd.lastIndexOf(QChar('&')); @@ -441,7 +512,7 @@ Q_SLOT void MythExternRecApp::LoadChannels(const QString & serial) if (m_channelsIni.isEmpty()) { LOG(VB_CHANNEL, LOG_ERR, LOC + ": No channels configured."); - emit SendMessage("LoadChannels", serial, "ERR:No channels configured."); + emit SendMessage("LoadChannels", serial, "No channels configured.", "ERR"); return; } @@ -460,8 +531,7 @@ Q_SLOT void MythExternRecApp::LoadChannels(const QString & serial) { QString errmsg = QString("Failed to start '%1': ").arg(cmd) + ENO; LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + errmsg); - emit SendMessage("LoadChannels", serial, - QString("ERR:%1").arg(errmsg)); + emit SendMessage("LoadChannels", serial, errmsg, "ERR"); return; } @@ -491,8 +561,7 @@ Q_SLOT void MythExternRecApp::LoadChannels(const QString & serial) { QString errmsg = QString("Timedout waiting for '%1'").arg(cmd); LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + errmsg); - emit SendMessage("LoadChannels", serial, - QString("ERR:%1").arg(errmsg)); + emit SendMessage("LoadChannels", serial, errmsg, "ERR"); return; } } @@ -502,8 +571,7 @@ Q_SLOT void MythExternRecApp::LoadChannels(const QString & serial) m_chanSettings->sync(); m_channels = m_chanSettings->childGroups(); - emit SendMessage("LoadChannels", serial, - QString("OK:%1").arg(m_channels.size())); + emit SendMessage("LoadChannels", serial, QString::number(m_channels.size()), "OK"); } void MythExternRecApp::GetChannel(const QString & serial, const QString & func) @@ -511,23 +579,21 @@ void MythExternRecApp::GetChannel(const QString & serial, const QString & func) if (m_channelsIni.isEmpty() || m_channels.empty()) { LOG(VB_CHANNEL, LOG_ERR, LOC + ": No channels configured."); - emit SendMessage("FirstChannel", serial, - QString("ERR:No channels configured.")); + emit SendMessage("FirstChannel", serial, "No channels configured.", "ERR"); return; } if (m_chanSettings == nullptr) { LOG(VB_CHANNEL, LOG_WARNING, LOC + ": Invalid channel configuration."); - emit SendMessage(func, serial, - "ERR:Invalid channel configuration."); + emit SendMessage(func, serial, "Invalid channel configuration.", "ERR"); return; } if (m_channels.size() <= m_channelIdx) { LOG(VB_CHANNEL, LOG_WARNING, LOC + ": No more channels."); - emit SendMessage(func, serial, "ERR:No more channels."); + emit SendMessage(func, serial, "No more channels", "ERR"); return; } @@ -546,9 +612,9 @@ void MythExternRecApp::GetChannel(const QString & serial, const QString & func) QString(": NextChannel Name:'%1',Callsign:'%2',xmltvid:%3,Icon:%4") .arg(name, callsign, xmltvid, icon)); - emit SendMessage(func, serial, QString("OK:%1,%2,%3,%4,%5") + emit SendMessage(func, serial, QString("%1,%2,%3,%4,%5") .arg(channum, name, callsign, - xmltvid, icon)); + xmltvid, icon), "OK"); } Q_SLOT void MythExternRecApp::FirstChannel(const QString & serial) @@ -562,7 +628,7 @@ Q_SLOT void MythExternRecApp::NextChannel(const QString & serial) GetChannel(serial, "NextChannel"); } -void MythExternRecApp::NewEpisodeStarting(const QString & channum) +void MythExternRecApp::NewEpisodeStarting(void) { QString cmd = m_newEpisodeCommand; int pos = cmd.lastIndexOf(QChar('&')); @@ -574,7 +640,7 @@ void MythExternRecApp::NewEpisodeStarting(const QString & channum) cmd = cmd.left(pos); } - cmd.replace("%CHANNUM%", channum); + cmd = replace_extra_args(cmd, m_chaninfo); QStringList args = MythCommandLineParser::MythSplitCommandString(cmd); cmd = args.takeFirst(); @@ -613,24 +679,28 @@ void MythExternRecApp::NewEpisodeStarting(const QString & channum) } Q_SLOT void MythExternRecApp::TuneChannel(const QString & serial, - const QString & channum) + const QVariantMap & chaninfo) { + m_chaninfo = chaninfo; + if (m_tuneCommand.isEmpty() && m_channelsIni.isEmpty()) { LOG(VB_CHANNEL, LOG_ERR, LOC + ": No 'tuner' configured."); - emit SendMessage("TuneChannel", serial, "ERR:No 'tuner' configured."); + emit SendMessage("TuneChannel", serial, "No 'tuner' configured.", "ERR"); return; } + QString channum = m_chaninfo["channum"].toString(); + if (m_tunedChannel == channum) { if (!m_newEpisodeCommand.isEmpty()) - NewEpisodeStarting(channum); + NewEpisodeStarting(); LOG(VB_CHANNEL, LOG_INFO, LOC + QString("TuneChannel: Already on %1").arg(channum)); emit SendMessage("TuneChannel", serial, - QString("OK:Tunned to %1").arg(channum)); + QString("Tunned to %1").arg(channum), "OK"); return; } @@ -692,8 +762,7 @@ Q_SLOT void MythExternRecApp::TuneChannel(const QString & serial, tunecmd = tunecmd.left(pos); } - tunecmd.replace("%CHANNUM%", channum); - m_command.replace("%CHANNUM%", channum); + tunecmd = replace_extra_args(tunecmd, m_chaninfo); if (!m_logFile.isEmpty() && m_command.indexOf("%LOGFILE%") >= 0) { @@ -712,7 +781,6 @@ Q_SLOT void MythExternRecApp::TuneChannel(const QString & serial, } m_desc.replace("%URL%", url); - m_desc.replace("%CHANNUM%", channum); if (!tunecmd.isEmpty()) { @@ -724,8 +792,7 @@ Q_SLOT void MythExternRecApp::TuneChannel(const QString & serial, { QString errmsg = QString("Tune `%1` failed: ").arg(tunecmd) + ENO; LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + errmsg); - emit SendMessage("TuneChannel", serial, - QString("ERR:%1").arg(errmsg)); + emit SendMessage("TuneChannel", serial, errmsg, "ERR"); return; } @@ -739,14 +806,14 @@ Q_SLOT void MythExternRecApp::TuneChannel(const QString & serial, m_tuningChannel.clear(); emit SetDescription(Desc()); emit SendMessage("TuneChannel", serial, - QString("OK:Tuned `%1`").arg(m_tunedChannel)); + QString("Tuned `%1`").arg(m_tunedChannel), "OK"); } else { LOG(VB_CHANNEL, LOG_INFO, LOC + QString(": Started `%1` URL '%2'") .arg(tunecmd, url)); emit SendMessage("TuneChannel", serial, - QString("OK:InProgress `%1`").arg(tunecmd)); + QString("InProgress `%1`").arg(tunecmd), "OK"); } } else @@ -754,7 +821,7 @@ Q_SLOT void MythExternRecApp::TuneChannel(const QString & serial, m_tunedChannel = channum; emit SetDescription(Desc()); emit SendMessage("TuneChannel", serial, - QString("OK:Tuned to %1").arg(m_tunedChannel)); + QString("Tuned to %1").arg(m_tunedChannel), "OK"); } } @@ -762,9 +829,9 @@ Q_SLOT void MythExternRecApp::TuneStatus(const QString & serial) { if (m_tuneProc.state() == QProcess::Running) { - LOG(VB_CHANNEL, LOG_INFO, LOC + + LOG(VB_CHANNEL, LOG_DEBUG, LOC + QString(": Tune process(%1) still running").arg(m_tuneProc.processId())); - emit SendMessage("TuneStatus", serial, "OK:InProgress"); + emit SendMessage("TuneStatus", serial, "InProgress", "OK"); return; } @@ -774,8 +841,7 @@ Q_SLOT void MythExternRecApp::TuneStatus(const QString & serial) QString errmsg = QString("'%1' failed: ") .arg(m_tuneProc.program()) + ENO; LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + errmsg); - emit SendMessage("TuneStatus", serial, - QString("ERR:%1").arg(errmsg)); + emit SendMessage("TuneStatus", serial, errmsg, "WARN"); return; } @@ -785,7 +851,7 @@ Q_SLOT void MythExternRecApp::TuneStatus(const QString & serial) LOG(VB_CHANNEL, LOG_INFO, LOC + QString(": Tuned %1").arg(m_tunedChannel)); emit SetDescription(Desc()); emit SendMessage("TuneChannel", serial, - QString("OK:Tuned to %1").arg(m_tunedChannel)); + QString("Tuned to %1").arg(m_tunedChannel), "OK"); } Q_SLOT void MythExternRecApp::LockTimeout(const QString & serial) @@ -794,7 +860,7 @@ Q_SLOT void MythExternRecApp::LockTimeout(const QString & serial) { LOG(VB_CHANNEL, LOG_WARNING, LOC + "Cannot read LockTimeout from config file."); - emit SendMessage("LockTimeout", serial, "ERR: Not open"); + emit SendMessage("LockTimeout", serial, "Not open", "ERR"); return; } @@ -802,32 +868,30 @@ Q_SLOT void MythExternRecApp::LockTimeout(const QString & serial) { LOG(VB_CHANNEL, LOG_INFO, LOC + QString("Using configured LockTimeout of %1").arg(m_lockTimeout)); - emit SendMessage("LockTimeout", serial, - QString("OK:%1").arg(m_lockTimeout)); + emit SendMessage("LockTimeout", serial, QString::number(m_lockTimeout), "OK"); return; } LOG(VB_CHANNEL, LOG_INFO, LOC + "No LockTimeout defined in config, defaulting to 12000ms"); - emit SendMessage("LockTimeout", serial, QString("OK:%1") - .arg(m_scanCommand.isEmpty() ? 12000 : 120000)); + emit SendMessage("LockTimeout", serial, + m_scanCommand.isEmpty() ? "12000" : "120000", "OK"); } Q_SLOT void MythExternRecApp::HasTuner(const QString & serial) { - emit SendMessage("HasTuner", serial, QString("OK:%1") - .arg(m_tuneCommand.isEmpty() && - m_channelsIni.isEmpty() ? "No" : "Yes")); + emit SendMessage("HasTuner", serial, m_tuneCommand.isEmpty() && + m_channelsIni.isEmpty() ? "No" : "Yes", "OK"); } Q_SLOT void MythExternRecApp::HasPictureAttributes(const QString & serial) { - emit SendMessage("HasPictureAttributes", serial, "OK:No"); + emit SendMessage("HasPictureAttributes", serial, "No", "OK"); } Q_SLOT void MythExternRecApp::SetBlockSize(const QString & serial, int blksz) { m_blockSize = blksz; - emit SendMessage("BlockSize", serial, "OK"); + emit SendMessage("BlockSize", serial, QString("Blocksize %1").arg(blksz), "OK"); } Q_SLOT void MythExternRecApp::StartStreaming(const QString & serial) @@ -837,7 +901,7 @@ Q_SLOT void MythExternRecApp::StartStreaming(const QString & serial) { LOG(VB_RECORD, LOG_ERR, LOC + ": No channel has been tuned"); emit SendMessage("StartStreaming", serial, - "ERR:No channel has been tuned"); + "No channel has been tuned", "ERR"); return; } @@ -845,7 +909,7 @@ Q_SLOT void MythExternRecApp::StartStreaming(const QString & serial) { LOG(VB_RECORD, LOG_ERR, LOC + ": Application already running"); emit SendMessage("StartStreaming", serial, - "WARN:Application already running"); + "Application already running", "WARN"); return; } @@ -864,7 +928,7 @@ Q_SLOT void MythExternRecApp::StartStreaming(const QString & serial) { LOG(VB_RECORD, LOG_ERR, LOC + ": Failed to start application."); emit SendMessage("StartStreaming", serial, - "ERR:Failed to start application."); + "Failed to start application.", "ERR"); return; } @@ -874,7 +938,7 @@ Q_SLOT void MythExternRecApp::StartStreaming(const QString & serial) { LOG(VB_RECORD, LOG_ERR, LOC + ": Application failed to start"); emit SendMessage("StartStreaming", serial, - "ERR:Application failed to start"); + "Application failed to start", "ERR"); return; } @@ -883,7 +947,7 @@ Q_SLOT void MythExternRecApp::StartStreaming(const QString & serial) emit Streaming(true); emit SetDescription(Desc()); - emit SendMessage("StartStreaming", serial, "OK:Streaming Started"); + emit SendMessage("StartStreaming", serial, "Streaming Started", "OK"); } Q_SLOT void MythExternRecApp::StopStreaming(const QString & serial, bool silent) @@ -895,21 +959,21 @@ Q_SLOT void MythExternRecApp::StopStreaming(const QString & serial, bool silent) LOG(VB_RECORD, LOG_INFO, LOC + ": External application terminated."); if (silent) - emit SendMessage("StopStreaming", serial, "STATUS:Streaming Stopped"); + emit SendMessage("StopStreaming", serial, "Streaming Stopped", "STATUS"); else - emit SendMessage("StopStreaming", serial, "OK:Streaming Stopped"); + emit SendMessage("StopStreaming", serial, "Streaming Stopped", "OK"); } else { if (silent) { emit SendMessage("StopStreaming", serial, - "STATUS:Already not Streaming"); + "Already not Streaming", "STATUS"); } else { emit SendMessage("StopStreaming", serial, - "WARN:Already not Streaming"); + "Already not Streaming", "WARN"); } } diff --git a/mythtv/programs/mythexternrecorder/MythExternRecApp.h b/mythtv/programs/mythexternrecorder/MythExternRecApp.h index 9c947b7779d..c149e749b78 100644 --- a/mythtv/programs/mythexternrecorder/MythExternRecApp.h +++ b/mythtv/programs/mythexternrecorder/MythExternRecApp.h @@ -27,6 +27,7 @@ #include #include #include +#include #include #include @@ -48,13 +49,15 @@ class MythExternRecApp : public QObject void ReplaceVariables(QString & cmd) const; QString Desc(void) const; void MythLog(const QString & msg) - { emit SendMessage("", "0", QString("STATUS:%1").arg(msg)); } + { emit SendMessage("", "0", "STATUS", msg); } void SetErrorMsg(const QString & msg) { emit ErrorMessage(msg); } signals: void SetDescription(const QString & desc); - void SendMessage(const QString & func, const QString & serial, - const QString & msg); + void SendMessage(const QString & command, + const QString & serial, + const QString & message, + const QString & status = ""); void ErrorMessage(const QString & msg); void Opened(void); void Done(void); @@ -80,8 +83,8 @@ class MythExternRecApp : public QObject void FirstChannel(const QString & serial); void NextChannel(const QString & serial); - void NewEpisodeStarting(const QString & channum); - void TuneChannel(const QString & serial, const QString & channum); + void NewEpisodeStarting(void); + void TuneChannel(const QString & serial, const QVariantMap & args); void TuneStatus(const QString & serial); void HasPictureAttributes(const QString & serial); void SetBlockSize(const QString & serial, int blksz); @@ -92,6 +95,9 @@ class MythExternRecApp : public QObject private: bool config(void); + QString sanitize_var(const QString & var) const; + QString replace_extra_args(const QString & var, + const QVariantMap & extra_args); bool m_fatal { false }; QString m_fatalMsg; @@ -114,6 +120,7 @@ class MythExternRecApp : public QObject QMap m_appEnv; QMap m_settingVars; + QVariantMap m_chaninfo; QProcess m_tuneProc; QProcess m_finishTuneProc;