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