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