MythTV  master
vboxutils.cpp
Go to the documentation of this file.
1 #include <chrono> // for milliseconds
2 #include <thread> // for sleep_for
3 
4 // Qt
5 #include <QString>
6 #include <QStringList>
7 #include <QDomDocument>
8 
9 // MythTV headers
12 #include "libmythbase/mythtimer.h"
13 #include "libmythupnp/ssdp.h"
14 #include "vboxutils.h"
15 
16 #define LOC QString("VBox: ")
17 
18 static constexpr const char* QUERY_BOARDINFO
19 { "http://{URL}/cgi-bin/HttpControl/HttpControlApp?OPTION=1&Method=QueryBoardInfo" };
20 static constexpr const char* QUERY_CHANNELS
21 { "http://{URL}/cgi-bin/HttpControl/HttpControlApp?OPTION=1&Method=GetXmltvChannelsList" \
22  "&FromChIndex=FirstChannel&ToChIndex=LastChannel&FilterBy=All" };
23 
24 static constexpr std::chrono::milliseconds SEARCH_TIME { 3s };
25 static constexpr const char* VBOX_URI { "urn:schemas-upnp-org:device:MediaServer:1" };
26 static constexpr const char* VBOX_UDN { "uuid:b7531642-0123-3210" };
27 
28 // static method
29 QStringList VBox::probeDevices(void)
30 {
31  const std::chrono::milliseconds milliSeconds { SEARCH_TIME };
32  auto seconds = duration_cast<std::chrono::seconds>(milliSeconds);
33 
34  // see if we have already found one or more vboxes
35  QStringList result = VBox::doUPNPSearch();
36 
37  if (!result.isEmpty())
38  return result;
39 
40  // non found so start a new search
41  LOG(VB_GENERAL, LOG_INFO, LOC + QString("Using UPNP to search for Vboxes (%1 secs)")
42  .arg(seconds.count()));
43 
45 
46  // Search for a total of 'milliSeconds' ms, sending new search packet
47  // about every 250 ms until less than one second remains.
48  MythTimer totalTime; totalTime.start();
49  MythTimer searchTime; searchTime.start();
50  while (totalTime.elapsed() < milliSeconds)
51  {
52  std::this_thread::sleep_for(25ms);
53  auto ttl = duration_cast<std::chrono::seconds>(milliSeconds - totalTime.elapsed());
54  if ((searchTime.elapsed() > 249ms) && (ttl > 1s))
55  {
56  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("UPNP Search %1 secs")
57  .arg(ttl.count()));
59  searchTime.start();
60  }
61  }
62 
63  return VBox::doUPNPSearch();
64 }
65 
66 QStringList VBox::doUPNPSearch(void)
67 {
68  QStringList result;
69 
71 
72  if (!vboxes)
73  {
74  LOG(VB_GENERAL, LOG_DEBUG, LOC + "No UPnP VBoxes found");
75  return {};
76  }
77 
78  int count = vboxes->Count();
79  if (count)
80  {
81  LOG(VB_GENERAL, LOG_DEBUG, LOC +
82  QString("Found %1 possible VBoxes").arg(count));
83  }
84  else
85  {
86  LOG(VB_GENERAL, LOG_ERR, LOC +
87  "No UPnP VBoxes found, but SSDP::Find() not NULL");
88  }
89 
90  EntryMap map;
91  vboxes->GetEntryMap(map);
92 
93  for (auto *BE : std::as_const(map))
94  {
95  if (!BE->GetDeviceDesc())
96  {
97  LOG(VB_GENERAL, LOG_INFO, LOC + QString("GetDeviceDesc() failed for %1").arg(BE->GetFriendlyName()));
98  continue;
99  }
100 
101  QString friendlyName = BE->GetDeviceDesc()->m_rootDevice.m_sFriendlyName;
102  QString ip = BE->GetDeviceDesc()->m_hostUrl.host();
103  QString udn = BE->GetDeviceDesc()->m_rootDevice.m_sUDN;
104  int port = BE->GetDeviceDesc()->m_hostUrl.port();
105 
106  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Found possible VBox at %1 (%2:%3)")
107  .arg(friendlyName, ip, QString::number(port)));
108 
109  if (udn.startsWith(VBOX_UDN))
110  {
111  // we found one
112  QString id;
113  int startPos = friendlyName.indexOf('(');
114  int endPos = friendlyName.indexOf(')');
115 
116  if (startPos != -1 && endPos != -1)
117  id = friendlyName.mid(startPos + 1, endPos - startPos - 1);
118  else
119  id = friendlyName;
120 
121  // get a list of tuners on this VBOX
122  QStringList tuners;
123 
124  VBox *vbox = new VBox(ip);
125  tuners = vbox->getTuners();
126  delete vbox;
127 
128  for (int x = 0; x < tuners.count(); x++)
129  {
130  // add a device in the format ID IP TUNERNO TUNERTYPE
131  // eg vbox_3718 192.168.1.204 1 DVBT/T2
132  const QString& tuner = tuners.at(x);
133  QString device = QString("%1 %2 %3").arg(id, ip, tuner);
134  result << device;
135  LOG(VB_GENERAL, LOG_INFO, QString("Found VBox - %1").arg(device));
136  }
137  }
138 
139  BE->DecrRef();
140  }
141 
142  vboxes->DecrRef();
143  vboxes = nullptr;
144 
145  return result;
146 }
147 
148 // static method
149 QString VBox::getIPFromVideoDevice(const QString& dev)
150 {
151  // dev is of the form xx.xx.xx.xx-n-t or xxxxxxx-n-t
152  // where xx is either an ip address or vbox id
153  // n is the tuner number and t is the tuner type ie DVBT/T2
154  QStringList devItems = dev.split("-");
155 
156  if (devItems.size() != 3)
157  {
158  LOG(VB_GENERAL, LOG_INFO, LOC + QString("Got malformed videodev %1").arg(dev));
159  return {};
160  }
161 
162  QString id = devItems.at(0).trimmed();
163 
164  // if we already have an ip address use that
165  static const QRegularExpression ipRE { R"(^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$)" };
166  auto match = ipRE.match(id);
167  if (match.hasMatch())
168  return id;
169 
170  // we must have a vbox id so look it up to find the ip address
171  QStringList vboxes = VBox::probeDevices();
172 
173  for (int x = 0; x < vboxes.count(); x++)
174  {
175  QStringList vboxItems = vboxes.at(x).split(" ");
176  if (vboxItems.size() != 4)
177  {
178  LOG(VB_GENERAL, LOG_INFO, LOC + QString("Got malformed probed device %1").arg(vboxes.at(x)));
179  continue;
180  }
181 
182  const QString& vboxID = vboxItems.at(0);
183  QString vboxIP = vboxItems.at(1);
184 
185  if (vboxID == id)
186  return vboxIP;
187  }
188 
189  // if we get here we didn't find it
190  return {};
191 }
192 
193 QDomDocument *VBox::getBoardInfo(void)
194 {
195  auto *xmlDoc = new QDomDocument();
196  QString query = QUERY_BOARDINFO;
197 
198  query.replace("{URL}", m_url);
199 
200  if (!sendQuery(query, xmlDoc))
201  {
202  delete xmlDoc;
203  return nullptr;
204  }
205 
206  return xmlDoc;
207 }
208 
210 {
211  // assume if we can download the board info we have a good connection
212  return (getBoardInfo() != nullptr);
213 }
214 
216 {
217  QString requiredVersion = VBOX_MIN_API_VERSION;
218  QStringList sList = requiredVersion.split('.');
219 
220  // sanity check this looks like a VBox version string
221  if (sList.count() < 3 || !requiredVersion.startsWith("V"))
222  {
223  LOG(VB_GENERAL, LOG_INFO, LOC + QString("Failed to parse required version from %1").arg(requiredVersion));
224  version = "UNKNOWN";
225  return false;
226  }
227 
228  int requiredMajor = sList[1].toInt();
229  int requiredMinor = sList[2].toInt();
230 
231  int major = 0;
232  int minor = 0;
233 
234  QDomDocument *xmlDoc = getBoardInfo();
235  QDomElement elem = xmlDoc->documentElement();
236 
237  if (!elem.isNull())
238  {
239  version = getStrValue(elem, "SoftwareVersion");
240 
241  sList = version.split('.');
242 
243  // sanity check this looks like a VBox version string
244  if (sList.count() < 3 || !(version.startsWith("VB.") || version.startsWith("VJ.")
245  || version.startsWith("VT.")))
246  {
247  LOG(VB_GENERAL, LOG_INFO, LOC + QString("Failed to parse version from %1").arg(version));
248  delete xmlDoc;
249  return false;
250  }
251 
252  major = sList[1].toInt();
253  minor = sList[2].toInt();
254  }
255 
256  delete xmlDoc;
257 
258  LOG(VB_GENERAL, LOG_INFO, LOC + QString("CheckVersion - required: %1, actual: %2")
260 
261  if (major < requiredMajor)
262  return false;
263 
264  if (major == requiredMajor && minor < requiredMinor)
265  return false;
266 
267  return true;
268 }
269 
271 QStringList VBox::getTuners(void)
272 {
273  QStringList result;
274 
275  QDomDocument *xmlDoc = getBoardInfo();
276  QDomElement elem = xmlDoc->documentElement();
277 
278  if (!elem.isNull())
279  {
280  int noTuners = getIntValue(elem, "TunersNumber");
281 
282  for (int x = 1; x <= noTuners; x++)
283  {
284  QString tuner = getStrValue(elem, QString("Tuner%1").arg(x));
285  QString s = QString("%1 %2").arg(x).arg(tuner);
286  result.append(s);
287  }
288  }
289 
290  delete xmlDoc;
291 
292  return result;
293 }
294 
295 
297 {
298  auto *result = new vbox_chan_map_t;
299  auto *xmlDoc = new QDomDocument();
300  QString query = QUERY_CHANNELS;
301 
302  query.replace("{URL}", m_url);
303 
304  if (!sendQuery(query, xmlDoc))
305  {
306  delete xmlDoc;
307  delete result;
308  return nullptr;
309  }
310 
311  QDomNodeList chanNodes = xmlDoc->elementsByTagName("channel");
312 
313  for (int x = 0; x < chanNodes.count(); x++)
314  {
315  QDomElement chanElem = chanNodes.at(x).toElement();
316  QString xmltvid = chanElem.attribute("id", "UNKNOWN_ID");
317  QString name = getStrValue(chanElem, "display-name", 0);
318  QString chanType = getStrValue(chanElem, "display-name", 1);
319  QString triplet = getStrValue(chanElem, "display-name", 2);
320  bool fta = (getStrValue(chanElem, "display-name", 3) == "Free");
321  QString lcn = getStrValue(chanElem, "display-name", 4);
322  uint serviceID = triplet.right(4).toUInt(nullptr, 16);
323 
324  QString transType = "UNKNOWN";
325  QStringList slist = triplet.split('-');
326  uint networkID = slist[2].left(4).toUInt(nullptr, 16);
327  uint transportID = slist[2].mid(4, 4).toUInt(nullptr, 16);
328  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("NIT/TID/SID %1 %2 %3)").arg(networkID).arg(transportID).arg(serviceID));
329 
330  //sanity check - the triplet should look something like this: T-GER-111100020001
331  // where T is the tuner type, GER is the country, and the numbers are the NIT/TID/SID
332  if (slist.count() == 3)
333  transType = slist[0];
334 
335  QString icon = "";
336  QDomNodeList iconNodes = chanElem.elementsByTagName("icon");
337  if (iconNodes.count())
338  {
339  QDomElement iconElem = iconNodes.at(0).toElement();
340  icon = iconElem.attribute("src", "");
341  }
342 
343  QString url = "";
344  QDomNodeList urlNodes = chanElem.elementsByTagName("url");
345  if (urlNodes.count())
346  {
347  QDomElement urlElem = urlNodes.at(0).toElement();
348  url = urlElem.attribute("src", "");
349  }
350 
351  VBoxChannelInfo chanInfo(name, xmltvid, url, fta, chanType, transType, serviceID, networkID, transportID);
352  result->insert(lcn, chanInfo);
353  }
354 
355  return result;
356 }
357 
358 bool VBox::sendQuery(const QString& query, QDomDocument* xmlDoc)
359 {
360  QByteArray result;
361 
362  if (!GetMythDownloadManager()->download(query, &result, true))
363  return false;
364 
365  QString errorMsg;
366  int errorLine = 0;
367  int errorColumn = 0;
368 
369  if (!xmlDoc->setContent(result, false, &errorMsg, &errorLine, &errorColumn))
370  {
371  LOG(VB_GENERAL, LOG_ERR, LOC +
372  QString("Error parsing: %1\nat line: %2 column: %3 msg: %4").
373  arg(query).arg(errorLine).arg(errorColumn).arg(errorMsg));
374  return false;
375  }
376 
377  // check for a status or error element
378  QDomNodeList statusNodes = xmlDoc->elementsByTagName("Status");
379 
380  if (!statusNodes.count())
381  statusNodes = xmlDoc->elementsByTagName("Error");
382 
383  if (statusNodes.count())
384  {
385  QDomElement elem = statusNodes.at(0).toElement();
386  if (!elem.isNull())
387  {
388  ErrorCode errorCode = (ErrorCode)getIntValue(elem, "ErrorCode");
389  QString errorDesc = getStrValue(elem, "ErrorDescription");
390 
391  if (errorCode == SUCCESS)
392  return true;
393 
394  LOG(VB_GENERAL, LOG_ERR, LOC +
395  QString("API Error: %1 - %2, Query was: %3").arg(errorCode).arg(errorDesc, query));
396 
397  return false;
398  }
399  }
400 
401  // no error detected so assume we got a valid xml result
402  return true;
403 }
404 
405 QString VBox::getStrValue(const QDomElement &element, const QString &name, int index)
406 {
407  QDomNodeList nodes = element.elementsByTagName(name);
408  if (!nodes.isEmpty())
409  {
410  if (index >= nodes.count())
411  index = 0;
412  QDomElement e = nodes.at(index).toElement();
413  return getFirstText(e);
414  }
415 
416  return {};
417 }
418 
419 int VBox::getIntValue(const QDomElement &element, const QString &name, int index)
420 {
421  QString value = getStrValue(element, name, index);
422 
423  return value.toInt();
424 }
425 
426 QString VBox::getFirstText(QDomElement &element)
427 {
428  for (QDomNode dname = element.firstChild(); !dname.isNull();
429  dname = dname.nextSibling())
430  {
431  QDomText t = dname.toText();
432  if (!t.isNull())
433  return t.data();
434  }
435  return {};
436 }
VBox::checkVersion
bool checkVersion(QString &version)
Definition: vboxutils.cpp:215
MythTimer::elapsed
std::chrono::milliseconds elapsed(void)
Returns milliseconds elapsed since last start() or restart()
Definition: mythtimer.cpp:91
VBox::VBox
VBox(QString url)
Definition: vboxutils.h:19
VBox::getTuners
QStringList getTuners(void)
returns a list of tuners in the format 'TUNERNO TUNERTYPE' eg '1 DVBT/T2'
Definition: vboxutils.cpp:271
VBox::checkConnection
bool checkConnection(void)
Definition: vboxutils.cpp:209
ReferenceCounter::DecrRef
virtual int DecrRef(void)
Decrements reference count and deletes on 0.
Definition: referencecounter.cpp:125
SSDP::PerformSearch
void PerformSearch(const QString &sST, std::chrono::seconds timeout=2s)
Definition: ssdp.cpp:204
vbox_chan_map_t
QMap< QString, VBoxChannelInfo > vbox_chan_map_t
Definition: vboxchannelfetcher.h:62
MythTimer
A QElapsedTimer based timer to replace use of QTime as a timer.
Definition: mythtimer.h:13
ssdp.h
SSDP::Find
static SSDPCacheEntries * Find(const QString &sURI)
Definition: ssdp.h:132
MythTimer::start
void start(void)
starts measuring elapsed time.
Definition: mythtimer.cpp:47
LOG
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
SSDPCacheEntries::GetEntryMap
void GetEntryMap(EntryMap &map)
Returns a copy of the EntryMap.
Definition: ssdpcache.cpp:85
QUERY_BOARDINFO
static constexpr const char * QUERY_BOARDINFO
Definition: vboxutils.cpp:19
VBox::getFirstText
static QString getFirstText(QDomElement &element)
Definition: vboxutils.cpp:426
VBOX_UDN
static constexpr const char * VBOX_UDN
Definition: vboxutils.cpp:26
SSDP::Instance
static SSDP * Instance()
Definition: ssdp.cpp:55
VBOX_MIN_API_VERSION
static constexpr const char * VBOX_MIN_API_VERSION
Definition: vboxutils.h:14
minor
#define minor(X)
Definition: compat.h:78
mythlogging.h
hardwareprofile.i18n.t
t
Definition: i18n.py:36
LOC
#define LOC
Definition: vboxutils.cpp:16
VBox::ErrorCode
ErrorCode
Definition: vboxutils.h:35
VBox::doUPNPSearch
static QStringList doUPNPSearch(void)
Definition: vboxutils.cpp:66
EntryMap
QMap< QString, DeviceLocation * > EntryMap
Key == Unique Service Name (USN)
Definition: ssdpcache.h:29
vboxutils.h
VBox::getStrValue
static QString getStrValue(const QDomElement &element, const QString &name, int index=0)
Definition: vboxutils.cpp:405
SSDPCacheEntries
Definition: ssdpcache.h:35
uint
unsigned int uint
Definition: compat.h:81
SEARCH_TIME
static constexpr std::chrono::milliseconds SEARCH_TIME
Definition: vboxutils.cpp:24
VBox::getChannels
vbox_chan_map_t * getChannels(void)
Definition: vboxutils.cpp:296
VBox::getBoardInfo
QDomDocument * getBoardInfo(void)
Definition: vboxutils.cpp:193
VBOX_URI
static constexpr const char * VBOX_URI
Definition: vboxutils.cpp:25
VBox
Definition: vboxutils.h:16
VBox::probeDevices
static QStringList probeDevices(void)
Definition: vboxutils.cpp:29
mythtimer.h
VBox::SUCCESS
@ SUCCESS
Definition: vboxutils.h:37
VBox::m_url
QString m_url
Definition: vboxutils.h:50
VBox::sendQuery
static bool sendQuery(const QString &query, QDomDocument *xmlDoc)
Definition: vboxutils.cpp:358
mythdownloadmanager.h
SSDPCacheEntries::Count
uint Count(void) const
Definition: ssdpcache.h:45
QUERY_CHANNELS
static constexpr const char * QUERY_CHANNELS
Definition: vboxutils.cpp:21
nv_python_libs.bbciplayer.bbciplayer_api.version
string version
Definition: bbciplayer_api.py:77
VBox::getIPFromVideoDevice
static QString getIPFromVideoDevice(const QString &dev)
Definition: vboxutils.cpp:149
GetMythDownloadManager
MythDownloadManager * GetMythDownloadManager(void)
Gets the pointer to the MythDownloadManager singleton.
Definition: mythdownloadmanager.cpp:145
VBox::getIntValue
static int getIntValue(const QDomElement &element, const QString &name, int index=0)
Definition: vboxutils.cpp:419
VBoxChannelInfo
Definition: vboxchannelfetcher.h:23