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