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 
25 static 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 
117  QString m_eventSubPath;
118  QString m_friendlyName;
119  uint8_t m_connectionAttempts {0};
120  bool m_subscribed {false};
123 };
124 
128 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
129 QMutex* UPNPScanner::gUPNPScannerLock = new QMutex(QMutex::Recursive);
130 #else
131 QRecursiveMutex* UPNPScanner::gUPNPScannerLock = new QRecursiveMutex();
132 #endif
133 
145 {
146  Stop();
147 }
148 
153 void UPNPScanner::Enable(bool enable, UPNPSubscription *sub)
154 {
155  QMutexLocker locker(gUPNPScannerLock);
156  gUPNPScannerEnabled = enable;
157  Instance(sub);
158 }
159 
166 {
167  QMutexLocker locker(gUPNPScannerLock);
168  if (!gUPNPScannerEnabled)
169  {
170  if (gUPNPScannerThread)
171  {
174  }
175  delete gUPNPScannerThread;
176  gUPNPScannerThread = nullptr;
177  delete gUPNPScanner;
178  gUPNPScanner = nullptr;
179  return nullptr;
180  }
181 
182  if (!gUPNPScannerThread)
183  gUPNPScannerThread = new MThread("UPnPScanner");
184  if (!gUPNPScanner)
185  gUPNPScanner = new UPNPScanner(sub);
186 
188  {
189  gUPNPScanner->moveToThread(gUPNPScannerThread->qthread());
190  QObject::connect(
191  gUPNPScannerThread->qthread(), &QThread::started,
193  gUPNPScannerThread->start(QThread::LowestPriority);
194  }
195 
196  return gUPNPScanner;
197 }
204 {
205  m_fullscan = true;
206  auto *me = new MythEvent(QString("UPNP_STARTSCAN"));
207  qApp->postEvent(this, me);
208 }
209 
217  meta_dir_node *node)
218 {
219  // nothing to see..
220  QMap<QString,QString> servers = ServerList();
221  if (servers.isEmpty())
222  return;
223 
224  // Add MediaServers
225  LOG(VB_GENERAL, LOG_INFO, QString("Adding MediaServer metadata."));
226 
227  smart_dir_node mediaservers = node->addSubDir(tr("Media Servers"));
228  mediaservers->setPathRoot();
229 
230  m_lock.lock();
231  QMutableHashIterator<QString,UpnpMediaServer*> it(m_servers);
232  while (it.hasNext())
233  {
234  it.next();
235  if (!it.value()->m_subscribed)
236  continue;
237 
238  QString usn = it.key();
239  GetServerContent(usn, it.value(), list, mediaservers.get());
240  }
241  m_lock.unlock();
242 }
243 
249  meta_dir_node *node)
250 {
251  // nothing to see..
252  QMap<QString,QString> servers = ServerList();
253  if (servers.isEmpty())
254  return;
255 
256  // Start scanning if it isn't already running
257  StartFullScan();
258 
259  // wait for the scanner to complete - with a 30 second timeout
260  LOG(VB_GENERAL, LOG_INFO, LOC + "Waiting for scan to complete.");
261 
262  int count = 0;
263  while (!m_scanComplete && (count++ < 300))
264  std::this_thread::sleep_for(100ms);
265 
266  // some scans may just take too long (PlayOn)
267  if (!m_scanComplete)
268  LOG(VB_GENERAL, LOG_ERR, LOC + "MediaServer scan is incomplete.");
269  else
270  LOG(VB_GENERAL, LOG_INFO, LOC + "MediaServer scanning finished.");
271 
272 
273  smart_dir_node mediaservers = node->addSubDir(tr("Media Servers"));
274  mediaservers->setPathRoot();
275 
276  m_lock.lock();
277  QMutableHashIterator<QString,UpnpMediaServer*> it(m_servers);
278  while (it.hasNext())
279  {
280  it.next();
281  if (!it.value()->m_subscribed)
282  continue;
283 
284  QString usn = it.key();
285  GetServerContent(usn, it.value(), list, mediaservers.get());
286  }
287  m_lock.unlock();
288 }
289 
290 bool UPNPScanner::GetMetadata(QVariant &data)
291 {
292  // we need a USN and objectID
293  if (!data.canConvert<QStringList>())
294  return false;
295 
296  QStringList list = data.toStringList();
297  if (list.size() != 2)
298  return false;
299 
300  QString usn = list[0];
301  QString object = list[1];
302 
303  m_lock.lock();
304  bool valid = m_servers.contains(usn);
305  if (valid)
306  {
307  MediaServerItem* item = m_servers[usn]->Find(object);
308  valid = item ? !item->m_scanned : false;
309  }
310  m_lock.unlock();
311  if (!valid)
312  return false;
313 
314  auto *me = new MythEvent("UPNP_BROWSEOBJECT", list);
315  qApp->postEvent(this, me);
316 
317  int count = 0;
318  bool found = false;
319  LOG(VB_GENERAL, LOG_INFO, "START");
320  while (!found && (count++ < 100)) // 10 seconds
321  {
322  std::this_thread::sleep_for(100ms);
323  m_lock.lock();
324  if (m_servers.contains(usn))
325  {
326  MediaServerItem *item = m_servers[usn]->Find(object);
327  if (item)
328  {
329  found = item->m_scanned;
330  }
331  else
332  {
333  LOG(VB_GENERAL, LOG_INFO, QString("Item went away..."));
334  found = true;
335  }
336  }
337  else
338  {
339  LOG(VB_GENERAL, LOG_INFO,
340  QString("Server went away while browsing."));
341  found = true;
342  }
343  m_lock.unlock();
344  }
345  LOG(VB_GENERAL, LOG_INFO, "END");
346  return true;
347 }
348 
357  meta_dir_node *node)
358 {
359  if (!content->m_scanned)
360  {
361  smart_dir_node subnode = node->addSubDir(content->m_name);
362 
363  QStringList data;
364  data << usn;
365  data << content->m_id;
366  subnode->SetData(data);
367 
369  item->SetTitle(QString("Dummy"));
370  list->push_back(item);
371  subnode->addEntry(smart_meta_node(new meta_data_node(item.get())));
372  return;
373  }
374 
375  node->SetData(QVariant());
376 
377  if (content->m_url.isEmpty())
378  {
379  smart_dir_node container = node->addSubDir(content->m_name);
380  QMutableMapIterator<QString,MediaServerItem> it(content->m_children);
381  while (it.hasNext())
382  {
383  it.next();
384  GetServerContent(usn, &it.value(), list, container.get());
385  }
386  return;
387  }
388 
390  item->SetTitle(content->m_name);
391  list->push_back(item);
392  node->addEntry(smart_meta_node(new meta_data_node(item.get())));
393 }
394 
400 QMap<QString,QString> UPNPScanner::ServerList(void)
401 {
402  QMap<QString,QString> servers;
403  m_lock.lock();
404  QHashIterator<QString,UpnpMediaServer*> it(m_servers);
405  while (it.hasNext())
406  {
407  it.next();
408  servers.insert(it.key(), it.value()->m_friendlyName);
409  }
410  m_lock.unlock();
411  return servers;
412 }
413 
420 {
421  m_lock.lock();
422 
423  // create our network handler
424  m_network = new QNetworkAccessManager();
425  connect(m_network, &QNetworkAccessManager::finished,
427 
428  // listen for SSDP updates
429  SSDP::AddListener(this);
430 
431  // listen for subscriptions and events
432  if (m_subscription)
434 
435  // create our update timer (driven by AddServer and ParseDescription)
436  m_updateTimer = new QTimer(this);
437  m_updateTimer->setSingleShot(true);
439 
440  // create our watchdog timer (checks for stale servers)
441  m_watchdogTimer = new QTimer(this);
443  m_watchdogTimer->start(10s);
444 
445  // avoid connecting to the master backend
448 
449  m_lock.unlock();
450  LOG(VB_GENERAL, LOG_INFO, LOC + "Started");
451 }
452 
458 {
459  m_lock.lock();
460 
461  // stop listening
462  SSDP::RemoveListener(this);
463  if (m_subscription)
465 
466  // disable updates
467  if (m_updateTimer)
468  m_updateTimer->stop();
469  if (m_watchdogTimer)
470  m_watchdogTimer->stop();
471 
472  // cleanup our servers and subscriptions
473  QHashIterator<QString,UpnpMediaServer*> it(m_servers);
474  while (it.hasNext())
475  {
476  it.next();
477  if (m_subscription && it.value()->m_subscribed)
478  m_subscription->Unsubscribe(it.key());
479  if (it.value()->m_renewalTimerId)
480  killTimer(it.value()->m_renewalTimerId);
481  delete it.value();
482  }
483  m_servers.clear();
484 
485  // cleanup the network
486  for (QNetworkReply *reply : qAsConst(m_descriptionRequests))
487  {
488  reply->abort();
489  delete reply;
490  }
491  m_descriptionRequests.clear();
492  for (QNetworkReply *reply : qAsConst(m_browseRequests))
493  {
494  reply->abort();
495  delete reply;
496  }
497  m_browseRequests.clear();
498  delete m_network;
499  m_network = nullptr;
500 
501  // delete the timers
502  delete m_updateTimer;
503  delete m_watchdogTimer;
504  m_updateTimer = nullptr;
505  m_watchdogTimer = nullptr;
506 
507  m_lock.unlock();
508  LOG(VB_GENERAL, LOG_INFO, LOC + "Finished");
509 }
510 
517 {
518  // decide which servers still need to be checked
519  m_lock.lock();
520  if (m_servers.isEmpty())
521  {
522  m_lock.unlock();
523  return;
524  }
525 
526  // if our network queue is full, then we may need to come back later
527  bool reschedule = false;
528 
529  QHashIterator<QString,UpnpMediaServer*> it(m_servers);
530  while (it.hasNext())
531  {
532  it.next();
533  if ((it.value()->m_connectionAttempts < MAX_ATTEMPTS) &&
534  (it.value()->m_controlURL.isEmpty()))
535  {
536  bool sent = false;
537  QUrl url = it.value()->m_url;
538  if (!m_descriptionRequests.contains(url) &&
539  (m_descriptionRequests.empty()) &&
540  url.isValid())
541  {
542  QNetworkReply *reply = m_network->get(QNetworkRequest(url));
543  if (reply)
544  {
545  sent = true;
546  m_descriptionRequests.insert(url, reply);
547  it.value()->m_connectionAttempts++;
548  }
549  }
550  if (!sent)
551  reschedule = true;
552  }
553  }
554 
555  if (reschedule)
556  ScheduleUpdate();
557  m_lock.unlock();
558 }
559 
565 {
566  // FIXME
567  // Remove stale servers - the SSDP cache code does not send out removal
568  // notifications for expired (rather than explicitly closed) connections
569  m_lock.lock();
570  QMutableHashIterator<QString,UpnpMediaServer*> it(m_servers);
571  while (it.hasNext())
572  {
573  it.next();
574  // FIXME UPNP version comparision done wrong, we are using urn:schemas-upnp-org:device:MediaServer:4 ourselves
575  if (!SSDP::Find("urn:schemas-upnp-org:device:MediaServer:1", it.key()))
576  {
577  LOG(VB_UPNP, LOG_INFO, LOC + QString("%1 no longer in SSDP cache. Removing")
578  .arg(it.value()->m_serverURL.toString()));
579  UpnpMediaServer* last = it.value();
580  it.remove();
581  delete last;
582  }
583  }
584  m_lock.unlock();
585 }
586 
592 void UPNPScanner::replyFinished(QNetworkReply *reply)
593 {
594  if (!reply)
595  return;
596 
597  QUrl url = reply->url();
598  bool valid = reply->error() == QNetworkReply::NoError;
599 
600  if (!valid)
601  {
602  LOG(VB_UPNP, LOG_ERR, LOC +
603  QString("Network request for '%1' returned error '%2'")
604  .arg(url.toString(), reply->errorString()));
605  }
606 
607  bool description = false;
608  bool browse = false;
609 
610  m_lock.lock();
611  if (m_descriptionRequests.contains(url, reply))
612  {
613  m_descriptionRequests.remove(url, reply);
614  description = true;
615  }
616  else if (m_browseRequests.contains(url, reply))
617  {
618  m_browseRequests.remove(url, reply);
619  browse = true;
620  }
621  m_lock.unlock();
622 
623  if (browse && valid)
624  {
625  ParseBrowse(url, reply);
626  if (m_fullscan)
628  }
629  else if (description)
630  {
631  if (!valid || !ParseDescription(url, reply))
632  {
633  // if there will be no more attempts, update the logs
634  CheckFailure(url);
635  // try again
636  ScheduleUpdate();
637  }
638  }
639  else
640  LOG(VB_UPNP, LOG_ERR, LOC + "Received unknown reply");
641 
642  reply->deleteLater();
643 }
644 
648 void UPNPScanner::customEvent(QEvent *event)
649 {
650  if (event->type() != MythEvent::MythEventMessage)
651  return;
652 
653  // UPnP events
654  auto *me = dynamic_cast<MythEvent *>(event);
655  if (me == nullptr)
656  return;
657 
658  const QString& ev = me->Message();
659 
660  if (ev == "UPNP_STARTSCAN")
661  {
663  return;
664  }
665  if (ev == "UPNP_BROWSEOBJECT")
666  {
667  if (me->ExtraDataCount() == 2)
668  {
669  QUrl url;
670  const QString& usn = me->ExtraData(0);
671  const QString& objectid = me->ExtraData(1);
672  m_lock.lock();
673  if (m_servers.contains(usn))
674  {
675  url = m_servers[usn]->m_controlURL;
676  LOG(VB_GENERAL, LOG_INFO, QString("UPNP_BROWSEOBJECT: %1->%2")
677  .arg(m_servers[usn]->m_friendlyName, objectid));
678  }
679  m_lock.unlock();
680  if (!url.isEmpty())
681  SendBrowseRequest(url, objectid);
682  }
683  return;
684  }
685  if (ev == "UPNP_EVENT")
686  {
687  auto *info = (MythInfoMapEvent*)event;
688  if (!info)
689  return;
690  if (!info->GetInfoMap())
691  return;
692 
693  QString usn = info->GetInfoMap()->value("usn");
694  QString id = info->GetInfoMap()->value("SystemUpdateID");
695  if (usn.isEmpty() || id.isEmpty())
696  return;
697 
698  m_lock.lock();
699  if (m_servers.contains(usn))
700  {
701  int newid = id.toInt();
702  if (m_servers[usn]->m_systemUpdateID != newid)
703  {
704  m_scanComplete &= m_servers[usn]->ResetContent(newid);
705  LOG(VB_GENERAL, LOG_INFO, LOC +
706  QString("New SystemUpdateID '%1' for %2").arg(id, usn));
707  Debug();
708  }
709  }
710  m_lock.unlock();
711  return;
712  }
713 
714  // process SSDP cache updates
715  QString uri = me->ExtraDataCount() > 0 ? me->ExtraData(0) : QString();
716  QString usn = me->ExtraDataCount() > 1 ? me->ExtraData(1) : QString();
717 
718  // FIXME UPNP version comparision done wrong, we are using urn:schemas-upnp-org:device:MediaServer:4 ourselves
719  if (uri == "urn:schemas-upnp-org:device:MediaServer:1")
720  {
721  QString url = (ev == "SSDP_ADD") ? me->ExtraData(2) : QString();
722  AddServer(usn, url);
723  }
724 }
725 
730 void UPNPScanner::timerEvent(QTimerEvent * event)
731 {
732  int id = event->timerId();
733  if (id)
734  killTimer(id);
735 
736  std::chrono::seconds timeout = 0s;
737  QString usn;
738 
739  m_lock.lock();
740  QHashIterator<QString,UpnpMediaServer*> it(m_servers);
741  while (it.hasNext())
742  {
743  it.next();
744  if (it.value()->m_renewalTimerId == id)
745  {
746  it.value()->m_renewalTimerId = 0;
747  usn = it.key();
748  if (m_subscription)
749  timeout = m_subscription->Renew(usn);
750  }
751  }
752  m_lock.unlock();
753 
754  if (timeout > 0s)
755  {
756  ScheduleRenewal(usn, timeout);
757  LOG(VB_GENERAL, LOG_INFO, LOC +
758  QString("Re-subscribed for %1 seconds to %2")
759  .arg(timeout.count()).arg(usn));
760  }
761 }
762 
767 {
768  m_lock.lock();
769  if (m_updateTimer && !m_updateTimer->isActive())
770  m_updateTimer->start(200ms);
771  m_lock.unlock();
772 }
773 
778 void UPNPScanner::CheckFailure(const QUrl &url)
779 {
780  m_lock.lock();
781  QHashIterator<QString,UpnpMediaServer*> it(m_servers);
782  while (it.hasNext())
783  {
784  it.next();
785  if (it.value()->m_serverURL == url && it.value()->m_connectionAttempts == MAX_ATTEMPTS)
786  {
787  Debug();
788  break;
789  }
790  }
791  m_lock.unlock();
792 }
793 
798 {
799  m_lock.lock();
800  LOG(VB_UPNP, LOG_INFO, LOC + QString("%1 media servers discovered:")
801  .arg(m_servers.size()));
802  QHashIterator<QString,UpnpMediaServer*> it(m_servers);
803  while (it.hasNext())
804  {
805  it.next();
806  QString status = "Probing";
807  if (it.value()->m_controlURL.toString().isEmpty())
808  {
809  if (it.value()->m_connectionAttempts >= MAX_ATTEMPTS)
810  status = "Failed";
811  }
812  else
813  status = "Yes";
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 
878 void 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 
924 void 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));
951  ScheduleUpdate();
952  }
953  m_lock.unlock();
954 }
955 
959 void 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)
971  m_subscription->Remove(usn);
972  }
973  m_lock.unlock();
974 
975  Debug();
976 }
977 
981 void 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 
997 void 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  QString errorMessage;
1006  int errorLine = 0;
1007  int errorColumn = 0;
1008  if (!parent->setContent(data, false, &errorMessage, &errorLine,
1009  &errorColumn))
1010  {
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));
1014  delete parent;
1015  return;
1016  }
1017 
1018  LOG(VB_UPNP, LOG_INFO, "\n\n" + parent->toString(4) + "\n\n");
1019 
1020  // pull out the actual result
1021  QDomDocument *result = nullptr;
1022  uint num = 0;
1023  uint total = 0;
1024  uint updateid = 0;
1025  QDomElement docElem = parent->documentElement();
1026  QDomNode n = docElem.firstChild();
1027  if (!n.isNull())
1028  result = FindResult(n, num, total, updateid);
1029  delete parent;
1030 
1031  if (!result || num < 1 || total < 1)
1032  {
1033  LOG(VB_GENERAL, LOG_ERR, LOC +
1034  QString("Failed to find result for %1") .arg(url.toString()));
1035  return;
1036  }
1037 
1038  // determine the 'server' which requested the browse
1039  m_lock.lock();
1040 
1041  UpnpMediaServer* server = nullptr;
1042  QHashIterator<QString,UpnpMediaServer*> it(m_servers);
1043  while (it.hasNext())
1044  {
1045  it.next();
1046  if (url == it.value()->m_controlURL)
1047  {
1048  server = it.value();
1049  break;
1050  }
1051  }
1052 
1053  // discard unmatched responses
1054  if (!server)
1055  {
1056  m_lock.unlock();
1057  LOG(VB_GENERAL, LOG_ERR, LOC +
1058  QString("Received unknown response for %1").arg(url.toString()));
1059  return;
1060  }
1061 
1062  // check the update ID
1063  if (server->m_systemUpdateID != (int)updateid)
1064  {
1065  // if this is not the root container, this browse will now fail
1066  // as the appropriate parentID will not be found
1067  LOG(VB_GENERAL, LOG_ERR, LOC +
1068  QString("%1 updateID changed during browse (old %2 new %3)")
1069  .arg(server->m_friendlyName).arg(server->m_systemUpdateID)
1070  .arg(updateid));
1071  m_scanComplete &= server->ResetContent(updateid);
1072  Debug();
1073  }
1074 
1075  // find containers (directories) and actual items and add them and reset
1076  // the parent when we have found the first item
1077  bool reset = true;
1078  docElem = result->documentElement();
1079  n = docElem.firstChild();
1080  while (!n.isNull())
1081  {
1082  FindItems(n, *server, reset);
1083  n = n.nextSibling();
1084  }
1085  delete result;
1086 
1087  m_lock.unlock();
1088 }
1089 
1091  bool &resetparent)
1092 {
1093  QDomElement node = n.toElement();
1094  if (node.isNull())
1095  return;
1096 
1097  if (node.tagName() == "container")
1098  {
1099  QString title = "ERROR";
1100  QDomNode next = node.firstChild();
1101  while (!next.isNull())
1102  {
1103  QDomElement container = next.toElement();
1104  if (!container.isNull() && container.tagName() == "title")
1105  title = container.text();
1106  next = next.nextSibling();
1107  }
1108 
1109  QString thisid = node.attribute("id", "ERROR");
1110  QString parentid = node.attribute("parentID", "ERROR");
1111  MediaServerItem container =
1112  MediaServerItem(thisid, parentid, title, QString());
1113  MediaServerItem *parent = content.Find(parentid);
1114  if (parent)
1115  {
1116  if (resetparent)
1117  {
1118  parent->Reset();
1119  resetparent = false;
1120  }
1121  parent->m_scanned = true;
1122  parent->Add(container);
1123  }
1124  return;
1125  }
1126 
1127  if (node.tagName() == "item")
1128  {
1129  QString title = "ERROR";
1130  QString url = "ERROR";
1131  QDomNode next = node.firstChild();
1132  while (!next.isNull())
1133  {
1134  QDomElement item = next.toElement();
1135  if (!item.isNull())
1136  {
1137  if(item.tagName() == "res")
1138  url = item.text();
1139  if(item.tagName() == "title")
1140  title = item.text();
1141  }
1142  next = next.nextSibling();
1143  }
1144 
1145  QString thisid = node.attribute("id", "ERROR");
1146  QString parentid = node.attribute("parentID", "ERROR");
1147  MediaServerItem item =
1148  MediaServerItem(thisid, parentid, title, url);
1149  item.m_scanned = true;
1150  MediaServerItem *parent = content.Find(parentid);
1151  if (parent)
1152  {
1153  if (resetparent)
1154  {
1155  parent->Reset();
1156  resetparent = false;
1157  }
1158  parent->m_scanned = true;
1159  parent->Add(item);
1160  }
1161  return;
1162  }
1163 
1164  QDomNode next = node.firstChild();
1165  while (!next.isNull())
1166  {
1167  FindItems(next, content, resetparent);
1168  next = next.nextSibling();
1169  }
1170 }
1171 
1172 QDomDocument* UPNPScanner::FindResult(const QDomNode &n, uint &num,
1173  uint &total, uint &updateid)
1174 {
1175  QDomDocument *result = nullptr;
1176  QDomElement node = n.toElement();
1177  if (node.isNull())
1178  return nullptr;
1179 
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)
1187  {
1188  QString errorMessage;
1189  int errorLine = 0;
1190  int errorColumn = 0;
1191  result = new QDomDocument();
1192  if (!result->setContent(node.text(), true, &errorMessage, &errorLine, &errorColumn))
1193  {
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));
1197  delete result;
1198  result = nullptr;
1199  }
1200  }
1201 
1202  QDomNode next = node.firstChild();
1203  while (!next.isNull())
1204  {
1205  QDomDocument *res = FindResult(next, num, total, updateid);
1206  if (res)
1207  result = res;
1208  next = next.nextSibling();
1209  }
1210  return result;
1211 }
1212 
1217 bool UPNPScanner::ParseDescription(const QUrl &url, QNetworkReply *reply)
1218 {
1219  if (url.isEmpty() || !reply)
1220  return false;
1221 
1222  QByteArray data = reply->readAll();
1223  if (data.isEmpty())
1224  {
1225  LOG(VB_GENERAL, LOG_ERR, LOC +
1226  QString("%1 returned an empty device description.")
1227  .arg(url.toString()));
1228  return false;
1229  }
1230 
1231  // parse the device description
1232  QString controlURL = QString();
1233  QString eventURL = QString();
1234  QString friendlyName = QString("Unknown");
1235  QString URLBase = QString();
1236 
1237  QDomDocument doc;
1238  QString errorMessage;
1239  int errorLine = 0;
1240  int errorColumn = 0;
1241  if (!doc.setContent(data, false, &errorMessage, &errorLine, &errorColumn))
1242  {
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));
1248  return false;
1249  }
1250 
1251  QDomElement docElem = doc.documentElement();
1252  QDomNode n = docElem.firstChild();
1253  while (!n.isNull())
1254  {
1255  QDomElement e1 = n.toElement();
1256  if (!e1.isNull())
1257  {
1258  if(e1.tagName() == "device")
1259  ParseDevice(e1, controlURL, eventURL, friendlyName);
1260  if (e1.tagName() == "URLBase")
1261  URLBase = e1.text();
1262  }
1263  n = n.nextSibling();
1264  }
1265 
1266  if (controlURL.isEmpty())
1267  {
1268  LOG(VB_UPNP, LOG_ERR, LOC +
1269  QString("Failed to parse device description for %1")
1270  .arg(url.toString()));
1271  return false;
1272  }
1273 
1274  // if no URLBase was provided, use the known url
1275  if (URLBase.isEmpty())
1276  URLBase = url.toString(QUrl::RemovePath | QUrl::RemoveFragment |
1277  QUrl::RemoveQuery);
1278 
1279  // strip leading slashes off the controlURL
1280  while (!controlURL.isEmpty() && controlURL.startsWith("/"))
1281  controlURL = controlURL.mid(1);
1282 
1283  // strip leading slashes off the eventURL
1284  //while (!eventURL.isEmpty() && eventURL.startsWith("/"))
1285  // eventURL = eventURL.mid(1);
1286 
1287  // strip trailing slashes off URLBase
1288  while (!URLBase.isEmpty() && URLBase.endsWith("/"))
1289  URLBase = URLBase.mid(0, URLBase.size() - 1);
1290 
1291  controlURL = URLBase + "/" + controlURL;
1292  QString fulleventURL = URLBase + "/" + eventURL;
1293 
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));
1298 
1299  // update the server details. If the server has gone away since the request
1300  // was posted, this will silently fail and we won't try again
1301  QString usn;
1302  QUrl qeventurl = QUrl(fulleventURL);
1303  std::chrono::seconds timeout = 0s;
1304 
1305  m_lock.lock();
1306  QHashIterator<QString,UpnpMediaServer*> it(m_servers);
1307  while (it.hasNext())
1308  {
1309  it.next();
1310  if (it.value()->m_serverURL == url)
1311  {
1312  usn = it.key();
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;
1319  break;
1320  }
1321  }
1322 
1323  if (m_subscription && !usn.isEmpty())
1324  {
1325  timeout = m_subscription->Subscribe(usn, qeventurl, eventURL);
1326  m_servers[usn]->m_subscribed = (timeout > 0s);
1327  }
1328  m_lock.unlock();
1329 
1330  if (timeout > 0s)
1331  {
1332  LOG(VB_GENERAL, LOG_INFO, LOC +
1333  QString("Subscribed for %1 seconds to %2") .arg(timeout.count()).arg(usn));
1334  ScheduleRenewal(usn, timeout);
1335  // we only scan servers we are subscribed to - and the scan is now
1336  // incomplete
1337  m_scanComplete = false;
1338  }
1339 
1340  Debug();
1341  return true;
1342 }
1343 
1344 
1345 void UPNPScanner::ParseDevice(QDomElement &element, QString &controlURL,
1346  QString &eventURL, QString &friendlyName)
1347 {
1348  QDomNode dev = element.firstChild();
1349  while (!dev.isNull())
1350  {
1351  QDomElement e = dev.toElement();
1352  if (!e.isNull())
1353  {
1354  if (e.tagName() == "friendlyName")
1355  friendlyName = e.text();
1356  if (e.tagName() == "serviceList")
1357  ParseServiceList(e, controlURL, eventURL);
1358  }
1359  dev = dev.nextSibling();
1360  }
1361 }
1362 
1363 void UPNPScanner::ParseServiceList(QDomElement &element, QString &controlURL,
1364  QString &eventURL)
1365 {
1366  QDomNode list = element.firstChild();
1367  while (!list.isNull())
1368  {
1369  QDomElement e = list.toElement();
1370  if (!e.isNull())
1371  if (e.tagName() == "service")
1372  ParseService(e, controlURL, eventURL);
1373  list = list.nextSibling();
1374  }
1375 }
1376 
1377 void UPNPScanner::ParseService(QDomElement &element, QString &controlURL,
1378  QString &eventURL)
1379 {
1380  bool iscds = false;
1381  QString control_url = QString();
1382  QString event_url = QString();
1383  QDomNode service = element.firstChild();
1384 
1385  while (!service.isNull())
1386  {
1387  QDomElement e = service.toElement();
1388  if (!e.isNull())
1389  {
1390  if (e.tagName() == "serviceType")
1391  // FIXME UPNP version comparision done wrong, we are using urn:schemas-upnp-org:device:MediaServer:4 ourselves
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();
1397  }
1398  service = service.nextSibling();
1399  }
1400 
1401  if (iscds)
1402  {
1403  controlURL = control_url;
1404  eventURL = event_url;
1405  }
1406 }
UPNPSubscription::Renew
std::chrono::seconds Renew(const QString &usn)
Definition: upnpsubscription.cpp:128
UpnpMediaServer
Definition: upnpscanner.cpp:87
UpnpMediaServer::m_friendlyName
QString m_friendlyName
Definition: upnpscanner.cpp:118
VideoMetadata
Definition: videometadata.h:24
UPNPScanner::m_fullscan
bool m_fullscan
Definition: upnpscanner.h:141
MediaServerItem::Add
bool Add(const MediaServerItem &item)
Definition: upnpscanner.cpp:67
UPNPScanner::customEvent
void customEvent(QEvent *event) override
Processes subscription and SSDP cache update events.
Definition: upnpscanner.cpp:648
MythEvent::MythEventMessage
static Type MythEventMessage
Definition: mythevent.h:79
meta_dir_node::SetData
void SetData(const QVariant &data)
Definition: videometadatalistmanager.cpp:317
MThread::start
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:283
hardwareprofile.smolt.timeout
float timeout
Definition: smolt.py:103
UPNPScanner::timerEvent
void timerEvent(QTimerEvent *event) override
Definition: upnpscanner.cpp:730
UPNPScanner::gUPNPScanner
static UPNPScanner * gUPNPScanner
Definition: upnpscanner.h:112
MThread::quit
void quit(void)
calls exit(0)
Definition: mthread.cpp:295
UpnpMediaServer::m_eventSubURL
QUrl m_eventSubURL
Definition: upnpscanner.cpp:116
MediaServerItem
Definition: upnpscanner.h:28
UPNPScanner::ScheduleUpdate
void ScheduleUpdate(void)
Definition: upnpscanner.cpp:766
MediaServerItem::m_children
QMap< QString, MediaServerItem > m_children
Definition: upnpscanner.h:46
meta_node::setPathRoot
void setPathRoot(bool is_root=true)
Definition: videometadatalistmanager.cpp:223
UPNPScanner::GetInitialMetadata
void GetInitialMetadata(VideoMetadataListManager::metadata_list *list, meta_dir_node *node)
Definition: upnpscanner.cpp:216
UpnpMediaServer::m_controlURL
QUrl m_controlURL
Definition: upnpscanner.cpp:115
ssdp.h
UPNPSubscription
Definition: upnpsubscription.h:10
MThread::wait
bool wait(std::chrono::milliseconds time=std::chrono::milliseconds::max())
Wait for the MThread to exit, with a maximum timeout.
Definition: mthread.cpp:300
simple_ref_ptr< meta_dir_node >
UpnpMediaServer::m_connectionAttempts
uint8_t m_connectionAttempts
Definition: upnpscanner.cpp:119
MediaServerItem::m_url
QString m_url
Definition: upnpscanner.h:44
smart_meta_node
simple_ref_ptr< meta_data_node > smart_meta_node
Definition: videometadatalistmanager.h:77
UPNPScanner::m_browseRequests
QMultiMap< QUrl, QNetworkReply * > m_browseRequests
Definition: upnpscanner.h:132
build_compdb.content
content
Definition: build_compdb.py:38
MythEvent
This class is used as a container for messages.
Definition: mythevent.h:16
UPNPScanner::UPNPScanner
UPNPScanner(UPNPSubscription *sub)
Definition: upnpscanner.h:79
UPNPScanner::m_servers
QHash< QString, UpnpMediaServer * > m_servers
Definition: upnpscanner.h:127
SSDP::Find
static SSDPCacheEntries * Find(const QString &sURI)
Definition: ssdp.h:132
meta_dir_node
Definition: videometadatalistmanager.h:82
LOG
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
UPNPScanner::m_lock
QRecursiveMutex m_lock
Definition: upnpscanner.h:125
meta_dir_node::addEntry
void addEntry(const smart_meta_node &entry)
Definition: videometadatalistmanager.cpp:372
UpnpMediaServer::m_systemUpdateID
int m_systemUpdateID
Definition: upnpscanner.cpp:122
MediaServerItem::m_id
QString m_id
Definition: upnpscanner.h:41
MythEvent::Message
const QString & Message() const
Definition: mythevent.h:65
MythCoreContext::GetMasterServerStatusPort
int GetMasterServerStatusPort(void)
Returns the Master Backend status port If no master server status port has been defined in the databa...
Definition: mythcorecontext.cpp:991
UPNPScanner::StartFullScan
void StartFullScan(void)
Definition: upnpscanner.cpp:203
UPNPScanner::m_descriptionRequests
QMultiMap< QUrl, QNetworkReply * > m_descriptionRequests
Definition: upnpscanner.h:131
UPNPSubscription::Unsubscribe
void Unsubscribe(const QString &usn)
Definition: upnpsubscription.cpp:108
MythObservable::addListener
void addListener(QObject *listener)
Add a listener to the observable.
Definition: mythobservable.cpp:38
UPNPScanner::m_network
QNetworkAccessManager * m_network
Definition: upnpscanner.h:128
UPNPScanner::GetServerContent
void GetServerContent(QString &usn, MediaServerItem *content, VideoMetadataListManager::metadata_list *list, meta_dir_node *node)
Definition: upnpscanner.cpp:354
UPNPSubscription::Subscribe
std::chrono::seconds Subscribe(const QString &usn, const QUrl &url, const QString &path)
Definition: upnpsubscription.cpp:75
UPNPScanner::Stop
void Stop(void)
Definition: upnpscanner.cpp:457
UPNPScanner::m_updateTimer
QTimer * m_updateTimer
Definition: upnpscanner.h:134
UPNPScanner::ScheduleRenewal
void ScheduleRenewal(const QString &usn, std::chrono::seconds timeout)
Creates a QTimer to trigger a subscription renewal for a given media server.
Definition: upnpscanner.cpp:981
UPNPScanner
Definition: upnpscanner.h:49
UPNPScanner::FindResult
QDomDocument * FindResult(const QDomNode &n, uint &num, uint &total, uint &updateid)
Definition: upnpscanner.cpp:1172
mythlogging.h
meta_dir_node::addSubDir
smart_dir_node addSubDir(const QString &subdir, const QString &name="", const QString &host="", const QString &prefix="", const QVariant &data=QVariant())
Definition: videometadatalistmanager.cpp:332
MythCoreContext::GetMasterServerIP
QString GetMasterServerIP(void)
Returns the Master Backend IP address If the address is an IPv6 address, the scope Id is removed.
Definition: mythcorecontext.cpp:964
UPNPScanner::m_subscription
UPNPSubscription * m_subscription
Definition: upnpscanner.h:121
UPNPScanner::ServerList
QMap< QString, QString > ServerList(void)
Definition: upnpscanner.cpp:400
MThread::qthread
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
UPNPScanner::RemoveServer
void RemoveServer(const QString &usn)
Definition: upnpscanner.cpp:959
UPNPScanner::AddServer
void AddServer(const QString &usn, const QString &url)
Definition: upnpscanner.cpp:924
UPNPScanner::m_scanComplete
bool m_scanComplete
Definition: upnpscanner.h:140
UpnpMediaServer::ResetContent
bool ResetContent(int new_id)
Definition: upnpscanner.cpp:102
UPNPScanner::ParseBrowse
void ParseBrowse(const QUrl &url, QNetworkReply *reply)
Definition: upnpscanner.cpp:997
uint
unsigned int uint
Definition: compat.h:79
UPNPScanner::SendBrowseRequest
void SendBrowseRequest(const QUrl &url, const QString &objectid)
Definition: upnpscanner.cpp:878
gCoreContext
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
Definition: mythcorecontext.cpp:54
MediaServerItem::Find
MediaServerItem * Find(QString &id)
Definition: upnpscanner.cpp:50
UPNPScanner::gUPNPScannerThread
static MThread * gUPNPScannerThread
Definition: upnpscanner.h:114
MAX_ATTEMPTS
static constexpr uint8_t MAX_ATTEMPTS
Definition: upnpscanner.cpp:25
UPNPScanner::replyFinished
void replyFinished(QNetworkReply *reply)
Definition: upnpscanner.cpp:592
MediaServerItem::Reset
void Reset(void)
Definition: upnpscanner.cpp:77
UpnpMediaServer::UpnpMediaServer
UpnpMediaServer(QUrl URL)
Definition: upnpscanner.cpp:95
MythInfoMapEvent
Definition: mythevent.h:128
UpnpMediaServer::m_eventSubPath
QString m_eventSubPath
Definition: upnpscanner.cpp:117
UPNPScanner::ParseDescription
bool ParseDescription(const QUrl &url, QNetworkReply *reply)
Definition: upnpscanner.cpp:1217
meta_data_node
Definition: videometadatalistmanager.h:60
mythcorecontext.h
UPNPScanner::Start
void Start()
Definition: upnpscanner.cpp:419
UpnpMediaServer::UpnpMediaServer
UpnpMediaServer()
Definition: upnpscanner.cpp:90
UPNPScanner::Instance
static UPNPScanner * Instance(UPNPSubscription *sub=nullptr)
Definition: upnpscanner.cpp:165
std
Definition: mythchrono.h:23
UPNPScanner::ParseDevice
static void ParseDevice(QDomElement &element, QString &controlURL, QString &eventURL, QString &friendlyName)
Definition: upnpscanner.cpp:1345
UPNPScanner::~UPNPScanner
~UPNPScanner() override
Definition: upnpscanner.cpp:144
UPNPScanner::gUPNPScannerLock
static QRecursiveMutex * gUPNPScannerLock
Definition: upnpscanner.h:118
MThread
This is a wrapper around QThread that does several additional things.
Definition: mthread.h:48
MediaServerItem::NextUnbrowsed
QString NextUnbrowsed(void)
Definition: upnpscanner.cpp:27
MThread::isRunning
bool isRunning(void) const
Definition: mthread.cpp:263
UPNPScanner::m_masterPort
int m_masterPort
Definition: upnpscanner.h:138
UPNPScanner::m_watchdogTimer
QTimer * m_watchdogTimer
Definition: upnpscanner.h:135
UPNPScanner::Update
void Update(void)
Definition: upnpscanner.cpp:516
UPNPScanner::Enable
static void Enable(bool enable, UPNPSubscription *sub=nullptr)
Definition: upnpscanner.cpp:153
VideoMetadataListManager::metadata_list
std::list< VideoMetadataPtr > metadata_list
Definition: videometadatalistmanager.h:14
MediaServerItem::m_scanned
bool m_scanned
Definition: upnpscanner.h:45
SSDP::AddListener
static void AddListener(QObject *listener)
Definition: ssdp.h:127
UPNPScanner::FindItems
void FindItems(const QDomNode &n, MediaServerItem &content, bool &resetparent)
Definition: upnpscanner.cpp:1090
UPNPScanner::CheckFailure
void CheckFailure(const QUrl &url)
Definition: upnpscanner.cpp:778
upnpscanner.h
UpnpMediaServer::m_renewalTimerId
int m_renewalTimerId
Definition: upnpscanner.cpp:121
UPNPScanner::BrowseNextContainer
void BrowseNextContainer(void)
Definition: upnpscanner.cpp:833
LOC
#define LOC
Definition: upnpscanner.cpp:22
UPNPScanner::ParseServiceList
static void ParseServiceList(QDomElement &element, QString &controlURL, QString &eventURL)
Definition: upnpscanner.cpp:1363
UPNPScanner::gUPNPScannerEnabled
static bool gUPNPScannerEnabled
Definition: upnpscanner.h:113
UpnpMediaServer::m_serverURL
QUrl m_serverURL
Definition: upnpscanner.cpp:114
UPNPScanner::m_masterHost
QString m_masterHost
Definition: upnpscanner.h:137
MythObservable::removeListener
void removeListener(QObject *listener)
Remove a listener to the observable.
Definition: mythobservable.cpp:55
UpnpMediaServer::m_subscribed
bool m_subscribed
Definition: upnpscanner.cpp:120
SSDP::RemoveListener
static void RemoveListener(QObject *listener)
Definition: ssdp.h:129
MediaServerItem::m_parentid
QString m_parentid
Definition: upnpscanner.h:42
UPNPSubscription::Remove
void Remove(const QString &usn)
Definition: upnpsubscription.cpp:162
simple_ref_ptr::get
T * get() const
Definition: quicksp.h:73
UPNPScanner::CheckStatus
void CheckStatus(void)
Definition: upnpscanner.cpp:564
UPNPScanner::GetMetadata
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...
Definition: upnpscanner.cpp:248
UPNPScanner::ParseService
static void ParseService(QDomElement &element, QString &controlURL, QString &eventURL)
Definition: upnpscanner.cpp:1377
UPNPScanner::Debug
void Debug(void)
Definition: upnpscanner.cpp:797