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