MythTV master
upnpscanner.cpp
Go to the documentation of this file.
1// C++
2#include <chrono> // for milliseconds
3#include <thread> // for sleep_for
4#include <utility>
5
6// Qt
7#include <QCoreApplication>
8#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
9#include <QStringConverter>
10#else
11#include <QTextCodec>
12#endif
13
14// MythTV
17#include "libmythupnp/ssdp.h"
18
19// MythFrontend
20#include "upnpscanner.h"
21
22#define LOC QString("UPnPScan: ")
23#define ERR QString("UPnPScan error: ")
24
25static constexpr uint8_t MAX_ATTEMPTS { 5 };
26
28{
29 // items don't need scanning
30 if (!m_url.isEmpty())
31 return {};
32
33 // scan this container
34 if (!m_scanned)
35 return m_id;
36
37 // scan children
38 QMutableMapIterator<QString,MediaServerItem> it(m_children);
39 while (it.hasNext())
40 {
41 it.next();
42 QString result = it.value().NextUnbrowsed();
43 if (!result.isEmpty())
44 return result;
45 }
46
47 return {};
48}
49
51{
52 if (m_id == id)
53 return this;
54
55 QMutableMapIterator<QString,MediaServerItem> it(m_children);
56 while (it.hasNext())
57 {
58 it.next();
59 MediaServerItem* result = it.value().Find(id);
60 if (result)
61 return result;
62 }
63
64 return nullptr;
65}
66
68{
69 if (m_id == item.m_parentid)
70 {
71 m_children.insert(item.m_id, item);
72 return true;
73 }
74 return false;
75}
76
78{
79 m_children.clear();
80 m_scanned = false;
81}
82
88{
89 public:
91 : MediaServerItem(QString("0"), QString(), QString(), QString()),
92 m_friendlyName(QString("Unknown"))
93 {
94 }
95 explicit UpnpMediaServer(QUrl URL)
96 : MediaServerItem(QString("0"), QString(), QString(), QString()),
97 m_serverURL(std::move(URL)),
98 m_friendlyName(QString("Unknown"))
99 {
100 }
101
102 bool ResetContent(int new_id)
103 {
104 bool result = true;
105 if (m_systemUpdateID != -1)
106 {
107 result = false;
108 Reset();
109 }
110 m_systemUpdateID = new_id;
111 return result;
112 }
113
120 bool m_subscribed {false};
123};
124
128QRecursiveMutex* UPNPScanner::gUPNPScannerLock = new QRecursiveMutex();
129
141{
142 Stop();
143}
144
150{
151 QMutexLocker locker(gUPNPScannerLock);
152 gUPNPScannerEnabled = enable;
153 Instance(sub);
154}
155
162{
163 QMutexLocker locker(gUPNPScannerLock);
165 {
167 {
170 }
171 delete gUPNPScannerThread;
172 gUPNPScannerThread = nullptr;
173 delete gUPNPScanner;
174 gUPNPScanner = nullptr;
175 return nullptr;
176 }
177
179 gUPNPScannerThread = new MThread("UPnPScanner");
180 if (!gUPNPScanner)
181 gUPNPScanner = new UPNPScanner(sub);
182
184 {
185 gUPNPScanner->moveToThread(gUPNPScannerThread->qthread());
186 QObject::connect(
187 gUPNPScannerThread->qthread(), &QThread::started,
189 gUPNPScannerThread->start(QThread::LowestPriority);
190 }
191
192 return gUPNPScanner;
193}
200{
201 m_fullscan = true;
202 auto *me = new MythEvent(QString("UPNP_STARTSCAN"));
203 qApp->postEvent(this, me);
204}
205
213 meta_dir_node *node)
214{
215 // nothing to see..
216 QMap<QString,QString> servers = ServerList();
217 if (servers.isEmpty())
218 return;
219
220 // Add MediaServers
221 LOG(VB_GENERAL, LOG_INFO, QString("Adding MediaServer metadata."));
222
223 smart_dir_node mediaservers = node->addSubDir(tr("Media Servers"));
224 mediaservers->setPathRoot();
225
226 m_lock.lock();
227 QMutableHashIterator<QString,UpnpMediaServer*> it(m_servers);
228 while (it.hasNext())
229 {
230 it.next();
231 if (!it.value()->m_subscribed)
232 continue;
233
234 QString usn = it.key();
235 GetServerContent(usn, it.value(), list, mediaservers.get());
236 }
237 m_lock.unlock();
238}
239
245 meta_dir_node *node)
246{
247 // nothing to see..
248 QMap<QString,QString> servers = ServerList();
249 if (servers.isEmpty())
250 return;
251
252 // Start scanning if it isn't already running
254
255 // wait for the scanner to complete - with a 30 second timeout
256 LOG(VB_GENERAL, LOG_INFO, LOC + "Waiting for scan to complete.");
257
258 int count = 0;
259 while (!m_scanComplete && (count++ < 300))
260 std::this_thread::sleep_for(100ms);
261
262 // some scans may just take too long (PlayOn)
263 if (!m_scanComplete)
264 LOG(VB_GENERAL, LOG_ERR, LOC + "MediaServer scan is incomplete.");
265 else
266 LOG(VB_GENERAL, LOG_INFO, LOC + "MediaServer scanning finished.");
267
268
269 smart_dir_node mediaservers = node->addSubDir(tr("Media Servers"));
270 mediaservers->setPathRoot();
271
272 m_lock.lock();
273 QMutableHashIterator<QString,UpnpMediaServer*> it(m_servers);
274 while (it.hasNext())
275 {
276 it.next();
277 if (!it.value()->m_subscribed)
278 continue;
279
280 QString usn = it.key();
281 GetServerContent(usn, it.value(), list, mediaservers.get());
282 }
283 m_lock.unlock();
284}
285
286bool UPNPScanner::GetMetadata(QVariant &data)
287{
288 // we need a USN and objectID
289 if (!data.canConvert<QStringList>())
290 return false;
291
292 QStringList list = data.toStringList();
293 if (list.size() != 2)
294 return false;
295
296 const QString& usn = list[0];
297 QString object = list[1];
298
299 m_lock.lock();
300 bool valid = m_servers.contains(usn);
301 if (valid)
302 {
303 MediaServerItem* item = m_servers[usn]->Find(object);
304 valid = item ? !item->m_scanned : false;
305 }
306 m_lock.unlock();
307 if (!valid)
308 return false;
309
310 auto *me = new MythEvent("UPNP_BROWSEOBJECT", list);
311 qApp->postEvent(this, me);
312
313 int count = 0;
314 bool found = false;
315 LOG(VB_GENERAL, LOG_INFO, "START");
316 while (!found && (count++ < 100)) // 10 seconds
317 {
318 std::this_thread::sleep_for(100ms);
319 m_lock.lock();
320 if (m_servers.contains(usn))
321 {
322 MediaServerItem *item = m_servers[usn]->Find(object);
323 if (item)
324 {
325 found = item->m_scanned;
326 }
327 else
328 {
329 LOG(VB_GENERAL, LOG_INFO, QString("Item went away..."));
330 found = true;
331 }
332 }
333 else
334 {
335 LOG(VB_GENERAL, LOG_INFO,
336 QString("Server went away while browsing."));
337 found = true;
338 }
339 m_lock.unlock();
340 }
341 LOG(VB_GENERAL, LOG_INFO, "END");
342 return true;
343}
344
353 meta_dir_node *node)
354{
355 if (!content->m_scanned)
356 {
357 smart_dir_node subnode = node->addSubDir(content->m_name);
358
359 QStringList data;
360 data << usn;
361 data << content->m_id;
362 subnode->SetData(data);
363
365 item->SetTitle(QString("Dummy"));
366 list->push_back(item);
367 subnode->addEntry(smart_meta_node(new meta_data_node(item.get())));
368 return;
369 }
370
371 node->SetData(QVariant());
372
373 if (content->m_url.isEmpty())
374 {
375 smart_dir_node container = node->addSubDir(content->m_name);
376 QMutableMapIterator<QString,MediaServerItem> it(content->m_children);
377 while (it.hasNext())
378 {
379 it.next();
380 GetServerContent(usn, &it.value(), list, container.get());
381 }
382 return;
383 }
384
386 item->SetTitle(content->m_name);
387 list->push_back(item);
388 node->addEntry(smart_meta_node(new meta_data_node(item.get())));
389}
390
396QMap<QString,QString> UPNPScanner::ServerList(void)
397{
398 QMap<QString,QString> servers;
399 m_lock.lock();
400 QHashIterator<QString,UpnpMediaServer*> it(m_servers);
401 while (it.hasNext())
402 {
403 it.next();
404 servers.insert(it.key(), it.value()->m_friendlyName);
405 }
406 m_lock.unlock();
407 return servers;
408}
409
416{
417 m_lock.lock();
418
419 // create our network handler
420 m_network = new QNetworkAccessManager();
421 connect(m_network, &QNetworkAccessManager::finished,
423
424 // listen for SSDP updates
425 SSDP::AddListener(this);
426
427 // listen for subscriptions and events
428 if (m_subscription)
430
431 // create our update timer (driven by AddServer and ParseDescription)
432 m_updateTimer = new QTimer(this);
433 m_updateTimer->setSingleShot(true);
435
436 // create our watchdog timer (checks for stale servers)
437 m_watchdogTimer = new QTimer(this);
439 m_watchdogTimer->start(10s);
440
441 // avoid connecting to the master backend
444
445 m_lock.unlock();
446 LOG(VB_GENERAL, LOG_INFO, LOC + "Started");
447}
448
454{
455 m_lock.lock();
456
457 // stop listening
459 if (m_subscription)
461
462 // disable updates
463 if (m_updateTimer)
464 m_updateTimer->stop();
465 if (m_watchdogTimer)
466 m_watchdogTimer->stop();
467
468 // cleanup our servers and subscriptions
469 QHashIterator<QString,UpnpMediaServer*> it(m_servers);
470 while (it.hasNext())
471 {
472 it.next();
473 if (m_subscription && it.value()->m_subscribed)
474 m_subscription->Unsubscribe(it.key());
475 if (it.value()->m_renewalTimerId)
476 killTimer(it.value()->m_renewalTimerId);
477 delete it.value();
478 }
479 m_servers.clear();
480
481 // cleanup the network
482 for (QNetworkReply *reply : std::as_const(m_descriptionRequests))
483 {
484 reply->abort();
485 delete reply;
486 }
487 m_descriptionRequests.clear();
488 for (QNetworkReply *reply : std::as_const(m_browseRequests))
489 {
490 reply->abort();
491 delete reply;
492 }
493 m_browseRequests.clear();
494 delete m_network;
495 m_network = nullptr;
496
497 // delete the timers
498 delete m_updateTimer;
499 delete m_watchdogTimer;
500 m_updateTimer = nullptr;
501 m_watchdogTimer = nullptr;
502
503 m_lock.unlock();
504 LOG(VB_GENERAL, LOG_INFO, LOC + "Finished");
505}
506
513{
514 // decide which servers still need to be checked
515 m_lock.lock();
516 if (m_servers.isEmpty())
517 {
518 m_lock.unlock();
519 return;
520 }
521
522 // if our network queue is full, then we may need to come back later
523 bool reschedule = false;
524
525 QHashIterator<QString,UpnpMediaServer*> it(m_servers);
526 while (it.hasNext())
527 {
528 it.next();
529 if ((it.value()->m_connectionAttempts < MAX_ATTEMPTS) &&
530 (it.value()->m_controlURL.isEmpty()))
531 {
532 bool sent = false;
533 QUrl url = it.value()->m_url;
534 if (!m_descriptionRequests.contains(url) &&
535 (m_descriptionRequests.empty()) &&
536 url.isValid())
537 {
538 QNetworkReply *reply = m_network->get(QNetworkRequest(url));
539 if (reply)
540 {
541 sent = true;
542 m_descriptionRequests.insert(url, reply);
543 it.value()->m_connectionAttempts++;
544 }
545 }
546 if (!sent)
547 reschedule = true;
548 }
549 }
550
551 if (reschedule)
553 m_lock.unlock();
554}
555
561{
562 // FIXME
563 // Remove stale servers - the SSDP cache code does not send out removal
564 // notifications for expired (rather than explicitly closed) connections
565 m_lock.lock();
566 QMutableHashIterator<QString,UpnpMediaServer*> it(m_servers);
567 while (it.hasNext())
568 {
569 it.next();
570 // FIXME UPNP version comparision done wrong, we are using urn:schemas-upnp-org:device:MediaServer:4 ourselves
571 if (!SSDP::Find("urn:schemas-upnp-org:device:MediaServer:1", it.key()))
572 {
573 LOG(VB_UPNP, LOG_INFO, LOC + QString("%1 no longer in SSDP cache. Removing")
574 .arg(it.value()->m_serverURL.toString()));
575 UpnpMediaServer* last = it.value();
576 it.remove();
577 delete last;
578 }
579 }
580 m_lock.unlock();
581}
582
588void UPNPScanner::replyFinished(QNetworkReply *reply)
589{
590 if (!reply)
591 return;
592
593 QUrl url = reply->url();
594 bool valid = reply->error() == QNetworkReply::NoError;
595
596 if (!valid)
597 {
598 LOG(VB_UPNP, LOG_ERR, LOC +
599 QString("Network request for '%1' returned error '%2'")
600 .arg(url.toString(), reply->errorString()));
601 }
602
603 bool description = false;
604 bool browse = false;
605
606 m_lock.lock();
607 if (m_descriptionRequests.contains(url, reply))
608 {
609 m_descriptionRequests.remove(url, reply);
610 description = true;
611 }
612 else if (m_browseRequests.contains(url, reply))
613 {
614 m_browseRequests.remove(url, reply);
615 browse = true;
616 }
617 m_lock.unlock();
618
619 if (browse && valid)
620 {
621 ParseBrowse(url, reply);
622 if (m_fullscan)
624 }
625 else if (description)
626 {
627 if (!valid || !ParseDescription(url, reply))
628 {
629 // if there will be no more attempts, update the logs
630 CheckFailure(url);
631 // try again
633 }
634 }
635 else
636 {
637 LOG(VB_UPNP, LOG_ERR, LOC + "Received unknown reply");
638 }
639
640 reply->deleteLater();
641}
642
646void UPNPScanner::customEvent(QEvent *event)
647{
648 if (event->type() != MythEvent::kMythEventMessage)
649 return;
650
651 // UPnP events
652 auto *me = dynamic_cast<MythEvent *>(event);
653 if (me == nullptr)
654 return;
655
656 const QString& ev = me->Message();
657
658 if (ev == "UPNP_STARTSCAN")
659 {
661 return;
662 }
663 if (ev == "UPNP_BROWSEOBJECT")
664 {
665 if (me->ExtraDataCount() == 2)
666 {
667 QUrl url;
668 const QString& usn = me->ExtraData(0);
669 const QString& objectid = me->ExtraData(1);
670 m_lock.lock();
671 if (m_servers.contains(usn))
672 {
673 url = m_servers[usn]->m_controlURL;
674 LOG(VB_GENERAL, LOG_INFO, QString("UPNP_BROWSEOBJECT: %1->%2")
675 .arg(m_servers[usn]->m_friendlyName, objectid));
676 }
677 m_lock.unlock();
678 if (!url.isEmpty())
679 SendBrowseRequest(url, objectid);
680 }
681 return;
682 }
683 if (ev == "UPNP_EVENT")
684 {
685 auto *info = (MythInfoMapEvent*)event;
686 if (!info)
687 return;
688 if (!info->GetInfoMap())
689 return;
690
691 QString usn = info->GetInfoMap()->value("usn");
692 QString id = info->GetInfoMap()->value("SystemUpdateID");
693 if (usn.isEmpty() || id.isEmpty())
694 return;
695
696 m_lock.lock();
697 if (m_servers.contains(usn))
698 {
699 int newid = id.toInt();
700 if (m_servers[usn]->m_systemUpdateID != newid)
701 {
702 m_scanComplete &= m_servers[usn]->ResetContent(newid);
703 LOG(VB_GENERAL, LOG_INFO, LOC +
704 QString("New SystemUpdateID '%1' for %2").arg(id, usn));
705 Debug();
706 }
707 }
708 m_lock.unlock();
709 return;
710 }
711
712 // process SSDP cache updates
713 QString uri = me->ExtraDataCount() > 0 ? me->ExtraData(0) : QString();
714 QString usn = me->ExtraDataCount() > 1 ? me->ExtraData(1) : QString();
715
716 // FIXME UPNP version comparision done wrong, we are using urn:schemas-upnp-org:device:MediaServer:4 ourselves
717 if (uri == "urn:schemas-upnp-org:device:MediaServer:1")
718 {
719 QString url = (ev == "SSDP_ADD") ? me->ExtraData(2) : QString();
720 AddServer(usn, url);
721 }
722}
723
728void UPNPScanner::timerEvent(QTimerEvent * event)
729{
730 int id = event->timerId();
731 if (id)
732 killTimer(id);
733
734 std::chrono::seconds timeout = 0s;
735 QString usn;
736
737 m_lock.lock();
738 QHashIterator<QString,UpnpMediaServer*> it(m_servers);
739 while (it.hasNext())
740 {
741 it.next();
742 if (it.value()->m_renewalTimerId == id)
743 {
744 it.value()->m_renewalTimerId = 0;
745 usn = it.key();
746 if (m_subscription)
748 }
749 }
750 m_lock.unlock();
751
752 if (timeout > 0s)
753 {
755 LOG(VB_GENERAL, LOG_INFO, LOC +
756 QString("Re-subscribed for %1 seconds to %2")
757 .arg(timeout.count()).arg(usn));
758 }
759}
760
765{
766 m_lock.lock();
767 if (m_updateTimer && !m_updateTimer->isActive())
768 m_updateTimer->start(200ms);
769 m_lock.unlock();
770}
771
776void UPNPScanner::CheckFailure(const QUrl &url)
777{
778 m_lock.lock();
779 QHashIterator<QString,UpnpMediaServer*> it(m_servers);
780 while (it.hasNext())
781 {
782 it.next();
783 if (it.value()->m_serverURL == url && it.value()->m_connectionAttempts == MAX_ATTEMPTS)
784 {
785 Debug();
786 break;
787 }
788 }
789 m_lock.unlock();
790}
791
796{
797 m_lock.lock();
798 LOG(VB_UPNP, LOG_INFO, LOC + QString("%1 media servers discovered:")
799 .arg(m_servers.size()));
800 QHashIterator<QString,UpnpMediaServer*> it(m_servers);
801 while (it.hasNext())
802 {
803 it.next();
804 QString status = "Probing";
805 if (it.value()->m_controlURL.toString().isEmpty())
806 {
807 if (it.value()->m_connectionAttempts >= MAX_ATTEMPTS)
808 status = "Failed";
809 }
810 else
811 {
812 status = "Yes";
813 }
814 LOG(VB_UPNP, LOG_INFO, LOC +
815 QString("'%1' Connected: %2 Subscribed: %3 SystemUpdateID: "
816 "%4 timerId: %5")
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)));
821 }
822 m_lock.unlock();
823}
824
834{
835 QMutexLocker locker(&m_lock);
836
837 QHashIterator<QString,UpnpMediaServer*> it(m_servers);
838 bool complete = true;
839 while (it.hasNext())
840 {
841 it.next();
842 if (it.value()->m_subscribed)
843 {
844 // limit browse requests to one active per server
845 if (m_browseRequests.contains(it.value()->m_controlURL))
846 {
847 complete = false;
848 continue;
849 }
850
851 QString next = it.value()->NextUnbrowsed();
852 if (!next.isEmpty())
853 {
854 complete = false;
855 SendBrowseRequest(it.value()->m_controlURL, next);
856 continue;
857 }
858
859 LOG(VB_UPNP, LOG_INFO, LOC + QString("Scan completed for %1")
860 .arg(it.value()->m_friendlyName));
861 }
862 }
863
864 if (complete)
865 {
866 LOG(VB_GENERAL, LOG_INFO, LOC +
867 QString("Media Server scan is complete."));
868 m_scanComplete = true;
869 m_fullscan = false;
870 }
871}
872
878void UPNPScanner::SendBrowseRequest(const QUrl &url, const QString &objectid)
879{
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\"");
884#if 0
885 req.setRawHeader("MAN", "\"http://schemasxmlsoap.org/soap/envelope/\"");
886 req.setRawHeader("01-SOAPACTION",
887 "\"urn:schemas-upnp-org:service:ContentDirectory:1#Browse\"");
888#endif
889
890 QByteArray body;
891 QTextStream data(&body);
892#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
893 data.setCodec(QTextCodec::codecForName("UTF-8"));
894#else
895 data.setEncoding(QStringConverter::Utf8);
896#endif
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";
910 data.flush();
911
912 m_lock.lock();
913 QNetworkReply *reply = m_network->post(req, body);
914 if (reply)
915 m_browseRequests.insert(url, reply);
916 m_lock.unlock();
917}
918
924void UPNPScanner::AddServer(const QString &usn, const QString &url)
925{
926 if (url.isEmpty())
927 {
928 RemoveServer(usn);
929 return;
930 }
931
932 // sometimes initialisation is too early and m_masterHost is empty
933 if (m_masterHost.isEmpty())
934 {
937 }
938
939 QUrl qurl(url);
940 if (qurl.host() == m_masterHost && qurl.port() == m_masterPort)
941 {
942 LOG(VB_UPNP, LOG_INFO, LOC + "Ignoring master backend.");
943 return;
944 }
945
946 m_lock.lock();
947 if (!m_servers.contains(usn))
948 {
949 m_servers.insert(usn, new UpnpMediaServer(url));
950 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Adding: %1").arg(usn));
952 }
953 m_lock.unlock();
954}
955
959void UPNPScanner::RemoveServer(const QString &usn)
960{
961 m_lock.lock();
962 if (m_servers.contains(usn))
963 {
964 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Removing: %1").arg(usn));
965 UpnpMediaServer* old = m_servers[usn];
966 if (old->m_renewalTimerId)
967 killTimer(old->m_renewalTimerId);
968 m_servers.remove(usn);
969 delete old;
970 if (m_subscription)
972 }
973 m_lock.unlock();
974
975 Debug();
976}
977
981void UPNPScanner::ScheduleRenewal(const QString &usn, std::chrono::seconds timeout)
982{
983 // sanitise the timeout
984 std::chrono::seconds twelvehours { 12h };
985 std::chrono::seconds renew = std::clamp(timeout - 10s, 10s, twelvehours);
986
987 m_lock.lock();
988 if (m_servers.contains(usn))
989 m_servers[usn]->m_renewalTimerId = startTimer(renew);
990 m_lock.unlock();
991}
992
997void UPNPScanner::ParseBrowse(const QUrl &url, QNetworkReply *reply)
998{
999 QByteArray data = reply->readAll();
1000 if (data.isEmpty())
1001 return;
1002
1003 // Open the response for parsing
1004 auto *parent = new QDomDocument();
1005#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
1006 QString errorMessage;
1007 int errorLine = 0;
1008 int errorColumn = 0;
1009 if (!parent->setContent(data, false, &errorMessage, &errorLine,
1010 &errorColumn))
1011 {
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));
1015 delete parent;
1016 return;
1017 }
1018#else
1019 auto parseResult = parent->setContent(data);
1020 if (!parseResult)
1021 {
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));
1026 delete parent;
1027 return;
1028 }
1029#endif
1030
1031 LOG(VB_UPNP, LOG_INFO, "\n\n" + parent->toString(4) + "\n\n");
1032
1033 // pull out the actual result
1034 QDomDocument *result = nullptr;
1035 uint num = 0;
1036 uint total = 0;
1037 uint updateid = 0;
1038 QDomElement docElem = parent->documentElement();
1039 QDomNode n = docElem.firstChild();
1040 if (!n.isNull())
1041 result = FindResult(n, num, total, updateid);
1042 delete parent;
1043
1044 if (!result || num < 1 || total < 1)
1045 {
1046 LOG(VB_GENERAL, LOG_ERR, LOC +
1047 QString("Failed to find result for %1") .arg(url.toString()));
1048 return;
1049 }
1050
1051 // determine the 'server' which requested the browse
1052 m_lock.lock();
1053
1054 UpnpMediaServer* server = nullptr;
1055 QHashIterator<QString,UpnpMediaServer*> it(m_servers);
1056 while (it.hasNext())
1057 {
1058 it.next();
1059 if (url == it.value()->m_controlURL)
1060 {
1061 server = it.value();
1062 break;
1063 }
1064 }
1065
1066 // discard unmatched responses
1067 if (!server)
1068 {
1069 m_lock.unlock();
1070 LOG(VB_GENERAL, LOG_ERR, LOC +
1071 QString("Received unknown response for %1").arg(url.toString()));
1072 return;
1073 }
1074
1075 // check the update ID
1076 if (server->m_systemUpdateID != (int)updateid)
1077 {
1078 // if this is not the root container, this browse will now fail
1079 // as the appropriate parentID will not be found
1080 LOG(VB_GENERAL, LOG_ERR, LOC +
1081 QString("%1 updateID changed during browse (old %2 new %3)")
1082 .arg(server->m_friendlyName).arg(server->m_systemUpdateID)
1083 .arg(updateid));
1084 m_scanComplete &= server->ResetContent(updateid);
1085 Debug();
1086 }
1087
1088 // find containers (directories) and actual items and add them and reset
1089 // the parent when we have found the first item
1090 bool reset = true;
1091 docElem = result->documentElement();
1092 n = docElem.firstChild();
1093 while (!n.isNull())
1094 {
1095 FindItems(n, *server, reset);
1096 n = n.nextSibling();
1097 }
1098 delete result;
1099
1100 m_lock.unlock();
1101}
1102
1104 bool &resetparent)
1105{
1106 QDomElement node = n.toElement();
1107 if (node.isNull())
1108 return;
1109
1110 if (node.tagName() == "container")
1111 {
1112 QString title = "ERROR";
1113 QDomNode next = node.firstChild();
1114 while (!next.isNull())
1115 {
1116 QDomElement container = next.toElement();
1117 if (!container.isNull() && container.tagName() == "title")
1118 title = container.text();
1119 next = next.nextSibling();
1120 }
1121
1122 QString thisid = node.attribute("id", "ERROR");
1123 QString parentid = node.attribute("parentID", "ERROR");
1124 MediaServerItem container =
1125 MediaServerItem(thisid, parentid, title, QString());
1126 MediaServerItem *parent = content.Find(parentid);
1127 if (parent)
1128 {
1129 if (resetparent)
1130 {
1131 parent->Reset();
1132 resetparent = false;
1133 }
1134 parent->m_scanned = true;
1135 parent->Add(container);
1136 }
1137 return;
1138 }
1139
1140 if (node.tagName() == "item")
1141 {
1142 QString title = "ERROR";
1143 QString url = "ERROR";
1144 QDomNode next = node.firstChild();
1145 while (!next.isNull())
1146 {
1147 QDomElement item = next.toElement();
1148 if (!item.isNull())
1149 {
1150 if(item.tagName() == "res")
1151 url = item.text();
1152 if(item.tagName() == "title")
1153 title = item.text();
1154 }
1155 next = next.nextSibling();
1156 }
1157
1158 QString thisid = node.attribute("id", "ERROR");
1159 QString parentid = node.attribute("parentID", "ERROR");
1160 MediaServerItem item =
1161 MediaServerItem(thisid, parentid, title, url);
1162 item.m_scanned = true;
1163 MediaServerItem *parent = content.Find(parentid);
1164 if (parent)
1165 {
1166 if (resetparent)
1167 {
1168 parent->Reset();
1169 resetparent = false;
1170 }
1171 parent->m_scanned = true;
1172 parent->Add(item);
1173 }
1174 return;
1175 }
1176
1177 QDomNode next = node.firstChild();
1178 while (!next.isNull())
1179 {
1180 FindItems(next, content, resetparent);
1181 next = next.nextSibling();
1182 }
1183}
1184
1185QDomDocument* UPNPScanner::FindResult(const QDomNode &n, uint &num,
1186 uint &total, uint &updateid)
1187{
1188 QDomDocument *result = nullptr;
1189 QDomElement node = n.toElement();
1190 if (node.isNull())
1191 return nullptr;
1192
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)
1200 {
1201 result = new QDomDocument();
1202#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
1203 QString errorMessage;
1204 int errorLine = 0;
1205 int errorColumn = 0;
1206 if (!result->setContent(node.text(), true, &errorMessage, &errorLine, &errorColumn))
1207 {
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));
1211 delete result;
1212 result = nullptr;
1213 }
1214#else
1215 auto parseResult =
1216 result->setContent(node.text(),
1217 QDomDocument::ParseOption::UseNamespaceProcessing);
1218 if (!parseResult)
1219 {
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));
1224 delete result;
1225 result = nullptr;
1226 }
1227#endif
1228 }
1229
1230 QDomNode next = node.firstChild();
1231 while (!next.isNull())
1232 {
1233 QDomDocument *res = FindResult(next, num, total, updateid);
1234 if (res)
1235 result = res;
1236 next = next.nextSibling();
1237 }
1238 return result;
1239}
1240
1245bool UPNPScanner::ParseDescription(const QUrl &url, QNetworkReply *reply)
1246{
1247 if (url.isEmpty() || !reply)
1248 return false;
1249
1250 QByteArray data = reply->readAll();
1251 if (data.isEmpty())
1252 {
1253 LOG(VB_GENERAL, LOG_ERR, LOC +
1254 QString("%1 returned an empty device description.")
1255 .arg(url.toString()));
1256 return false;
1257 }
1258
1259 // parse the device description
1260 QString controlURL = QString();
1261 QString eventURL = QString();
1262 QString friendlyName = QString("Unknown");
1263 QString URLBase = QString();
1264
1265 QDomDocument doc;
1266#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
1267 QString errorMessage;
1268 int errorLine = 0;
1269 int errorColumn = 0;
1270 if (!doc.setContent(data, false, &errorMessage, &errorLine, &errorColumn))
1271 {
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));
1277 return false;
1278 }
1279#else
1280 auto parseResult = doc.setContent(data);
1281 if (!parseResult)
1282 {
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));
1289 return false;
1290 }
1291#endif
1292
1293 QDomElement docElem = doc.documentElement();
1294 QDomNode n = docElem.firstChild();
1295 while (!n.isNull())
1296 {
1297 QDomElement e1 = n.toElement();
1298 if (!e1.isNull())
1299 {
1300 if(e1.tagName() == "device")
1301 ParseDevice(e1, controlURL, eventURL, friendlyName);
1302 if (e1.tagName() == "URLBase")
1303 URLBase = e1.text();
1304 }
1305 n = n.nextSibling();
1306 }
1307
1308 if (controlURL.isEmpty())
1309 {
1310 LOG(VB_UPNP, LOG_ERR, LOC +
1311 QString("Failed to parse device description for %1")
1312 .arg(url.toString()));
1313 return false;
1314 }
1315
1316 // if no URLBase was provided, use the known url
1317 if (URLBase.isEmpty())
1318 URLBase = url.toString(QUrl::RemovePath | QUrl::RemoveFragment |
1319 QUrl::RemoveQuery);
1320
1321 // strip leading slashes off the controlURL
1322 while (!controlURL.isEmpty() && controlURL.startsWith("/"))
1323 controlURL = controlURL.mid(1);
1324
1325 // strip leading slashes off the eventURL
1326 //while (!eventURL.isEmpty() && eventURL.startsWith("/"))
1327 // eventURL = eventURL.mid(1);
1328
1329 // strip trailing slashes off URLBase
1330 while (!URLBase.isEmpty() && URLBase.endsWith("/"))
1331 URLBase = URLBase.mid(0, URLBase.size() - 1);
1332
1333 controlURL = URLBase + "/" + controlURL;
1334 QString fulleventURL = URLBase + "/" + eventURL;
1335
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));
1340
1341 // update the server details. If the server has gone away since the request
1342 // was posted, this will silently fail and we won't try again
1343 QString usn;
1344 QUrl qeventurl = QUrl(fulleventURL);
1345 std::chrono::seconds timeout = 0s;
1346
1347 m_lock.lock();
1348 QHashIterator<QString,UpnpMediaServer*> it(m_servers);
1349 while (it.hasNext())
1350 {
1351 it.next();
1352 if (it.value()->m_serverURL == url)
1353 {
1354 usn = it.key();
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;
1361 break;
1362 }
1363 }
1364
1365 if (m_subscription && !usn.isEmpty())
1366 {
1367 timeout = m_subscription->Subscribe(usn, qeventurl, eventURL);
1368 m_servers[usn]->m_subscribed = (timeout > 0s);
1369 }
1370 m_lock.unlock();
1371
1372 if (timeout > 0s)
1373 {
1374 LOG(VB_GENERAL, LOG_INFO, LOC +
1375 QString("Subscribed for %1 seconds to %2") .arg(timeout.count()).arg(usn));
1377 // we only scan servers we are subscribed to - and the scan is now
1378 // incomplete
1379 m_scanComplete = false;
1380 }
1381
1382 Debug();
1383 return true;
1384}
1385
1386
1387void UPNPScanner::ParseDevice(QDomElement &element, QString &controlURL,
1388 QString &eventURL, QString &friendlyName)
1389{
1390 QDomNode dev = element.firstChild();
1391 while (!dev.isNull())
1392 {
1393 QDomElement e = dev.toElement();
1394 if (!e.isNull())
1395 {
1396 if (e.tagName() == "friendlyName")
1397 friendlyName = e.text();
1398 if (e.tagName() == "serviceList")
1399 ParseServiceList(e, controlURL, eventURL);
1400 }
1401 dev = dev.nextSibling();
1402 }
1403}
1404
1405void UPNPScanner::ParseServiceList(QDomElement &element, QString &controlURL,
1406 QString &eventURL)
1407{
1408 QDomNode list = element.firstChild();
1409 while (!list.isNull())
1410 {
1411 QDomElement e = list.toElement();
1412 if (!e.isNull())
1413 if (e.tagName() == "service")
1414 ParseService(e, controlURL, eventURL);
1415 list = list.nextSibling();
1416 }
1417}
1418
1419void UPNPScanner::ParseService(QDomElement &element, QString &controlURL,
1420 QString &eventURL)
1421{
1422 bool iscds = false;
1423 QString control_url = QString();
1424 QString event_url = QString();
1425 QDomNode service = element.firstChild();
1426
1427 while (!service.isNull())
1428 {
1429 QDomElement e = service.toElement();
1430 if (!e.isNull())
1431 {
1432 if (e.tagName() == "serviceType")
1433 // FIXME UPNP version comparision done wrong, we are using urn:schemas-upnp-org:device:MediaServer:4 ourselves
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();
1439 }
1440 service = service.nextSibling();
1441 }
1442
1443 if (iscds)
1444 {
1445 controlURL = control_url;
1446 eventURL = event_url;
1447 }
1448}
This is a wrapper around QThread that does several additional things.
Definition: mthread.h:49
bool isRunning(void) const
Definition: mthread.cpp:263
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:283
void quit(void)
calls exit(0)
Definition: mthread.cpp:295
bool wait(std::chrono::milliseconds time=std::chrono::milliseconds::max())
Wait for the MThread to exit, with a maximum timeout.
Definition: mthread.cpp:300
QThread * qthread(void)
Returns the thread, this will always return the same pointer no matter how often you restart the thre...
Definition: mthread.cpp:233
QString m_parentid
Definition: upnpscanner.h:38
QMap< QString, MediaServerItem > m_children
Definition: upnpscanner.h:42
bool Add(const MediaServerItem &item)
Definition: upnpscanner.cpp:67
QString NextUnbrowsed(void)
Definition: upnpscanner.cpp:27
void Reset(void)
Definition: upnpscanner.cpp:77
MediaServerItem * Find(QString &id)
Definition: upnpscanner.cpp:50
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 SSDPCacheEntries * Find(const QString &sURI)
Definition: ssdp.h:137
static void RemoveListener(QObject *listener)
Definition: ssdp.h:134
static void AddListener(QObject *listener)
Definition: ssdp.h:132
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
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
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.
~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
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.
QDomDocument * FindResult(const QDomNode &n, uint &num, uint &total, uint &updateid)
QTimer * m_updateTimer
Definition: upnpscanner.h:122
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
Definition: upnpscanner.h:120
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.
bool m_fullscan
Definition: upnpscanner.h:129
static void ParseService(QDomElement &element, QString &controlURL, QString &eventURL)
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:88
UpnpMediaServer(QUrl URL)
Definition: upnpscanner.cpp:95
bool ResetContent(int new_id)
QString m_eventSubPath
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: freesurround.h:24
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:22
static constexpr uint8_t MAX_ATTEMPTS
Definition: upnpscanner.cpp:25
simple_ref_ptr< meta_data_node > smart_meta_node