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