MythTV master
upnpscanner.cpp
Go to the documentation of this file.
1// C++
2#include <algorithm>
3#include <chrono> // for milliseconds
4#include <thread> // for sleep_for
5#include <utility>
6
7// Qt
8#include <QCoreApplication>
9#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
10#include <QStringConverter>
11#else
12#include <QTextCodec>
13#endif
14
15// MythTV
19
20// MythFrontend
21#include "upnpscanner.h"
22
23#define LOC QString("UPnPScan: ")
24#define ERR QString("UPnPScan error: ")
25
26static constexpr uint8_t MAX_ATTEMPTS { 5 };
27
29{
30 // items don't need scanning
31 if (!m_url.isEmpty())
32 return {};
33
34 // scan this container
35 if (!m_scanned)
36 return m_id;
37
38 // scan children
39 for (const auto& value : std::as_const(m_children))
40 {
41 QString result = value.NextUnbrowsed();
42 if (!result.isEmpty())
43 return result;
44 }
45
46 return {};
47}
48
50{
51 if (m_id == id)
52 return this;
53
54 for (auto& value : m_children)
55 {
56 MediaServerItem* result = value.Find(id);
57 if (result)
58 return result;
59 }
60
61 return nullptr;
62}
63
65{
66 if (m_id == item.m_parentid)
67 {
68 m_children.insert(item.m_id, item);
69 return true;
70 }
71 return false;
72}
73
75{
76 m_children.clear();
77 m_scanned = false;
78}
79
85{
86 public:
88 : MediaServerItem(QString("0"), QString(), QString(), QString()),
89 m_friendlyName(QString("Unknown"))
90 {
91 }
92 explicit UpnpMediaServer(QUrl URL)
93 : MediaServerItem(QString("0"), QString(), QString(), QString()),
94 m_serverURL(std::move(URL)),
95 m_friendlyName(QString("Unknown"))
96 {
97 }
98
99 bool ResetContent(int new_id)
100 {
101 bool result = true;
102 if (m_systemUpdateID != -1)
103 {
104 result = false;
105 Reset();
106 }
107 m_systemUpdateID = new_id;
108 return result;
109 }
110
117 bool m_subscribed {false};
120};
121
125QRecursiveMutex* UPNPScanner::gUPNPScannerLock = new QRecursiveMutex();
126
138{
139 Stop();
140}
141
147{
148 QMutexLocker locker(gUPNPScannerLock);
149 gUPNPScannerEnabled = enable;
150 Instance(sub);
151}
152
159{
160 QMutexLocker locker(gUPNPScannerLock);
162 {
164 {
167 }
168 delete gUPNPScannerThread;
169 gUPNPScannerThread = nullptr;
170 delete gUPNPScanner;
171 gUPNPScanner = nullptr;
172 return nullptr;
173 }
174
176 gUPNPScannerThread = new MThread("UPnPScanner");
177 if (!gUPNPScanner)
178 gUPNPScanner = new UPNPScanner(sub);
179
181 {
182 gUPNPScanner->moveToThread(gUPNPScannerThread->qthread());
183 QObject::connect(
184 gUPNPScannerThread->qthread(), &QThread::started,
186 gUPNPScannerThread->start(QThread::LowestPriority);
187 }
188
189 return gUPNPScanner;
190}
197{
198 m_fullscan = true;
199 auto *me = new MythEvent(QString("UPNP_STARTSCAN"));
200 qApp->postEvent(this, me);
201}
202
210 meta_dir_node *node)
211{
212 // nothing to see..
213 QMap<QString,QString> servers = ServerList();
214 if (servers.isEmpty())
215 return;
216
217 // Add MediaServers
218 LOG(VB_GENERAL, LOG_INFO, QString("Adding MediaServer metadata."));
219
220 smart_dir_node mediaservers = node->addSubDir(tr("Media Servers"));
221 mediaservers->setPathRoot();
222
223 m_lock.lock();
224 for (auto it = m_servers.cbegin(); it != m_servers.cend(); ++it)
225 {
226 if (!it.value()->m_subscribed)
227 continue;
228
229 const QString& usn = it.key();
230 GetServerContent(usn, it.value(), list, mediaservers.get());
231 }
232 m_lock.unlock();
233}
234
240 meta_dir_node *node)
241{
242 // nothing to see..
243 QMap<QString,QString> servers = ServerList();
244 if (servers.isEmpty())
245 return;
246
247 // Start scanning if it isn't already running
249
250 // wait for the scanner to complete - with a 30 second timeout
251 LOG(VB_GENERAL, LOG_INFO, LOC + "Waiting for scan to complete.");
252
253 int count = 0;
254 while (!m_scanComplete && (count++ < 300))
255 std::this_thread::sleep_for(100ms);
256
257 // some scans may just take too long (PlayOn)
258 if (!m_scanComplete)
259 LOG(VB_GENERAL, LOG_ERR, LOC + "MediaServer scan is incomplete.");
260 else
261 LOG(VB_GENERAL, LOG_INFO, LOC + "MediaServer scanning finished.");
262
263
264 smart_dir_node mediaservers = node->addSubDir(tr("Media Servers"));
265 mediaservers->setPathRoot();
266
267 m_lock.lock();
268 for (auto it = m_servers.cbegin(); it != m_servers.cend(); ++it)
269 {
270 if (!it.value()->m_subscribed)
271 continue;
272
273 const QString& usn = it.key();
274 GetServerContent(usn, it.value(), list, mediaservers.get());
275 }
276 m_lock.unlock();
277}
278
279bool UPNPScanner::GetMetadata(QVariant &data)
280{
281 // we need a USN and objectID
282 if (!data.canConvert<QStringList>())
283 return false;
284
285 QStringList list = data.toStringList();
286 if (list.size() != 2)
287 return false;
288
289 const QString& usn = list[0];
290 QString object = list[1];
291
292 m_lock.lock();
293 bool valid = m_servers.contains(usn);
294 if (valid)
295 {
296 MediaServerItem* item = m_servers[usn]->Find(object);
297 valid = item ? !item->m_scanned : false;
298 }
299 m_lock.unlock();
300 if (!valid)
301 return false;
302
303 auto *me = new MythEvent("UPNP_BROWSEOBJECT", list);
304 qApp->postEvent(this, me);
305
306 int count = 0;
307 bool found = false;
308 LOG(VB_GENERAL, LOG_INFO, "START");
309 while (!found && (count++ < 100)) // 10 seconds
310 {
311 std::this_thread::sleep_for(100ms);
312 m_lock.lock();
313 if (m_servers.contains(usn))
314 {
315 MediaServerItem *item = m_servers[usn]->Find(object);
316 if (item)
317 {
318 found = item->m_scanned;
319 }
320 else
321 {
322 LOG(VB_GENERAL, LOG_INFO, QString("Item went away..."));
323 found = true;
324 }
325 }
326 else
327 {
328 LOG(VB_GENERAL, LOG_INFO,
329 QString("Server went away while browsing."));
330 found = true;
331 }
332 m_lock.unlock();
333 }
334 LOG(VB_GENERAL, LOG_INFO, "END");
335 return true;
336}
337
343void UPNPScanner::GetServerContent(const QString &usn,
346 meta_dir_node *node)
347{
348 if (!content->m_scanned)
349 {
350 smart_dir_node subnode = node->addSubDir(content->m_name);
351
352 QStringList data;
353 data << usn;
354 data << content->m_id;
355 subnode->SetData(data);
356
358 item->SetTitle(QString("Dummy"));
359 list->push_back(item);
360 subnode->addEntry(smart_meta_node(new meta_data_node(item.get())));
361 return;
362 }
363
364 node->SetData(QVariant());
365
366 if (content->m_url.isEmpty())
367 {
368 smart_dir_node container = node->addSubDir(content->m_name);
369 for (const auto& child : std::as_const(content->m_children))
370 GetServerContent(usn, &child, list, container.get());
371 return;
372 }
373
375 item->SetTitle(content->m_name);
376 list->push_back(item);
377 node->addEntry(smart_meta_node(new meta_data_node(item.get())));
378}
379
385QMap<QString,QString> UPNPScanner::ServerList(void)
386{
387 QMap<QString,QString> servers;
388 m_lock.lock();
389 for (auto it = m_servers.cbegin(); it != m_servers.cend(); ++it)
390 servers.insert(it.key(), it.value()->m_friendlyName);
391 m_lock.unlock();
392 return servers;
393}
394
401{
402 m_lock.lock();
403
404 // create our network handler
405 m_network = new QNetworkAccessManager();
406 connect(m_network, &QNetworkAccessManager::finished,
408
409 // listen for SSDP updates
411
412 // listen for subscriptions and events
413 if (m_subscription)
415
416 // create our update timer (driven by AddServer and ParseDescription)
417 m_updateTimer = new QTimer(this);
418 m_updateTimer->setSingleShot(true);
420
421 // create our watchdog timer (checks for stale servers)
422 m_watchdogTimer = new QTimer(this);
424 m_watchdogTimer->start(10s);
425
426 // avoid connecting to the master backend
429
430 m_lock.unlock();
431 LOG(VB_GENERAL, LOG_INFO, LOC + "Started");
432}
433
439{
440 m_lock.lock();
441
442 // stop listening
444 if (m_subscription)
446
447 // disable updates
448 if (m_updateTimer)
449 m_updateTimer->stop();
450 if (m_watchdogTimer)
451 m_watchdogTimer->stop();
452
453 // cleanup our servers and subscriptions
454 for (auto it = m_servers.cbegin(); it != m_servers.cend(); ++it)
455 {
456 if (m_subscription && it.value()->m_subscribed)
457 m_subscription->Unsubscribe(it.key());
458 if (it.value()->m_renewalTimerId)
459 killTimer(it.value()->m_renewalTimerId);
460 delete it.value();
461 }
462 m_servers.clear();
463
464 // cleanup the network
465 for (QNetworkReply *reply : std::as_const(m_descriptionRequests))
466 {
467 reply->abort();
468 delete reply;
469 }
470 m_descriptionRequests.clear();
471 for (QNetworkReply *reply : std::as_const(m_browseRequests))
472 {
473 reply->abort();
474 delete reply;
475 }
476 m_browseRequests.clear();
477 delete m_network;
478 m_network = nullptr;
479
480 // delete the timers
481 delete m_updateTimer;
482 delete m_watchdogTimer;
483 m_updateTimer = nullptr;
484 m_watchdogTimer = nullptr;
485
486 m_lock.unlock();
487 LOG(VB_GENERAL, LOG_INFO, LOC + "Finished");
488}
489
496{
497 // decide which servers still need to be checked
498 m_lock.lock();
499 if (m_servers.isEmpty())
500 {
501 m_lock.unlock();
502 return;
503 }
504
505 // if our network queue is full, then we may need to come back later
506 bool reschedule = false;
507
508 for (auto *server : std::as_const(m_servers))
509 {
510 if ((server->m_connectionAttempts < MAX_ATTEMPTS) &&
511 (server->m_controlURL.isEmpty()))
512 {
513 bool sent = false;
514 QUrl url { server->m_serverURL };
515 if (!m_descriptionRequests.contains(url) &&
516 (m_descriptionRequests.empty()) &&
517 url.isValid())
518 {
519 QNetworkReply *reply = m_network->get(QNetworkRequest(url));
520 if (reply)
521 {
522 sent = true;
523 m_descriptionRequests.insert(url, reply);
524 server->m_connectionAttempts++;
525 }
526 }
527 if (!sent)
528 reschedule = true;
529 }
530 }
531
532 if (reschedule)
534 m_lock.unlock();
535}
536
542{
543 // FIXME
544 // Remove stale servers - the SSDP cache code does not send out removal
545 // notifications for expired (rather than explicitly closed) connections
546 m_lock.lock();
547 for (auto it = m_servers.begin(); it != m_servers.end(); /* no inc */)
548 {
549 // FIXME UPNP version comparision done wrong, we are using urn:schemas-upnp-org:device:MediaServer:4 ourselves
550 if (!SSDPCache::Instance()->Find("urn:schemas-upnp-org:device:MediaServer:1", it.key()))
551 {
552 LOG(VB_UPNP, LOG_INFO, LOC + QString("%1 no longer in SSDP cache. Removing")
553 .arg(it.value()->m_serverURL.toString()));
554 UpnpMediaServer* last = it.value();
555 it = m_servers.erase(it);
556 delete last;
557 }
558 else
559 {
560 ++it;
561 }
562 }
563 m_lock.unlock();
564}
565
571void UPNPScanner::replyFinished(QNetworkReply *reply)
572{
573 if (!reply)
574 return;
575
576 QUrl url = reply->url();
577 bool valid = reply->error() == QNetworkReply::NoError;
578
579 if (!valid)
580 {
581 LOG(VB_UPNP, LOG_ERR, LOC +
582 QString("Network request for '%1' returned error '%2'")
583 .arg(url.toString(), reply->errorString()));
584 }
585
586 bool description = false;
587 bool browse = false;
588
589 m_lock.lock();
590 if (m_descriptionRequests.contains(url, reply))
591 {
592 m_descriptionRequests.remove(url, reply);
593 description = true;
594 }
595 else if (m_browseRequests.contains(url, reply))
596 {
597 m_browseRequests.remove(url, reply);
598 browse = true;
599 }
600 m_lock.unlock();
601
602 if (browse && valid)
603 {
604 ParseBrowse(url, reply);
605 if (m_fullscan)
607 }
608 else if (description)
609 {
610 if (!valid || !ParseDescription(url, reply))
611 {
612 // if there will be no more attempts, update the logs
613 CheckFailure(url);
614 // try again
616 }
617 }
618 else
619 {
620 LOG(VB_UPNP, LOG_ERR, LOC + "Received unknown reply");
621 }
622
623 reply->deleteLater();
624}
625
629void UPNPScanner::customEvent(QEvent *event)
630{
631 if (event->type() != MythEvent::kMythEventMessage)
632 return;
633
634 // UPnP events
635 auto *me = dynamic_cast<MythEvent *>(event);
636 if (me == nullptr)
637 return;
638
639 const QString& ev = me->Message();
640
641 if (ev == "UPNP_STARTSCAN")
642 {
644 return;
645 }
646 if (ev == "UPNP_BROWSEOBJECT")
647 {
648 if (me->ExtraDataCount() == 2)
649 {
650 QUrl url;
651 const QString& usn = me->ExtraData(0);
652 const QString& objectid = me->ExtraData(1);
653 m_lock.lock();
654 if (m_servers.contains(usn))
655 {
656 url = m_servers[usn]->m_controlURL;
657 LOG(VB_GENERAL, LOG_INFO, QString("UPNP_BROWSEOBJECT: %1->%2")
658 .arg(m_servers[usn]->m_friendlyName, objectid));
659 }
660 m_lock.unlock();
661 if (!url.isEmpty())
662 SendBrowseRequest(url, objectid);
663 }
664 return;
665 }
666 if (ev == "UPNP_EVENT")
667 {
668 auto *info = (MythInfoMapEvent*)event;
669 if (!info)
670 return;
671 if (!info->GetInfoMap())
672 return;
673
674 QString usn = info->GetInfoMap()->value("usn");
675 QString id = info->GetInfoMap()->value("SystemUpdateID");
676 if (usn.isEmpty() || id.isEmpty())
677 return;
678
679 m_lock.lock();
680 if (m_servers.contains(usn))
681 {
682 int newid = id.toInt();
683 if (m_servers[usn]->m_systemUpdateID != newid)
684 {
685 m_scanComplete &= m_servers[usn]->ResetContent(newid);
686 LOG(VB_GENERAL, LOG_INFO, LOC +
687 QString("New SystemUpdateID '%1' for %2").arg(id, usn));
688 Debug();
689 }
690 }
691 m_lock.unlock();
692 return;
693 }
694
695 // process SSDP cache updates
696 QString uri = me->ExtraDataCount() > 0 ? me->ExtraData(0) : QString();
697 QString usn = me->ExtraDataCount() > 1 ? me->ExtraData(1) : QString();
698
699 // FIXME UPNP version comparision done wrong, we are using urn:schemas-upnp-org:device:MediaServer:4 ourselves
700 if (uri == "urn:schemas-upnp-org:device:MediaServer:1")
701 {
702 QString url = (ev == "SSDP_ADD") ? me->ExtraData(2) : QString();
703 AddServer(usn, url);
704 }
705}
706
711void UPNPScanner::timerEvent(QTimerEvent * event)
712{
713 int id = event->timerId();
714 if (id)
715 killTimer(id);
716
717 std::chrono::seconds timeout = 0s;
718 QString usn;
719
720 m_lock.lock();
721 for (auto it = m_servers.cbegin(); it != m_servers.cend(); ++it)
722 {
723 if (it.value()->m_renewalTimerId == id)
724 {
725 it.value()->m_renewalTimerId = 0;
726 usn = it.key();
727 if (m_subscription)
729 }
730 }
731 m_lock.unlock();
732
733 if (timeout > 0s)
734 {
736 LOG(VB_GENERAL, LOG_INFO, LOC +
737 QString("Re-subscribed for %1 seconds to %2")
738 .arg(timeout.count()).arg(usn));
739 }
740}
741
746{
747 m_lock.lock();
748 if (m_updateTimer && !m_updateTimer->isActive())
749 m_updateTimer->start(200ms);
750 m_lock.unlock();
751}
752
757void UPNPScanner::CheckFailure(const QUrl &url)
758{
759 m_lock.lock();
760 for (auto *server : std::as_const(m_servers))
761 {
762 if (server->m_serverURL == url && server->m_connectionAttempts == MAX_ATTEMPTS)
763 {
764 Debug();
765 break;
766 }
767 }
768 m_lock.unlock();
769}
770
775{
776 m_lock.lock();
777 LOG(VB_UPNP, LOG_INFO, LOC + QString("%1 media servers discovered:")
778 .arg(m_servers.size()));
779 for (auto *server: std::as_const(m_servers))
780 {
781 QString status = "Probing";
782 if (server->m_controlURL.toString().isEmpty())
783 {
784 if (server->m_connectionAttempts >= MAX_ATTEMPTS)
785 status = "Failed";
786 }
787 else
788 {
789 status = "Yes";
790 }
791 LOG(VB_UPNP, LOG_INFO, LOC +
792 QString("'%1' Connected: %2 Subscribed: %3 SystemUpdateID: "
793 "%4 timerId: %5")
794 .arg(server->m_friendlyName, status,
795 server->m_subscribed ? "Yes" : "No",
796 QString::number(server->m_systemUpdateID),
797 QString::number(server->m_renewalTimerId)));
798 }
799 m_lock.unlock();
800}
801
811{
812 QMutexLocker locker(&m_lock);
813
814 bool complete = true;
815 for (auto *server : std::as_const(m_servers))
816 {
817 if (server->m_subscribed)
818 {
819 // limit browse requests to one active per server
820 if (m_browseRequests.contains(server->m_controlURL))
821 {
822 complete = false;
823 continue;
824 }
825
826 QString next = server->NextUnbrowsed();
827 if (!next.isEmpty())
828 {
829 complete = false;
830 SendBrowseRequest(server->m_controlURL, next);
831 continue;
832 }
833
834 LOG(VB_UPNP, LOG_INFO, LOC + QString("Scan completed for %1")
835 .arg(server->m_friendlyName));
836 }
837 }
838
839 if (complete)
840 {
841 LOG(VB_GENERAL, LOG_INFO, LOC +
842 QString("Media Server scan is complete."));
843 m_scanComplete = true;
844 m_fullscan = false;
845 }
846}
847
853void UPNPScanner::SendBrowseRequest(const QUrl &url, const QString &objectid)
854{
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\"");
859#if 0
860 req.setRawHeader("MAN", "\"http://schemasxmlsoap.org/soap/envelope/\"");
861 req.setRawHeader("01-SOAPACTION",
862 "\"urn:schemas-upnp-org:service:ContentDirectory:1#Browse\"");
863#endif
864
865 QByteArray body;
866 QTextStream data(&body);
867#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
868 data.setCodec(QTextCodec::codecForName("UTF-8"));
869#else
870 data.setEncoding(QStringConverter::Utf8);
871#endif
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";
885 data.flush();
886
887 m_lock.lock();
888 QNetworkReply *reply = m_network->post(req, body);
889 if (reply)
890 m_browseRequests.insert(url, reply);
891 m_lock.unlock();
892}
893
899void UPNPScanner::AddServer(const QString &usn, const QString &url)
900{
901 if (url.isEmpty())
902 {
903 RemoveServer(usn);
904 return;
905 }
906
907 // sometimes initialisation is too early and m_masterHost is empty
908 if (m_masterHost.isEmpty())
909 {
912 }
913
914 QUrl qurl(url);
915 if (!qurl.isValid())
916 {
917 LOG(VB_UPNP, LOG_INFO, LOC + "Ignoring invalid url: " + url);
918 return;
919 }
920 if (qurl.host() == m_masterHost && qurl.port() == m_masterPort)
921 {
922 LOG(VB_UPNP, LOG_INFO, LOC + "Ignoring master backend.");
923 return;
924 }
925
926 m_lock.lock();
927 if (!m_servers.contains(usn))
928 {
929 m_servers.insert(usn, new UpnpMediaServer(qurl));
930 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Adding: %1").arg(usn));
932 }
933 m_lock.unlock();
934}
935
939void UPNPScanner::RemoveServer(const QString &usn)
940{
941 m_lock.lock();
942 if (m_servers.contains(usn))
943 {
944 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Removing: %1").arg(usn));
945 UpnpMediaServer* old = m_servers[usn];
946 if (old->m_renewalTimerId)
947 killTimer(old->m_renewalTimerId);
948 m_servers.remove(usn);
949 delete old;
950 if (m_subscription)
952 }
953 m_lock.unlock();
954
955 Debug();
956}
957
961void UPNPScanner::ScheduleRenewal(const QString &usn, std::chrono::seconds timeout)
962{
963 // sanitise the timeout
964 std::chrono::seconds twelvehours { 12h };
965 std::chrono::seconds renew = std::clamp(timeout - 10s, 10s, twelvehours);
966
967 m_lock.lock();
968 if (m_servers.contains(usn))
969 m_servers[usn]->m_renewalTimerId = startTimer(renew);
970 m_lock.unlock();
971}
972
977void UPNPScanner::ParseBrowse(const QUrl &url, QNetworkReply *reply)
978{
979 QByteArray data = reply->readAll();
980 if (data.isEmpty())
981 return;
982
983 // Open the response for parsing
984 auto *parent = new QDomDocument();
985#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
986 QString errorMessage;
987 int errorLine = 0;
988 int errorColumn = 0;
989 if (!parent->setContent(data, false, &errorMessage, &errorLine,
990 &errorColumn))
991 {
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));
995 delete parent;
996 return;
997 }
998#else
999 auto parseResult = parent->setContent(data);
1000 if (!parseResult)
1001 {
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));
1006 delete parent;
1007 return;
1008 }
1009#endif
1010
1011 LOG(VB_UPNP, LOG_INFO, "\n\n" + parent->toString(4) + "\n\n");
1012
1013 // pull out the actual result
1014 QDomDocument *result = nullptr;
1015 uint num = 0;
1016 uint total = 0;
1017 uint updateid = 0;
1018 QDomElement docElem = parent->documentElement();
1019 QDomNode n = docElem.firstChild();
1020 if (!n.isNull())
1021 result = FindResult(n, num, total, updateid);
1022 delete parent;
1023
1024 if (!result || num < 1 || total < 1)
1025 {
1026 LOG(VB_GENERAL, LOG_ERR, LOC +
1027 QString("Failed to find result for %1") .arg(url.toString()));
1028 return;
1029 }
1030
1031 // determine the 'server' which requested the browse
1032 m_lock.lock();
1033
1034 UpnpMediaServer* server = nullptr;
1035 auto it = std::find_if(m_servers.cbegin(), m_servers.cend(),
1036 [&url](UpnpMediaServer* s){ return url == s->m_controlURL;} );
1037 if (it != m_servers.cend())
1038 server = it.value();
1039
1040 // discard unmatched responses
1041 if (!server)
1042 {
1043 m_lock.unlock();
1044 LOG(VB_GENERAL, LOG_ERR, LOC +
1045 QString("Received unknown response for %1").arg(url.toString()));
1046 return;
1047 }
1048
1049 // check the update ID
1050 if (server->m_systemUpdateID != (int)updateid)
1051 {
1052 // if this is not the root container, this browse will now fail
1053 // as the appropriate parentID will not be found
1054 LOG(VB_GENERAL, LOG_ERR, LOC +
1055 QString("%1 updateID changed during browse (old %2 new %3)")
1056 .arg(server->m_friendlyName).arg(server->m_systemUpdateID)
1057 .arg(updateid));
1058 m_scanComplete &= server->ResetContent(updateid);
1059 Debug();
1060 }
1061
1062 // find containers (directories) and actual items and add them and reset
1063 // the parent when we have found the first item
1064 bool reset = true;
1065 docElem = result->documentElement();
1066 n = docElem.firstChild();
1067 while (!n.isNull())
1068 {
1069 FindItems(n, *server, reset);
1070 n = n.nextSibling();
1071 }
1072 delete result;
1073
1074 m_lock.unlock();
1075}
1076
1078 bool &resetparent)
1079{
1080 QDomElement node = n.toElement();
1081 if (node.isNull())
1082 return;
1083
1084 if (node.tagName() == "container")
1085 {
1086 QString title = "ERROR";
1087 QDomNode next = node.firstChild();
1088 while (!next.isNull())
1089 {
1090 QDomElement container = next.toElement();
1091 if (!container.isNull() && container.tagName() == "title")
1092 title = container.text();
1093 next = next.nextSibling();
1094 }
1095
1096 QString thisid = node.attribute("id", "ERROR");
1097 QString parentid = node.attribute("parentID", "ERROR");
1098 MediaServerItem container =
1099 MediaServerItem(thisid, parentid, title, QString());
1100 MediaServerItem *parent = content.Find(parentid);
1101 if (parent)
1102 {
1103 if (resetparent)
1104 {
1105 parent->Reset();
1106 resetparent = false;
1107 }
1108 parent->m_scanned = true;
1109 parent->Add(container);
1110 }
1111 return;
1112 }
1113
1114 if (node.tagName() == "item")
1115 {
1116 QString title = "ERROR";
1117 QString url = "ERROR";
1118 QDomNode next = node.firstChild();
1119 while (!next.isNull())
1120 {
1121 QDomElement item = next.toElement();
1122 if (!item.isNull())
1123 {
1124 if(item.tagName() == "res")
1125 url = item.text();
1126 if(item.tagName() == "title")
1127 title = item.text();
1128 }
1129 next = next.nextSibling();
1130 }
1131
1132 QString thisid = node.attribute("id", "ERROR");
1133 QString parentid = node.attribute("parentID", "ERROR");
1134 MediaServerItem item =
1135 MediaServerItem(thisid, parentid, title, url);
1136 item.m_scanned = true;
1137 MediaServerItem *parent = content.Find(parentid);
1138 if (parent)
1139 {
1140 if (resetparent)
1141 {
1142 parent->Reset();
1143 resetparent = false;
1144 }
1145 parent->m_scanned = true;
1146 parent->Add(item);
1147 }
1148 return;
1149 }
1150
1151 QDomNode next = node.firstChild();
1152 while (!next.isNull())
1153 {
1154 FindItems(next, content, resetparent);
1155 next = next.nextSibling();
1156 }
1157}
1158
1159QDomDocument* UPNPScanner::FindResult(const QDomNode &n, uint &num,
1160 uint &total, uint &updateid)
1161{
1162 QDomDocument *result = nullptr;
1163 QDomElement node = n.toElement();
1164 if (node.isNull())
1165 return nullptr;
1166
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)
1174 {
1175 result = new QDomDocument();
1176#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
1177 QString errorMessage;
1178 int errorLine = 0;
1179 int errorColumn = 0;
1180 if (!result->setContent(node.text(), true, &errorMessage, &errorLine, &errorColumn))
1181 {
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));
1185 delete result;
1186 result = nullptr;
1187 }
1188#else
1189 auto parseResult =
1190 result->setContent(node.text(),
1191 QDomDocument::ParseOption::UseNamespaceProcessing);
1192 if (!parseResult)
1193 {
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));
1198 delete result;
1199 result = nullptr;
1200 }
1201#endif
1202 }
1203
1204 QDomNode next = node.firstChild();
1205 while (!next.isNull())
1206 {
1207 QDomDocument *res = FindResult(next, num, total, updateid);
1208 if (res)
1209 result = res;
1210 next = next.nextSibling();
1211 }
1212 return result;
1213}
1214
1215static QUrl urlAddBaseAndPath (QUrl base, const QUrl& relative)
1216{
1217 base.setFragment("");
1218 base.setQuery("");
1219 base.setPath(relative.path());
1220 return base;
1221}
1222
1227bool UPNPScanner::ParseDescription(const QUrl &url, QNetworkReply *reply)
1228{
1229 if (url.isEmpty() || !reply)
1230 return false;
1231
1232 QByteArray data = reply->readAll();
1233 if (data.isEmpty())
1234 {
1235 LOG(VB_GENERAL, LOG_ERR, LOC +
1236 QString("%1 returned an empty device description.")
1237 .arg(url.toString()));
1238 return false;
1239 }
1240
1241 // parse the device description
1242 QUrl URLBase;
1243 QUrl controlURL;
1244 QUrl eventURL;
1245 QString friendlyName = QString("Unknown");
1246
1247 QDomDocument doc;
1248#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
1249 QString errorMessage;
1250 int errorLine = 0;
1251 int errorColumn = 0;
1252 if (!doc.setContent(data, false, &errorMessage, &errorLine, &errorColumn))
1253 {
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));
1259 return false;
1260 }
1261#else
1262 auto parseResult = doc.setContent(data);
1263 if (!parseResult)
1264 {
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));
1271 return false;
1272 }
1273#endif
1274
1275 QDomElement docElem = doc.documentElement();
1276 QDomNode n = docElem.firstChild();
1277 while (!n.isNull())
1278 {
1279 QDomElement e1 = n.toElement();
1280 if (!e1.isNull())
1281 {
1282 if(e1.tagName() == "device")
1283 ParseDevice(e1, controlURL, eventURL, friendlyName);
1284 if (e1.tagName() == "URLBase")
1285 URLBase = QUrl(e1.text());
1286 }
1287 n = n.nextSibling();
1288 }
1289
1290 if (!controlURL.isValid())
1291 {
1292 LOG(VB_UPNP, LOG_ERR, LOC +
1293 QString("Failed to parse device description for %1")
1294 .arg(url.toString()));
1295 return false;
1296 }
1297
1298 // if no URLBase was provided, use the known url
1299 if (!URLBase.isValid())
1300 {
1301 URLBase = url;
1302 URLBase.setPath("");
1303 URLBase.setFragment("");
1304 URLBase.setQuery("");
1305 }
1306
1307 if (controlURL.isRelative())
1308 controlURL = urlAddBaseAndPath(URLBase, controlURL);
1309 if (eventURL.isRelative())
1310 eventURL = urlAddBaseAndPath(URLBase, eventURL);
1311
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()));
1316
1317 // update the server details. If the server has gone away since the request
1318 // was posted, this will silently fail and we won't try again
1319 QString usn;
1320 std::chrono::seconds timeout = 0s;
1321
1322 m_lock.lock();
1323 auto it = std::find_if(m_servers.cbegin(), m_servers.cend(),
1324 [&url](UpnpMediaServer* server){ return url == server->m_serverURL;} );
1325 if (it != m_servers.cend())
1326 {
1327 usn = it.key();
1328 UpnpMediaServer* server = it.value();
1329 server->m_controlURL = controlURL;
1330 server->m_eventSubURL = eventURL;
1331 server->m_eventSubPath = eventURL;
1332 server->m_friendlyName = friendlyName;
1333 server->m_name = friendlyName;
1334 }
1335
1336 if (m_subscription && !usn.isEmpty())
1337 {
1338 timeout = m_subscription->Subscribe(usn, eventURL, eventURL.toString());
1339 m_servers[usn]->m_subscribed = (timeout > 0s);
1340 }
1341 m_lock.unlock();
1342
1343 if (timeout > 0s)
1344 {
1345 LOG(VB_GENERAL, LOG_INFO, LOC +
1346 QString("Subscribed for %1 seconds to %2") .arg(timeout.count()).arg(usn));
1348 // we only scan servers we are subscribed to - and the scan is now
1349 // incomplete
1350 m_scanComplete = false;
1351 }
1352
1353 Debug();
1354 return true;
1355}
1356
1357
1358void UPNPScanner::ParseDevice(QDomElement &element, QUrl &controlURL,
1359 QUrl &eventURL, QString &friendlyName)
1360{
1361 QDomNode dev = element.firstChild();
1362 while (!dev.isNull())
1363 {
1364 QDomElement e = dev.toElement();
1365 if (!e.isNull())
1366 {
1367 if (e.tagName() == "friendlyName")
1368 friendlyName = e.text();
1369 if (e.tagName() == "serviceList")
1370 ParseServiceList(e, controlURL, eventURL);
1371 }
1372 dev = dev.nextSibling();
1373 }
1374}
1375
1376void UPNPScanner::ParseServiceList(QDomElement &element, QUrl &controlURL,
1377 QUrl &eventURL)
1378{
1379 QDomNode list = element.firstChild();
1380 while (!list.isNull())
1381 {
1382 QDomElement e = list.toElement();
1383 if (!e.isNull())
1384 if (e.tagName() == "service")
1385 ParseService(e, controlURL, eventURL);
1386 list = list.nextSibling();
1387 }
1388}
1389
1390void UPNPScanner::ParseService(QDomElement &element, QUrl &controlURL,
1391 QUrl &eventURL)
1392{
1393 bool iscds = false;
1394 QUrl control_url;
1395 QUrl event_url;
1396 QDomNode service = element.firstChild();
1397
1398 while (!service.isNull())
1399 {
1400 QDomElement e = service.toElement();
1401 if (!e.isNull())
1402 {
1403 if (e.tagName() == "serviceType")
1404 // FIXME UPNP version comparision done wrong, we are using urn:schemas-upnp-org:device:MediaServer:4 ourselves
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());
1410 }
1411 service = service.nextSibling();
1412 }
1413
1414 if (iscds)
1415 {
1416 controlURL = control_url;
1417 eventURL = event_url;
1418 }
1419}
This is a wrapper around QThread that does several additional things.
Definition: mthread.h:49
bool isRunning(void) const
Definition: mthread.cpp:261
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:281
void quit(void)
calls exit(0)
Definition: mthread.cpp:293
bool wait(std::chrono::milliseconds time=std::chrono::milliseconds::max())
Wait for the MThread to exit, with a maximum timeout.
Definition: mthread.cpp:298
QThread * qthread(void)
Returns the thread, this will always return the same pointer no matter how often you restart the thre...
Definition: mthread.cpp:231
QString m_parentid
Definition: upnpscanner.h:38
QMap< QString, MediaServerItem > m_children
Definition: upnpscanner.h:42
QString NextUnbrowsed(void) const
Definition: upnpscanner.cpp:28
bool Add(const MediaServerItem &item)
Definition: upnpscanner.cpp:64
QString m_name
Definition: upnpscanner.h:39
void Reset(void)
Definition: upnpscanner.cpp:74
MediaServerItem * Find(QString &id)
Definition: upnpscanner.cpp:49
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.
Definition: mythevent.h:17
const QString & Message() const
Definition: mythevent.h:65
static const Type kMythEventMessage
Definition: mythevent.h:79
void addListener(QObject *listener)
Add a listener to the observable.
void removeListener(QObject *listener)
Remove a listener to the observable.
static SSDPCache * Instance()
Definition: ssdpcache.cpp:285
UPnPScanner detects UPnP Media Servers available on the local network (via the UPnP SSDP cache),...
Definition: upnpscanner.h:46
static UPNPScanner * Instance(UPNPSubscription *sub=nullptr)
Returns the global UPNPScanner instance if it has been enabled or nullptr if UPNPScanner is currently...
QString m_masterHost
Definition: upnpscanner.h:125
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...
bool m_scanComplete
Definition: upnpscanner.h:128
static QRecursiveMutex * gUPNPScannerLock
Definition: upnpscanner.h:111
static bool gUPNPScannerEnabled
Definition: upnpscanner.h:109
void CheckStatus(void)
Removes media servers that can no longer be found in the SSDP cache.
~UPNPScanner() override
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
Definition: upnpscanner.h:119
void RemoveServer(const QString &usn)
QHash< QString, UpnpMediaServer * > m_servers
Definition: upnpscanner.h:115
static void ParseServiceList(QDomElement &element, QUrl &controlURL, QUrl &eventURL)
int m_masterPort
Definition: upnpscanner.h:126
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
Definition: upnpscanner.h:110
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
Definition: upnpscanner.h:116
UPNPSubscription * m_subscription
Definition: upnpscanner.h:113
void Stop(void)
Stops scanning.
void Debug(void)
void BrowseNextContainer(void)
For each known media server, find the next container which needs to be browsed and trigger sending of...
QTimer * m_watchdogTimer
Definition: upnpscanner.h:123
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
Definition: upnpscanner.h:108
UPNPScanner(UPNPSubscription *sub)
Definition: upnpscanner.h:75
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)
QTimer * m_updateTimer
Definition: upnpscanner.h:122
void Update(void)
Iterates through the list of known servers and initialises a connection by requesting the device desc...
QMultiMap< QUrl, QNetworkReply * > m_browseRequests
Definition: upnpscanner.h:120
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.
bool m_fullscan
Definition: upnpscanner.h:129
QRecursiveMutex m_lock
Definition: upnpscanner.h:114
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)
A simple wrapper containing details about a UPnP Media Server.
Definition: upnpscanner.cpp:85
UpnpMediaServer(QUrl URL)
Definition: upnpscanner.cpp:92
bool ResetContent(int new_id)
Definition: upnpscanner.cpp:99
QString m_friendlyName
uint8_t m_connectionAttempts
std::list< VideoMetadataPtr > metadata_list
void addEntry(const smart_meta_node &entry)
smart_dir_node addSubDir(const QString &subdir, const QString &name="", const QString &host="", const QString &prefix="", const QVariant &data=QVariant())
void SetData(const QVariant &data)
void setPathRoot(bool is_root=true)
T * get() const
Definition: quicksp.h:73
unsigned int uint
Definition: compat.h:60
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
dictionary info
Definition: azlyrics.py:7
STL namespace.
static eu8 clamp(eu8 value, eu8 low, eu8 high)
Definition: pxsup2dast.c:206
#define LOC
Definition: upnpscanner.cpp:23
static QUrl urlAddBaseAndPath(QUrl base, const QUrl &relative)
static constexpr uint8_t MAX_ATTEMPTS
Definition: upnpscanner.cpp:26
simple_ref_ptr< meta_data_node > smart_meta_node