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;