8#include <QCoreApplication>
9#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
10#include <QStringConverter>
23#define LOC QString("UPnPScan: ")
24#define ERR QString("UPnPScan error: ")
39 for (
const auto& value : std::as_const(
m_children))
41 QString result = value.NextUnbrowsed();
42 if (!result.isEmpty())
199 auto *me =
new MythEvent(QString(
"UPNP_STARTSCAN"));
200 qApp->postEvent(
this, me);
214 if (servers.isEmpty())
218 LOG(VB_GENERAL, LOG_INFO, QString(
"Adding MediaServer metadata."));
226 if (!it.value()->m_subscribed)
229 const QString& usn = it.key();
244 if (servers.isEmpty())
251 LOG(VB_GENERAL, LOG_INFO,
LOC +
"Waiting for scan to complete.");
255 std::this_thread::sleep_for(100ms);
259 LOG(VB_GENERAL, LOG_ERR,
LOC +
"MediaServer scan is incomplete.");
261 LOG(VB_GENERAL, LOG_INFO,
LOC +
"MediaServer scanning finished.");
270 if (!it.value()->m_subscribed)
273 const QString& usn = it.key();
282 if (!data.canConvert<QStringList>())
285 QStringList list = data.toStringList();
286 if (list.size() != 2)
289 const QString& usn = list[0];
290 QString
object = list[1];
303 auto *me =
new MythEvent(
"UPNP_BROWSEOBJECT", list);
304 qApp->postEvent(
this, me);
308 LOG(VB_GENERAL, LOG_INFO,
"START");
309 while (!found && (count++ < 100))
311 std::this_thread::sleep_for(100ms);
322 LOG(VB_GENERAL, LOG_INFO, QString(
"Item went away..."));
328 LOG(VB_GENERAL, LOG_INFO,
329 QString(
"Server went away while browsing."));
334 LOG(VB_GENERAL, LOG_INFO,
"END");
358 item->SetTitle(QString(
"Dummy"));
359 list->push_back(item);
369 for (
const auto& child : std::as_const(
content->m_children))
375 item->SetTitle(
content->m_name);
376 list->push_back(item);
387 QMap<QString,QString> servers;
390 servers.insert(it.key(), it.value()->m_friendlyName);
406 connect(
m_network, &QNetworkAccessManager::finished,
431 LOG(VB_GENERAL, LOG_INFO,
LOC +
"Started");
458 if (it.value()->m_renewalTimerId)
459 killTimer(it.value()->m_renewalTimerId);
487 LOG(VB_GENERAL, LOG_INFO,
LOC +
"Finished");
506 bool reschedule =
false;
508 for (
auto *server : std::as_const(
m_servers))
511 (server->m_controlURL.isEmpty()))
514 QUrl url { server->m_serverURL };
519 QNetworkReply *reply =
m_network->get(QNetworkRequest(url));
524 server->m_connectionAttempts++;
552 LOG(VB_UPNP, LOG_INFO,
LOC + QString(
"%1 no longer in SSDP cache. Removing")
553 .arg(it.value()->m_serverURL.toString()));
576 QUrl url = reply->url();
577 bool valid = reply->error() == QNetworkReply::NoError;
581 LOG(VB_UPNP, LOG_ERR,
LOC +
582 QString(
"Network request for '%1' returned error '%2'")
583 .arg(url.toString(), reply->errorString()));
586 bool description =
false;
608 else if (description)
620 LOG(VB_UPNP, LOG_ERR,
LOC +
"Received unknown reply");
623 reply->deleteLater();
635 auto *me =
dynamic_cast<MythEvent *
>(event);
639 const QString& ev = me->
Message();
641 if (ev ==
"UPNP_STARTSCAN")
646 if (ev ==
"UPNP_BROWSEOBJECT")
648 if (me->ExtraDataCount() == 2)
651 const QString& usn = me->ExtraData(0);
652 const QString& objectid = me->ExtraData(1);
657 LOG(VB_GENERAL, LOG_INFO, QString(
"UPNP_BROWSEOBJECT: %1->%2")
658 .arg(
m_servers[usn]->m_friendlyName, objectid));
666 if (ev ==
"UPNP_EVENT")
671 if (!
info->GetInfoMap())
674 QString usn =
info->GetInfoMap()->value(
"usn");
675 QString
id =
info->GetInfoMap()->value(
"SystemUpdateID");
676 if (usn.isEmpty() ||
id.isEmpty())
682 int newid =
id.toInt();
683 if (
m_servers[usn]->m_systemUpdateID != newid)
686 LOG(VB_GENERAL, LOG_INFO,
LOC +
687 QString(
"New SystemUpdateID '%1' for %2").arg(
id, usn));
696 QString uri = me->ExtraDataCount() > 0 ? me->ExtraData(0) : QString();
697 QString usn = me->ExtraDataCount() > 1 ? me->ExtraData(1) : QString();
700 if (uri ==
"urn:schemas-upnp-org:device:MediaServer:1")
702 QString url = (ev ==
"SSDP_ADD") ? me->ExtraData(2) : QString();
713 int id =
event->timerId();
717 std::chrono::seconds
timeout = 0s;
723 if (it.value()->m_renewalTimerId ==
id)
725 it.value()->m_renewalTimerId = 0;
736 LOG(VB_GENERAL, LOG_INFO,
LOC +
737 QString(
"Re-subscribed for %1 seconds to %2")
738 .arg(
timeout.count()).arg(usn));
760 for (
auto *server : std::as_const(
m_servers))
762 if (server->m_serverURL == url && server->m_connectionAttempts ==
MAX_ATTEMPTS)
777 LOG(VB_UPNP, LOG_INFO,
LOC + QString(
"%1 media servers discovered:")
779 for (
auto *server: std::as_const(
m_servers))
781 QString status =
"Probing";
782 if (server->m_controlURL.toString().isEmpty())
791 LOG(VB_UPNP, LOG_INFO,
LOC +
792 QString(
"'%1' Connected: %2 Subscribed: %3 SystemUpdateID: "
794 .arg(server->m_friendlyName, status,
795 server->m_subscribed ?
"Yes" :
"No",
796 QString::number(server->m_systemUpdateID),
797 QString::number(server->m_renewalTimerId)));
812 QMutexLocker locker(&
m_lock);
814 bool complete =
true;
815 for (
auto *server : std::as_const(
m_servers))
817 if (server->m_subscribed)
826 QString next = server->NextUnbrowsed();
834 LOG(VB_UPNP, LOG_INFO,
LOC + QString(
"Scan completed for %1")
835 .arg(server->m_friendlyName));
841 LOG(VB_GENERAL, LOG_INFO,
LOC +
842 QString(
"Media Server scan is complete."));
855 QNetworkRequest req = QNetworkRequest(url);
856 req.setRawHeader(
"CONTENT-TYPE",
"text/xml; charset=\"utf-8\"");
857 req.setRawHeader(
"SOAPACTION",
858 "\"urn:schemas-upnp-org:service:ContentDirectory:1#Browse\"");
860 req.setRawHeader(
"MAN",
"\"http://schemasxmlsoap.org/soap/envelope/\"");
861 req.setRawHeader(
"01-SOAPACTION",
862 "\"urn:schemas-upnp-org:service:ContentDirectory:1#Browse\"");
866 QTextStream data(&body);
867#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
868 data.setCodec(QTextCodec::codecForName(
"UTF-8"));
870 data.setEncoding(QStringConverter::Utf8);
872 data <<
"<?xml version=\"1.0\"?>\r\n";
873 data <<
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n";
874 data <<
" <s:Body>\r\n";
875 data <<
" <u:Browse xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">\r\n";
876 data <<
" <ObjectID>" << objectid.toUtf8() <<
"</ObjectID>\r\n";
877 data <<
" <BrowseFlag>BrowseDirectChildren</BrowseFlag>\r\n";
878 data <<
" <Filter>*</Filter>\r\n";
879 data <<
" <StartingIndex>0</StartingIndex>\r\n";
880 data <<
" <RequestedCount>0</RequestedCount>\r\n";
881 data <<
" <SortCriteria></SortCriteria>\r\n";
882 data <<
" </u:Browse>\r\n";
883 data <<
" </s:Body>\r\n";
884 data <<
"</s:Envelope>\r\n";
888 QNetworkReply *reply =
m_network->post(req, body);
917 LOG(VB_UPNP, LOG_INFO,
LOC +
"Ignoring invalid url: " + url);
922 LOG(VB_UPNP, LOG_INFO,
LOC +
"Ignoring master backend.");
930 LOG(VB_GENERAL, LOG_INFO,
LOC + QString(
"Adding: %1").arg(usn));
944 LOG(VB_GENERAL, LOG_INFO,
LOC + QString(
"Removing: %1").arg(usn));
964 std::chrono::seconds twelvehours { 12h };
969 m_servers[usn]->m_renewalTimerId = startTimer(renew);
979 QByteArray data = reply->readAll();
984 auto *parent =
new QDomDocument();
985#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
986 QString errorMessage;
989 if (!parent->setContent(data,
false, &errorMessage, &errorLine,
992 LOG(VB_GENERAL, LOG_ERR,
LOC +
993 QString(
"DIDL Parse error, Line: %1 Col: %2 Error: '%3'")
994 .arg(errorLine).arg(errorColumn).arg(errorMessage));
999 auto parseResult = parent->setContent(data);
1002 LOG(VB_GENERAL, LOG_ERR,
LOC +
1003 QString(
"DIDL Parse error, Line: %1 Col: %2 Error: '%3'")
1004 .arg(parseResult.errorLine).arg(parseResult.errorColumn)
1005 .arg(parseResult.errorMessage));
1011 LOG(VB_UPNP, LOG_INFO,
"\n\n" + parent->toString(4) +
"\n\n");
1014 QDomDocument *result =
nullptr;
1018 QDomElement docElem = parent->documentElement();
1019 QDomNode n = docElem.firstChild();
1021 result =
FindResult(n, num, total, updateid);
1024 if (!result || num < 1 || total < 1)
1026 LOG(VB_GENERAL, LOG_ERR,
LOC +
1027 QString(
"Failed to find result for %1") .arg(url.toString()));
1038 server = it.value();
1044 LOG(VB_GENERAL, LOG_ERR,
LOC +
1045 QString(
"Received unknown response for %1").arg(url.toString()));
1054 LOG(VB_GENERAL, LOG_ERR,
LOC +
1055 QString(
"%1 updateID changed during browse (old %2 new %3)")
1065 docElem = result->documentElement();
1066 n = docElem.firstChild();
1070 n = n.nextSibling();
1080 QDomElement node = n.toElement();
1084 if (node.tagName() ==
"container")
1086 QString title =
"ERROR";
1087 QDomNode next = node.firstChild();
1088 while (!next.isNull())
1090 QDomElement container = next.toElement();
1091 if (!container.isNull() && container.tagName() ==
"title")
1092 title = container.text();
1093 next = next.nextSibling();
1096 QString thisid = node.attribute(
"id",
"ERROR");
1097 QString parentid = node.attribute(
"parentID",
"ERROR");
1106 resetparent =
false;
1109 parent->
Add(container);
1114 if (node.tagName() ==
"item")
1116 QString title =
"ERROR";
1117 QString url =
"ERROR";
1118 QDomNode next = node.firstChild();
1119 while (!next.isNull())
1121 QDomElement item = next.toElement();
1124 if(item.tagName() ==
"res")
1126 if(item.tagName() ==
"title")
1127 title = item.text();
1129 next = next.nextSibling();
1132 QString thisid = node.attribute(
"id",
"ERROR");
1133 QString parentid = node.attribute(
"parentID",
"ERROR");
1143 resetparent =
false;
1151 QDomNode next = node.firstChild();
1152 while (!next.isNull())
1155 next = next.nextSibling();
1162 QDomDocument *result =
nullptr;
1163 QDomElement node = n.toElement();
1167 if (node.tagName() ==
"NumberReturned")
1168 num = node.text().toUInt();
1169 if (node.tagName() ==
"TotalMatches")
1170 total = node.text().toUInt();
1171 if (node.tagName() ==
"UpdateID")
1172 updateid = node.text().toUInt();
1173 if (node.tagName() ==
"Result" && !result)
1175 result =
new QDomDocument();
1176#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
1177 QString errorMessage;
1179 int errorColumn = 0;
1180 if (!result->setContent(node.text(),
true, &errorMessage, &errorLine, &errorColumn))
1182 LOG(VB_GENERAL, LOG_ERR,
LOC +
1183 QString(
"DIDL Parse error, Line: %1 Col: %2 Error: '%3'")
1184 .arg(errorLine).arg(errorColumn).arg(errorMessage));
1190 result->setContent(node.text(),
1191 QDomDocument::ParseOption::UseNamespaceProcessing);
1194 LOG(VB_GENERAL, LOG_ERR,
LOC +
1195 QString(
"DIDL Parse error, Line: %1 Col: %2 Error: '%3'")
1196 .arg(parseResult.errorLine).arg(parseResult.errorColumn)
1197 .arg(parseResult.errorMessage));
1204 QDomNode next = node.firstChild();
1205 while (!next.isNull())
1207 QDomDocument *res =
FindResult(next, num, total, updateid);
1210 next = next.nextSibling();
1217 base.setFragment(
"");
1219 base.setPath(relative.path());
1229 if (url.isEmpty() || !reply)
1232 QByteArray data = reply->readAll();
1235 LOG(VB_GENERAL, LOG_ERR,
LOC +
1236 QString(
"%1 returned an empty device description.")
1237 .arg(url.toString()));
1245 QString friendlyName = QString(
"Unknown");
1248#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
1249 QString errorMessage;
1251 int errorColumn = 0;
1252 if (!doc.setContent(data,
false, &errorMessage, &errorLine, &errorColumn))
1254 LOG(VB_GENERAL, LOG_ERR,
LOC +
1255 QString(
"Failed to parse device description from %1")
1256 .arg(url.toString()));
1257 LOG(VB_GENERAL, LOG_ERR,
LOC + QString(
"Line: %1 Col: %2 Error: '%3'")
1258 .arg(errorLine).arg(errorColumn).arg(errorMessage));
1262 auto parseResult = doc.setContent(data);
1265 LOG(VB_GENERAL, LOG_ERR,
LOC +
1266 QString(
"Failed to parse device description from %1")
1267 .arg(url.toString()));
1268 LOG(VB_GENERAL, LOG_ERR,
LOC + QString(
"Line: %1 Col: %2 Error: '%3'")
1269 .arg(parseResult.errorLine).arg(parseResult.errorColumn)
1270 .arg(parseResult.errorMessage));
1275 QDomElement docElem = doc.documentElement();
1276 QDomNode n = docElem.firstChild();
1279 QDomElement e1 = n.toElement();
1282 if(e1.tagName() ==
"device")
1283 ParseDevice(e1, controlURL, eventURL, friendlyName);
1284 if (e1.tagName() ==
"URLBase")
1285 URLBase = QUrl(e1.text());
1287 n = n.nextSibling();
1290 if (!controlURL.isValid())
1292 LOG(VB_UPNP, LOG_ERR,
LOC +
1293 QString(
"Failed to parse device description for %1")
1294 .arg(url.toString()));
1299 if (!URLBase.isValid())
1302 URLBase.setPath(
"");
1303 URLBase.setFragment(
"");
1304 URLBase.setQuery(
"");
1307 if (controlURL.isRelative())
1309 if (eventURL.isRelative())
1312 LOG(VB_UPNP, LOG_INFO,
LOC + QString(
"Control URL for %1 at %2")
1313 .arg(friendlyName, controlURL.toString()));
1314 LOG(VB_UPNP, LOG_INFO,
LOC + QString(
"Event URL for %1 at %2")
1315 .arg(friendlyName, eventURL.toString()));
1320 std::chrono::seconds
timeout = 0s;
1324 [&url](
UpnpMediaServer* server){ return url == server->m_serverURL;} );
1333 server->
m_name = friendlyName;
1345 LOG(VB_GENERAL, LOG_INFO,
LOC +
1346 QString(
"Subscribed for %1 seconds to %2") .arg(
timeout.count()).arg(usn));
1359 QUrl &eventURL, QString &friendlyName)
1361 QDomNode dev = element.firstChild();
1362 while (!dev.isNull())
1364 QDomElement e = dev.toElement();
1367 if (e.tagName() ==
"friendlyName")
1368 friendlyName = e.text();
1369 if (e.tagName() ==
"serviceList")
1372 dev = dev.nextSibling();
1379 QDomNode list = element.firstChild();
1380 while (!list.isNull())
1382 QDomElement e = list.toElement();
1384 if (e.tagName() ==
"service")
1386 list = list.nextSibling();
1396 QDomNode service = element.firstChild();
1398 while (!service.isNull())
1400 QDomElement e = service.toElement();
1403 if (e.tagName() ==
"serviceType")
1405 iscds = (e.text() ==
"urn:schemas-upnp-org:service:ContentDirectory:1");
1406 if (e.tagName() ==
"controlURL")
1407 control_url = QUrl(e.text());
1408 if (e.tagName() ==
"eventSubURL")
1409 event_url = QUrl(e.text());
1411 service = service.nextSibling();
1416 controlURL = control_url;
1417 eventURL = event_url;
This is a wrapper around QThread that does several additional things.
bool isRunning(void) const
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
void quit(void)
calls exit(0)
bool wait(std::chrono::milliseconds time=std::chrono::milliseconds::max())
Wait for the MThread to exit, with a maximum timeout.
QThread * qthread(void)
Returns the thread, this will always return the same pointer no matter how often you restart the thre...
QString GetMasterServerIP(void)
Returns the Master Backend IP address If the address is an IPv6 address, the scope Id is removed.
int GetMasterServerStatusPort(void)
Returns the Master Backend status port If no master server status port has been defined in the databa...
This class is used as a container for messages.
const QString & Message() const
static const Type kMythEventMessage
void addListener(QObject *listener)
Add a listener to the observable.
void removeListener(QObject *listener)
Remove a listener to the observable.
static SSDPCache * Instance()
UPnPScanner detects UPnP Media Servers available on the local network (via the UPnP SSDP cache),...
static UPNPScanner * Instance(UPNPSubscription *sub=nullptr)
Returns the global UPNPScanner instance if it has been enabled or nullptr if UPNPScanner is currently...
static void ParseService(QDomElement &element, QUrl &controlURL, QUrl &eventURL)
void replyFinished(QNetworkReply *reply)
Validates network responses against known requests and parses expected responses for the required dat...
static QRecursiveMutex * gUPNPScannerLock
static bool gUPNPScannerEnabled
void CheckStatus(void)
Removes media servers that can no longer be found in the SSDP cache.
void AddServer(const QString &usn, const QString &url)
Adds the server identified by usn and reachable via url to the list of known media servers and schedu...
void FindItems(const QDomNode &n, MediaServerItem &content, bool &resetparent)
QMultiMap< QUrl, QNetworkReply * > m_descriptionRequests
void RemoveServer(const QString &usn)
QHash< QString, UpnpMediaServer * > m_servers
static void ParseServiceList(QDomElement &element, QUrl &controlURL, QUrl &eventURL)
bool ParseDescription(const QUrl &url, QNetworkReply *reply)
Parse the device description XML return my a media server.
void Start()
Initialises the scanner, hooks it up to the subscription service and the SSDP cache and starts scanni...
void ScheduleRenewal(const QString &usn, std::chrono::seconds timeout)
Creates a QTimer to trigger a subscription renewal for a given media server.
QMap< QString, QString > ServerList(void)
Returns a list of valid Media Servers discovered on the network.
static MThread * gUPNPScannerThread
void GetMetadata(VideoMetadataListManager::metadata_list *list, meta_dir_node *node)
Fill the given metadata_list and meta_dir_node with the metadata of content retrieved from known medi...
QNetworkAccessManager * m_network
UPNPSubscription * m_subscription
void Stop(void)
Stops scanning.
void BrowseNextContainer(void)
For each known media server, find the next container which needs to be browsed and trigger sending of...
void customEvent(QEvent *event) override
Processes subscription and SSDP cache update events.
void ParseBrowse(const QUrl &url, QNetworkReply *reply)
Parse the XML returned from Content Directory Service browse request.
static UPNPScanner * gUPNPScanner
UPNPScanner(UPNPSubscription *sub)
void ScheduleUpdate(void)
static void Enable(bool enable, UPNPSubscription *sub=nullptr)
Creates or destroys the global UPNPScanner instance.
void GetServerContent(const QString &usn, const MediaServerItem *content, VideoMetadataListManager::metadata_list *list, meta_dir_node *node)
Recursively search a MediaServerItem for video metadata and add it to the metadata_list and meta_dir_...
QDomDocument * FindResult(const QDomNode &n, uint &num, uint &total, uint &updateid)
void Update(void)
Iterates through the list of known servers and initialises a connection by requesting the device desc...
QMultiMap< QUrl, QNetworkReply * > m_browseRequests
void SendBrowseRequest(const QUrl &url, const QString &objectid)
Formulates and sends a ContentDirectory Service Browse Request to the given control URL,...
static void ParseDevice(QDomElement &element, QUrl &controlURL, QUrl &eventURL, QString &friendlyName)
void CheckFailure(const QUrl &url)
Updates the logs for failed server connections.
void GetInitialMetadata(VideoMetadataListManager::metadata_list *list, meta_dir_node *node)
Fill the given metadata_list and meta_dir_node with the root media server metadata (i....
void timerEvent(QTimerEvent *event) override
Handles subscription renewal timer events.
void StartFullScan(void)
Instruct the UPNPScanner thread to start a full scan of metadata from known media servers.
void Unsubscribe(const QString &usn)
void Remove(const QString &usn)
std::chrono::seconds Renew(const QString &usn)
std::chrono::seconds Subscribe(const QString &usn, const QUrl &url, const QString &path)
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
static eu8 clamp(eu8 value, eu8 low, eu8 high)
static QUrl urlAddBaseAndPath(QUrl base, const QUrl &relative)
static constexpr uint8_t MAX_ATTEMPTS