7#include <QCoreApplication>
8#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
9#include <QStringConverter>
22#define LOC QString("UPnPScan: ")
23#define ERR QString("UPnPScan error: ")
38 QMutableMapIterator<QString,MediaServerItem> it(
m_children);
42 QString result = it.value().NextUnbrowsed();
43 if (!result.isEmpty())
55 QMutableMapIterator<QString,MediaServerItem> it(
m_children);
202 auto *me =
new MythEvent(QString(
"UPNP_STARTSCAN"));
203 qApp->postEvent(
this, me);
217 if (servers.isEmpty())
221 LOG(VB_GENERAL, LOG_INFO, QString(
"Adding MediaServer metadata."));
227 QMutableHashIterator<QString,UpnpMediaServer*> it(
m_servers);
231 if (!it.value()->m_subscribed)
234 QString usn = it.key();
249 if (servers.isEmpty())
256 LOG(VB_GENERAL, LOG_INFO,
LOC +
"Waiting for scan to complete.");
260 std::this_thread::sleep_for(100ms);
264 LOG(VB_GENERAL, LOG_ERR,
LOC +
"MediaServer scan is incomplete.");
266 LOG(VB_GENERAL, LOG_INFO,
LOC +
"MediaServer scanning finished.");
273 QMutableHashIterator<QString,UpnpMediaServer*> it(
m_servers);
277 if (!it.value()->m_subscribed)
280 QString usn = it.key();
289 if (!data.canConvert<QStringList>())
292 QStringList list = data.toStringList();
293 if (list.size() != 2)
296 const QString& usn = list[0];
297 QString
object = list[1];
310 auto *me =
new MythEvent(
"UPNP_BROWSEOBJECT", list);
311 qApp->postEvent(
this, me);
315 LOG(VB_GENERAL, LOG_INFO,
"START");
316 while (!found && (count++ < 100))
318 std::this_thread::sleep_for(100ms);
329 LOG(VB_GENERAL, LOG_INFO, QString(
"Item went away..."));
335 LOG(VB_GENERAL, LOG_INFO,
336 QString(
"Server went away while browsing."));
341 LOG(VB_GENERAL, LOG_INFO,
"END");
365 item->SetTitle(QString(
"Dummy"));
366 list->push_back(item);
376 QMutableMapIterator<QString,MediaServerItem> it(
content->m_children);
386 item->SetTitle(
content->m_name);
387 list->push_back(item);
398 QMap<QString,QString> servers;
400 QHashIterator<QString,UpnpMediaServer*> it(
m_servers);
404 servers.insert(it.key(), it.value()->m_friendlyName);
421 connect(
m_network, &QNetworkAccessManager::finished,
446 LOG(VB_GENERAL, LOG_INFO,
LOC +
"Started");
469 QHashIterator<QString,UpnpMediaServer*> it(
m_servers);
475 if (it.value()->m_renewalTimerId)
476 killTimer(it.value()->m_renewalTimerId);
504 LOG(VB_GENERAL, LOG_INFO,
LOC +
"Finished");
523 bool reschedule =
false;
525 QHashIterator<QString,UpnpMediaServer*> it(
m_servers);
529 if ((it.value()->m_connectionAttempts <
MAX_ATTEMPTS) &&
530 (it.value()->m_controlURL.isEmpty()))
533 QUrl url = it.value()->m_url;
538 QNetworkReply *reply =
m_network->get(QNetworkRequest(url));
543 it.value()->m_connectionAttempts++;
566 QMutableHashIterator<QString,UpnpMediaServer*> it(
m_servers);
571 if (!
SSDP::Find(
"urn:schemas-upnp-org:device:MediaServer:1", it.key()))
573 LOG(VB_UPNP, LOG_INFO,
LOC + QString(
"%1 no longer in SSDP cache. Removing")
574 .arg(it.value()->m_serverURL.toString()));
593 QUrl url = reply->url();
594 bool valid = reply->error() == QNetworkReply::NoError;
598 LOG(VB_UPNP, LOG_ERR,
LOC +
599 QString(
"Network request for '%1' returned error '%2'")
600 .arg(url.toString(), reply->errorString()));
603 bool description =
false;
625 else if (description)
637 LOG(VB_UPNP, LOG_ERR,
LOC +
"Received unknown reply");
640 reply->deleteLater();
652 auto *me =
dynamic_cast<MythEvent *
>(event);
656 const QString& ev = me->
Message();
658 if (ev ==
"UPNP_STARTSCAN")
663 if (ev ==
"UPNP_BROWSEOBJECT")
665 if (me->ExtraDataCount() == 2)
668 const QString& usn = me->ExtraData(0);
669 const QString& objectid = me->ExtraData(1);
674 LOG(VB_GENERAL, LOG_INFO, QString(
"UPNP_BROWSEOBJECT: %1->%2")
675 .arg(
m_servers[usn]->m_friendlyName, objectid));
683 if (ev ==
"UPNP_EVENT")
688 if (!
info->GetInfoMap())
691 QString usn =
info->GetInfoMap()->value(
"usn");
692 QString
id =
info->GetInfoMap()->value(
"SystemUpdateID");
693 if (usn.isEmpty() ||
id.isEmpty())
699 int newid =
id.toInt();
700 if (
m_servers[usn]->m_systemUpdateID != newid)
703 LOG(VB_GENERAL, LOG_INFO,
LOC +
704 QString(
"New SystemUpdateID '%1' for %2").arg(
id, usn));
713 QString uri = me->ExtraDataCount() > 0 ? me->ExtraData(0) : QString();
714 QString usn = me->ExtraDataCount() > 1 ? me->ExtraData(1) : QString();
717 if (uri ==
"urn:schemas-upnp-org:device:MediaServer:1")
719 QString url = (ev ==
"SSDP_ADD") ? me->ExtraData(2) : QString();
730 int id =
event->timerId();
734 std::chrono::seconds
timeout = 0s;
738 QHashIterator<QString,UpnpMediaServer*> it(
m_servers);
742 if (it.value()->m_renewalTimerId ==
id)
744 it.value()->m_renewalTimerId = 0;
755 LOG(VB_GENERAL, LOG_INFO,
LOC +
756 QString(
"Re-subscribed for %1 seconds to %2")
757 .arg(
timeout.count()).arg(usn));
779 QHashIterator<QString,UpnpMediaServer*> it(
m_servers);
783 if (it.value()->m_serverURL == url && it.value()->m_connectionAttempts ==
MAX_ATTEMPTS)
798 LOG(VB_UPNP, LOG_INFO,
LOC + QString(
"%1 media servers discovered:")
800 QHashIterator<QString,UpnpMediaServer*> it(
m_servers);
804 QString status =
"Probing";
805 if (it.value()->m_controlURL.toString().isEmpty())
814 LOG(VB_UPNP, LOG_INFO,
LOC +
815 QString(
"'%1' Connected: %2 Subscribed: %3 SystemUpdateID: "
817 .arg(it.value()->m_friendlyName, status,
818 it.value()->m_subscribed ?
"Yes" :
"No",
819 QString::number(it.value()->m_systemUpdateID),
820 QString::number(it.value()->m_renewalTimerId)));
835 QMutexLocker locker(&
m_lock);
837 QHashIterator<QString,UpnpMediaServer*> it(
m_servers);
838 bool complete =
true;
842 if (it.value()->m_subscribed)
851 QString next = it.value()->NextUnbrowsed();
859 LOG(VB_UPNP, LOG_INFO,
LOC + QString(
"Scan completed for %1")
860 .arg(it.value()->m_friendlyName));
866 LOG(VB_GENERAL, LOG_INFO,
LOC +
867 QString(
"Media Server scan is complete."));
880 QNetworkRequest req = QNetworkRequest(url);
881 req.setRawHeader(
"CONTENT-TYPE",
"text/xml; charset=\"utf-8\"");
882 req.setRawHeader(
"SOAPACTION",
883 "\"urn:schemas-upnp-org:service:ContentDirectory:1#Browse\"");
885 req.setRawHeader(
"MAN",
"\"http://schemasxmlsoap.org/soap/envelope/\"");
886 req.setRawHeader(
"01-SOAPACTION",
887 "\"urn:schemas-upnp-org:service:ContentDirectory:1#Browse\"");
891 QTextStream data(&body);
892#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
893 data.setCodec(QTextCodec::codecForName(
"UTF-8"));
895 data.setEncoding(QStringConverter::Utf8);
897 data <<
"<?xml version=\"1.0\"?>\r\n";
898 data <<
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n";
899 data <<
" <s:Body>\r\n";
900 data <<
" <u:Browse xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">\r\n";
901 data <<
" <ObjectID>" << objectid.toUtf8() <<
"</ObjectID>\r\n";
902 data <<
" <BrowseFlag>BrowseDirectChildren</BrowseFlag>\r\n";
903 data <<
" <Filter>*</Filter>\r\n";
904 data <<
" <StartingIndex>0</StartingIndex>\r\n";
905 data <<
" <RequestedCount>0</RequestedCount>\r\n";
906 data <<
" <SortCriteria></SortCriteria>\r\n";
907 data <<
" </u:Browse>\r\n";
908 data <<
" </s:Body>\r\n";
909 data <<
"</s:Envelope>\r\n";
913 QNetworkReply *reply =
m_network->post(req, body);
942 LOG(VB_UPNP, LOG_INFO,
LOC +
"Ignoring master backend.");
950 LOG(VB_GENERAL, LOG_INFO,
LOC + QString(
"Adding: %1").arg(usn));
964 LOG(VB_GENERAL, LOG_INFO,
LOC + QString(
"Removing: %1").arg(usn));
984 std::chrono::seconds twelvehours { 12h };
989 m_servers[usn]->m_renewalTimerId = startTimer(renew);
999 QByteArray data = reply->readAll();
1004 auto *parent =
new QDomDocument();
1005#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
1006 QString errorMessage;
1008 int errorColumn = 0;
1009 if (!parent->setContent(data,
false, &errorMessage, &errorLine,
1012 LOG(VB_GENERAL, LOG_ERR,
LOC +
1013 QString(
"DIDL Parse error, Line: %1 Col: %2 Error: '%3'")
1014 .arg(errorLine).arg(errorColumn).arg(errorMessage));
1019 auto parseResult = parent->setContent(data);
1022 LOG(VB_GENERAL, LOG_ERR,
LOC +
1023 QString(
"DIDL Parse error, Line: %1 Col: %2 Error: '%3'")
1024 .arg(parseResult.errorLine).arg(parseResult.errorColumn)
1025 .arg(parseResult.errorMessage));
1031 LOG(VB_UPNP, LOG_INFO,
"\n\n" + parent->toString(4) +
"\n\n");
1034 QDomDocument *result =
nullptr;
1038 QDomElement docElem = parent->documentElement();
1039 QDomNode n = docElem.firstChild();
1041 result =
FindResult(n, num, total, updateid);
1044 if (!result || num < 1 || total < 1)
1046 LOG(VB_GENERAL, LOG_ERR,
LOC +
1047 QString(
"Failed to find result for %1") .arg(url.toString()));
1055 QHashIterator<QString,UpnpMediaServer*> it(
m_servers);
1056 while (it.hasNext())
1059 if (url == it.value()->m_controlURL)
1061 server = it.value();
1070 LOG(VB_GENERAL, LOG_ERR,
LOC +
1071 QString(
"Received unknown response for %1").arg(url.toString()));
1080 LOG(VB_GENERAL, LOG_ERR,
LOC +
1081 QString(
"%1 updateID changed during browse (old %2 new %3)")
1091 docElem = result->documentElement();
1092 n = docElem.firstChild();
1096 n = n.nextSibling();
1106 QDomElement node = n.toElement();
1110 if (node.tagName() ==
"container")
1112 QString title =
"ERROR";
1113 QDomNode next = node.firstChild();
1114 while (!next.isNull())
1116 QDomElement container = next.toElement();
1117 if (!container.isNull() && container.tagName() ==
"title")
1118 title = container.text();
1119 next = next.nextSibling();
1122 QString thisid = node.attribute(
"id",
"ERROR");
1123 QString parentid = node.attribute(
"parentID",
"ERROR");
1132 resetparent =
false;
1135 parent->
Add(container);
1140 if (node.tagName() ==
"item")
1142 QString title =
"ERROR";
1143 QString url =
"ERROR";
1144 QDomNode next = node.firstChild();
1145 while (!next.isNull())
1147 QDomElement item = next.toElement();
1150 if(item.tagName() ==
"res")
1152 if(item.tagName() ==
"title")
1153 title = item.text();
1155 next = next.nextSibling();
1158 QString thisid = node.attribute(
"id",
"ERROR");
1159 QString parentid = node.attribute(
"parentID",
"ERROR");
1169 resetparent =
false;
1177 QDomNode next = node.firstChild();
1178 while (!next.isNull())
1181 next = next.nextSibling();
1188 QDomDocument *result =
nullptr;
1189 QDomElement node = n.toElement();
1193 if (node.tagName() ==
"NumberReturned")
1194 num = node.text().toUInt();
1195 if (node.tagName() ==
"TotalMatches")
1196 total = node.text().toUInt();
1197 if (node.tagName() ==
"UpdateID")
1198 updateid = node.text().toUInt();
1199 if (node.tagName() ==
"Result" && !result)
1201 result =
new QDomDocument();
1202#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
1203 QString errorMessage;
1205 int errorColumn = 0;
1206 if (!result->setContent(node.text(),
true, &errorMessage, &errorLine, &errorColumn))
1208 LOG(VB_GENERAL, LOG_ERR,
LOC +
1209 QString(
"DIDL Parse error, Line: %1 Col: %2 Error: '%3'")
1210 .arg(errorLine).arg(errorColumn).arg(errorMessage));
1216 result->setContent(node.text(),
1217 QDomDocument::ParseOption::UseNamespaceProcessing);
1220 LOG(VB_GENERAL, LOG_ERR,
LOC +
1221 QString(
"DIDL Parse error, Line: %1 Col: %2 Error: '%3'")
1222 .arg(parseResult.errorLine).arg(parseResult.errorColumn)
1223 .arg(parseResult.errorMessage));
1230 QDomNode next = node.firstChild();
1231 while (!next.isNull())
1233 QDomDocument *res =
FindResult(next, num, total, updateid);
1236 next = next.nextSibling();
1247 if (url.isEmpty() || !reply)
1250 QByteArray data = reply->readAll();
1253 LOG(VB_GENERAL, LOG_ERR,
LOC +
1254 QString(
"%1 returned an empty device description.")
1255 .arg(url.toString()));
1260 QString controlURL = QString();
1261 QString eventURL = QString();
1262 QString friendlyName = QString(
"Unknown");
1263 QString URLBase = QString();
1266#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
1267 QString errorMessage;
1269 int errorColumn = 0;
1270 if (!doc.setContent(data,
false, &errorMessage, &errorLine, &errorColumn))
1272 LOG(VB_GENERAL, LOG_ERR,
LOC +
1273 QString(
"Failed to parse device description from %1")
1274 .arg(url.toString()));
1275 LOG(VB_GENERAL, LOG_ERR,
LOC + QString(
"Line: %1 Col: %2 Error: '%3'")
1276 .arg(errorLine).arg(errorColumn).arg(errorMessage));
1280 auto parseResult = doc.setContent(data);
1283 LOG(VB_GENERAL, LOG_ERR,
LOC +
1284 QString(
"Failed to parse device description from %1")
1285 .arg(url.toString()));
1286 LOG(VB_GENERAL, LOG_ERR,
LOC + QString(
"Line: %1 Col: %2 Error: '%3'")
1287 .arg(parseResult.errorLine).arg(parseResult.errorColumn)
1288 .arg(parseResult.errorMessage));
1293 QDomElement docElem = doc.documentElement();
1294 QDomNode n = docElem.firstChild();
1297 QDomElement e1 = n.toElement();
1300 if(e1.tagName() ==
"device")
1301 ParseDevice(e1, controlURL, eventURL, friendlyName);
1302 if (e1.tagName() ==
"URLBase")
1303 URLBase = e1.text();
1305 n = n.nextSibling();
1308 if (controlURL.isEmpty())
1310 LOG(VB_UPNP, LOG_ERR,
LOC +
1311 QString(
"Failed to parse device description for %1")
1312 .arg(url.toString()));
1317 if (URLBase.isEmpty())
1318 URLBase = url.toString(QUrl::RemovePath | QUrl::RemoveFragment |
1322 while (!controlURL.isEmpty() && controlURL.startsWith(
"/"))
1323 controlURL = controlURL.mid(1);
1330 while (!URLBase.isEmpty() && URLBase.endsWith(
"/"))
1331 URLBase = URLBase.mid(0, URLBase.size() - 1);
1333 controlURL = URLBase +
"/" + controlURL;
1334 QString fulleventURL = URLBase +
"/" + eventURL;
1336 LOG(VB_UPNP, LOG_INFO,
LOC + QString(
"Control URL for %1 at %2")
1337 .arg(friendlyName, controlURL));
1338 LOG(VB_UPNP, LOG_INFO,
LOC + QString(
"Event URL for %1 at %2")
1339 .arg(friendlyName, fulleventURL));
1344 QUrl qeventurl = QUrl(fulleventURL);
1345 std::chrono::seconds
timeout = 0s;
1348 QHashIterator<QString,UpnpMediaServer*> it(
m_servers);
1349 while (it.hasNext())
1352 if (it.value()->m_serverURL == url)
1355 QUrl qcontrolurl(controlURL);
1356 it.value()->m_controlURL = qcontrolurl;
1357 it.value()->m_eventSubURL = qeventurl;
1358 it.value()->m_eventSubPath = eventURL;
1359 it.value()->m_friendlyName = friendlyName;
1360 it.value()->m_name = friendlyName;
1374 LOG(VB_GENERAL, LOG_INFO,
LOC +
1375 QString(
"Subscribed for %1 seconds to %2") .arg(
timeout.count()).arg(usn));
1388 QString &eventURL, QString &friendlyName)
1390 QDomNode dev = element.firstChild();
1391 while (!dev.isNull())
1393 QDomElement e = dev.toElement();
1396 if (e.tagName() ==
"friendlyName")
1397 friendlyName = e.text();
1398 if (e.tagName() ==
"serviceList")
1401 dev = dev.nextSibling();
1408 QDomNode list = element.firstChild();
1409 while (!list.isNull())
1411 QDomElement e = list.toElement();
1413 if (e.tagName() ==
"service")
1415 list = list.nextSibling();
1423 QString control_url = QString();
1424 QString event_url = QString();
1425 QDomNode service = element.firstChild();
1427 while (!service.isNull())
1429 QDomElement e = service.toElement();
1432 if (e.tagName() ==
"serviceType")
1434 iscds = (e.text() ==
"urn:schemas-upnp-org:service:ContentDirectory:1");
1435 if (e.tagName() ==
"controlURL")
1436 control_url = e.text();
1437 if (e.tagName() ==
"eventSubURL")
1438 event_url = e.text();
1440 service = service.nextSibling();
1445 controlURL = control_url;
1446 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 SSDPCacheEntries * Find(const QString &sURI)
static void RemoveListener(QObject *listener)
static void AddListener(QObject *listener)
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...
void replyFinished(QNetworkReply *reply)
Validates network responses against known requests and parses expected responses for the required dat...
static QRecursiveMutex * gUPNPScannerLock
static bool gUPNPScannerEnabled
static void ParseDevice(QDomElement &element, QString &controlURL, QString &eventURL, QString &friendlyName)
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
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.
QDomDocument * FindResult(const QDomNode &n, uint &num, uint &total, uint &updateid)
static void ParseServiceList(QDomElement &element, QString &controlURL, QString &eventURL)
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 GetServerContent(QString &usn, 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_...
void SendBrowseRequest(const QUrl &url, const QString &objectid)
Formulates and sends a ContentDirectory Service Browse Request to the given control URL,...
void CheckFailure(const QUrl &url)
Updates the logs for failed server connections.
static void ParseService(QDomElement &element, QString &controlURL, QString &eventURL)
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 constexpr uint8_t MAX_ATTEMPTS