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);
128 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
206 auto *me =
new MythEvent(QString(
"UPNP_STARTSCAN"));
207 qApp->postEvent(
this, me);
221 if (servers.isEmpty())
225 LOG(VB_GENERAL, LOG_INFO, QString(
"Adding MediaServer metadata."));
231 QMutableHashIterator<QString,UpnpMediaServer*> it(
m_servers);
235 if (!it.value()->m_subscribed)
238 QString usn = it.key();
253 if (servers.isEmpty())
260 LOG(VB_GENERAL, LOG_INFO,
LOC +
"Waiting for scan to complete.");
264 std::this_thread::sleep_for(100ms);
268 LOG(VB_GENERAL, LOG_ERR,
LOC +
"MediaServer scan is incomplete.");
270 LOG(VB_GENERAL, LOG_INFO,
LOC +
"MediaServer scanning finished.");
277 QMutableHashIterator<QString,UpnpMediaServer*> it(
m_servers);
281 if (!it.value()->m_subscribed)
284 QString usn = it.key();
293 if (!data.canConvert<QStringList>())
296 QStringList list = data.toStringList();
297 if (list.size() != 2)
300 QString usn = list[0];
301 QString
object = list[1];
314 auto *me =
new MythEvent(
"UPNP_BROWSEOBJECT", list);
315 qApp->postEvent(
this, me);
319 LOG(VB_GENERAL, LOG_INFO,
"START");
320 while (!found && (count++ < 100))
322 std::this_thread::sleep_for(100ms);
333 LOG(VB_GENERAL, LOG_INFO, QString(
"Item went away..."));
339 LOG(VB_GENERAL, LOG_INFO,
340 QString(
"Server went away while browsing."));
345 LOG(VB_GENERAL, LOG_INFO,
"END");
369 item->SetTitle(QString(
"Dummy"));
370 list->push_back(item);
380 QMutableMapIterator<QString,MediaServerItem> it(
content->m_children);
390 item->SetTitle(
content->m_name);
391 list->push_back(item);
402 QMap<QString,QString> servers;
404 QHashIterator<QString,UpnpMediaServer*> it(
m_servers);
408 servers.insert(it.key(), it.value()->m_friendlyName);
425 connect(
m_network, &QNetworkAccessManager::finished,
450 LOG(VB_GENERAL, LOG_INFO,
LOC +
"Started");
473 QHashIterator<QString,UpnpMediaServer*> it(
m_servers);
479 if (it.value()->m_renewalTimerId)
480 killTimer(it.value()->m_renewalTimerId);
508 LOG(VB_GENERAL, LOG_INFO,
LOC +
"Finished");
527 bool reschedule =
false;
529 QHashIterator<QString,UpnpMediaServer*> it(
m_servers);
533 if ((it.value()->m_connectionAttempts <
MAX_ATTEMPTS) &&
534 (it.value()->m_controlURL.isEmpty()))
537 QUrl url = it.value()->m_url;
542 QNetworkReply *reply =
m_network->get(QNetworkRequest(url));
547 it.value()->m_connectionAttempts++;
570 QMutableHashIterator<QString,UpnpMediaServer*> it(
m_servers);
575 if (!
SSDP::Find(
"urn:schemas-upnp-org:device:MediaServer:1", it.key()))
577 LOG(VB_UPNP, LOG_INFO,
LOC + QString(
"%1 no longer in SSDP cache. Removing")
578 .arg(it.value()->m_serverURL.toString()));
597 QUrl url = reply->url();
598 bool valid = reply->error() == QNetworkReply::NoError;
602 LOG(VB_UPNP, LOG_ERR,
LOC +
603 QString(
"Network request for '%1' returned error '%2'")
604 .arg(url.toString(), reply->errorString()));
607 bool description =
false;
629 else if (description)
640 LOG(VB_UPNP, LOG_ERR,
LOC +
"Received unknown reply");
642 reply->deleteLater();
654 auto *me =
dynamic_cast<MythEvent *
>(event);
658 const QString& ev = me->
Message();
660 if (ev ==
"UPNP_STARTSCAN")
665 if (ev ==
"UPNP_BROWSEOBJECT")
667 if (me->ExtraDataCount() == 2)
670 const QString& usn = me->ExtraData(0);
671 const QString& objectid = me->ExtraData(1);
676 LOG(VB_GENERAL, LOG_INFO, QString(
"UPNP_BROWSEOBJECT: %1->%2")
677 .arg(
m_servers[usn]->m_friendlyName, objectid));
685 if (ev ==
"UPNP_EVENT")
690 if (!info->GetInfoMap())
693 QString usn = info->GetInfoMap()->value(
"usn");
694 QString
id = info->GetInfoMap()->value(
"SystemUpdateID");
695 if (usn.isEmpty() ||
id.isEmpty())
701 int newid =
id.toInt();
702 if (
m_servers[usn]->m_systemUpdateID != newid)
705 LOG(VB_GENERAL, LOG_INFO,
LOC +
706 QString(
"New SystemUpdateID '%1' for %2").arg(
id, usn));
715 QString uri = me->ExtraDataCount() > 0 ? me->ExtraData(0) : QString();
716 QString usn = me->ExtraDataCount() > 1 ? me->ExtraData(1) : QString();
719 if (uri ==
"urn:schemas-upnp-org:device:MediaServer:1")
721 QString url = (ev ==
"SSDP_ADD") ? me->ExtraData(2) : QString();
732 int id =
event->timerId();
736 std::chrono::seconds
timeout = 0s;
740 QHashIterator<QString,UpnpMediaServer*> it(
m_servers);
744 if (it.value()->m_renewalTimerId ==
id)
746 it.value()->m_renewalTimerId = 0;
757 LOG(VB_GENERAL, LOG_INFO,
LOC +
758 QString(
"Re-subscribed for %1 seconds to %2")
759 .arg(
timeout.count()).arg(usn));
781 QHashIterator<QString,UpnpMediaServer*> it(
m_servers);
785 if (it.value()->m_serverURL == url && it.value()->m_connectionAttempts ==
MAX_ATTEMPTS)
800 LOG(VB_UPNP, LOG_INFO,
LOC + QString(
"%1 media servers discovered:")
802 QHashIterator<QString,UpnpMediaServer*> it(
m_servers);
806 QString status =
"Probing";
807 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 };
985 std::chrono::seconds renew = std::clamp(
timeout - 10s, 10s, twelvehours);
989 m_servers[usn]->m_renewalTimerId = startTimer(renew);
999 QByteArray data = reply->readAll();
1004 auto *parent =
new QDomDocument();
1005 QString errorMessage;
1007 int errorColumn = 0;
1008 if (!parent->setContent(data,
false, &errorMessage, &errorLine,
1011 LOG(VB_GENERAL, LOG_ERR,
LOC +
1012 QString(
"DIDL Parse error, Line: %1 Col: %2 Error: '%3'")
1013 .arg(errorLine).arg(errorColumn).arg(errorMessage));
1018 LOG(VB_UPNP, LOG_INFO,
"\n\n" + parent->toString(4) +
"\n\n");
1021 QDomDocument *result =
nullptr;
1025 QDomElement docElem = parent->documentElement();
1026 QDomNode n = docElem.firstChild();
1028 result =
FindResult(n, num, total, updateid);
1031 if (!result || num < 1 || total < 1)
1033 LOG(VB_GENERAL, LOG_ERR,
LOC +
1034 QString(
"Failed to find result for %1") .arg(url.toString()));
1042 QHashIterator<QString,UpnpMediaServer*> it(
m_servers);
1043 while (it.hasNext())
1046 if (url == it.value()->m_controlURL)
1048 server = it.value();
1057 LOG(VB_GENERAL, LOG_ERR,
LOC +
1058 QString(
"Received unknown response for %1").arg(url.toString()));
1067 LOG(VB_GENERAL, LOG_ERR,
LOC +
1068 QString(
"%1 updateID changed during browse (old %2 new %3)")
1078 docElem = result->documentElement();
1079 n = docElem.firstChild();
1083 n = n.nextSibling();
1093 QDomElement node = n.toElement();
1097 if (node.tagName() ==
"container")
1099 QString title =
"ERROR";
1100 QDomNode next = node.firstChild();
1101 while (!next.isNull())
1103 QDomElement container = next.toElement();
1104 if (!container.isNull() && container.tagName() ==
"title")
1105 title = container.text();
1106 next = next.nextSibling();
1109 QString thisid = node.attribute(
"id",
"ERROR");
1110 QString parentid = node.attribute(
"parentID",
"ERROR");
1119 resetparent =
false;
1122 parent->
Add(container);
1127 if (node.tagName() ==
"item")
1129 QString title =
"ERROR";
1130 QString url =
"ERROR";
1131 QDomNode next = node.firstChild();
1132 while (!next.isNull())
1134 QDomElement item = next.toElement();
1137 if(item.tagName() ==
"res")
1139 if(item.tagName() ==
"title")
1140 title = item.text();
1142 next = next.nextSibling();
1145 QString thisid = node.attribute(
"id",
"ERROR");
1146 QString parentid = node.attribute(
"parentID",
"ERROR");
1156 resetparent =
false;
1164 QDomNode next = node.firstChild();
1165 while (!next.isNull())
1168 next = next.nextSibling();
1175 QDomDocument *result =
nullptr;
1176 QDomElement node = n.toElement();
1180 if (node.tagName() ==
"NumberReturned")
1181 num = node.text().toUInt();
1182 if (node.tagName() ==
"TotalMatches")
1183 total = node.text().toUInt();
1184 if (node.tagName() ==
"UpdateID")
1185 updateid = node.text().toUInt();
1186 if (node.tagName() ==
"Result" && !result)
1188 QString errorMessage;
1190 int errorColumn = 0;
1191 result =
new QDomDocument();
1192 if (!result->setContent(node.text(),
true, &errorMessage, &errorLine, &errorColumn))
1194 LOG(VB_GENERAL, LOG_ERR,
LOC +
1195 QString(
"DIDL Parse error, Line: %1 Col: %2 Error: '%3'")
1196 .arg(errorLine).arg(errorColumn).arg(errorMessage));
1202 QDomNode next = node.firstChild();
1203 while (!next.isNull())
1205 QDomDocument *res =
FindResult(next, num, total, updateid);
1208 next = next.nextSibling();
1219 if (url.isEmpty() || !reply)
1222 QByteArray data = reply->readAll();
1225 LOG(VB_GENERAL, LOG_ERR,
LOC +
1226 QString(
"%1 returned an empty device description.")
1227 .arg(url.toString()));
1232 QString controlURL = QString();
1233 QString eventURL = QString();
1234 QString friendlyName = QString(
"Unknown");
1235 QString URLBase = QString();
1238 QString errorMessage;
1240 int errorColumn = 0;
1241 if (!doc.setContent(data,
false, &errorMessage, &errorLine, &errorColumn))
1243 LOG(VB_GENERAL, LOG_ERR,
LOC +
1244 QString(
"Failed to parse device description from %1")
1245 .arg(url.toString()));
1246 LOG(VB_GENERAL, LOG_ERR,
LOC + QString(
"Line: %1 Col: %2 Error: '%3'")
1247 .arg(errorLine).arg(errorColumn).arg(errorMessage));
1251 QDomElement docElem = doc.documentElement();
1252 QDomNode n = docElem.firstChild();
1255 QDomElement e1 = n.toElement();
1258 if(e1.tagName() ==
"device")
1259 ParseDevice(e1, controlURL, eventURL, friendlyName);
1260 if (e1.tagName() ==
"URLBase")
1261 URLBase = e1.text();
1263 n = n.nextSibling();
1266 if (controlURL.isEmpty())
1268 LOG(VB_UPNP, LOG_ERR,
LOC +
1269 QString(
"Failed to parse device description for %1")
1270 .arg(url.toString()));
1275 if (URLBase.isEmpty())
1276 URLBase = url.toString(QUrl::RemovePath | QUrl::RemoveFragment |
1280 while (!controlURL.isEmpty() && controlURL.startsWith(
"/"))
1281 controlURL = controlURL.mid(1);
1288 while (!URLBase.isEmpty() && URLBase.endsWith(
"/"))
1289 URLBase = URLBase.mid(0, URLBase.size() - 1);
1291 controlURL = URLBase +
"/" + controlURL;
1292 QString fulleventURL = URLBase +
"/" + eventURL;
1294 LOG(VB_UPNP, LOG_INFO,
LOC + QString(
"Control URL for %1 at %2")
1295 .arg(friendlyName, controlURL));
1296 LOG(VB_UPNP, LOG_INFO,
LOC + QString(
"Event URL for %1 at %2")
1297 .arg(friendlyName, fulleventURL));
1302 QUrl qeventurl = QUrl(fulleventURL);
1303 std::chrono::seconds
timeout = 0s;
1306 QHashIterator<QString,UpnpMediaServer*> it(
m_servers);
1307 while (it.hasNext())
1310 if (it.value()->m_serverURL == url)
1313 QUrl qcontrolurl(controlURL);
1314 it.value()->m_controlURL = qcontrolurl;
1315 it.value()->m_eventSubURL = qeventurl;
1316 it.value()->m_eventSubPath = eventURL;
1317 it.value()->m_friendlyName = friendlyName;
1318 it.value()->m_name = friendlyName;
1332 LOG(VB_GENERAL, LOG_INFO,
LOC +
1333 QString(
"Subscribed for %1 seconds to %2") .arg(
timeout.count()).arg(usn));
1346 QString &eventURL, QString &friendlyName)
1348 QDomNode dev = element.firstChild();
1349 while (!dev.isNull())
1351 QDomElement e = dev.toElement();
1354 if (e.tagName() ==
"friendlyName")
1355 friendlyName = e.text();
1356 if (e.tagName() ==
"serviceList")
1359 dev = dev.nextSibling();
1366 QDomNode list = element.firstChild();
1367 while (!list.isNull())
1369 QDomElement e = list.toElement();
1371 if (e.tagName() ==
"service")
1373 list = list.nextSibling();
1381 QString control_url = QString();
1382 QString event_url = QString();
1383 QDomNode service = element.firstChild();
1385 while (!service.isNull())
1387 QDomElement e = service.toElement();
1390 if (e.tagName() ==
"serviceType")
1392 iscds = (e.text() ==
"urn:schemas-upnp-org:service:ContentDirectory:1");
1393 if (e.tagName() ==
"controlURL")
1394 control_url = e.text();
1395 if (e.tagName() ==
"eventSubURL")
1396 event_url = e.text();
1398 service = service.nextSibling();
1403 controlURL = control_url;
1404 eventURL = event_url;