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  for (auto *BE : qAsConst(map))
96  {
97  if (!BE->GetDeviceDesc())
98  {
99  LOG(VB_GENERAL, LOG_INFO, LOC + QString("GetDeviceDesc() failed for %1").arg(BE->GetFriendlyName()));
100  continue;
101  }
102 
103  QString friendlyName = BE->GetDeviceDesc()->m_rootDevice.m_sFriendlyName;
104  QString ip = BE->GetDeviceDesc()->m_hostUrl.host();
105  int port = BE->GetDeviceDesc()->m_hostUrl.port();
106 
107  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Found possible VBox at %1 (%2:%3)").arg(friendlyName).arg(ip).arg(port));
108 
109  if (friendlyName.startsWith("VBox"))
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).arg(ip).arg(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 QString();
160  }
161 
162  QString id = devItems.at(0).trimmed();
163 
164  // if we already have an ip address use that
165  QRegExp ipRegExp(R"([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})");
166  if (id.indexOf(ipRegExp) == 0)
167  return id;
168 
169  // we must have a vbox id so look it up to find the ip address
170  QStringList vboxes = VBox::probeDevices();
171 
172  for (int x = 0; x < vboxes.count(); x++)
173  {
174  QStringList vboxItems = vboxes.at(x).split(" ");
175  if (vboxItems.size() != 4)
176  {
177  LOG(VB_GENERAL, LOG_INFO, LOC + QString("Got malformed probed device %1").arg(vboxes.at(x)));
178  continue;
179  }
180 
181  const QString& vboxID = vboxItems.at(0);
182  QString vboxIP = vboxItems.at(1);
183 
184  if (vboxID == id)
185  return vboxIP;
186  }
187 
188  // if we get here we didn't find it
189  return QString();
190 }
191 
192 QDomDocument *VBox::getBoardInfo(void)
193 {
194  auto *xmlDoc = new QDomDocument();
195  QString query = QUERY_BOARDINFO;
196 
197  query.replace("{URL}", m_url);
198 
199  if (!sendQuery(query, xmlDoc))
200  {
201  delete xmlDoc;
202  return nullptr;
203  }
204 
205  return xmlDoc;
206 }
207 
209 {
210  // assume if we can download the board info we have a good connection
211  return (getBoardInfo() != nullptr);
212 }
213 
215 {
216  QString requiredVersion = VBOX_MIN_API_VERSION;
217  QStringList sList = requiredVersion.split('.');
218 
219  // sanity check this looks like a VBox version string
220  if (sList.count() < 3 || !requiredVersion.startsWith("V"))
221  {
222  LOG(VB_GENERAL, LOG_INFO, LOC + QString("Failed to parse required version from %1").arg(requiredVersion));
223  version = "UNKNOWN";
224  return false;
225  }
226 
227  int requiredMajor = sList[1].toInt();
228  int requiredMinor = sList[2].toInt();
229 
230  int major = 0;
231  int minor = 0;
232 
233  QDomDocument *xmlDoc = getBoardInfo();
234  QDomElement elem = xmlDoc->documentElement();
235 
236  if (!elem.isNull())
237  {
238  version = getStrValue(elem, "SoftwareVersion");
239 
240  sList = version.split('.');
241 
242  // sanity check this looks like a VBox version string
243  if (sList.count() < 3 || !(version.startsWith("VB.") || version.startsWith("VJ.")))
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").arg(VBOX_MIN_API_VERSION).arg(version));
257 
258  if (major < requiredMajor)
259  return false;
260 
261  if (major == requiredMajor && minor < requiredMinor)
262  return false;
263 
264  return true;
265 }
266 
268 QStringList VBox::getTuners(void)
269 {
270  QStringList result;
271 
272  QDomDocument *xmlDoc = getBoardInfo();
273  QDomElement elem = xmlDoc->documentElement();
274 
275  if (!elem.isNull())
276  {
277  int noTuners = getIntValue(elem, "TunersNumber");
278 
279  for (int x = 1; x <= noTuners; x++)
280  {
281  QString tuner = getStrValue(elem, QString("Tuner%1").arg(x));
282  QString s = QString("%1 %2").arg(x).arg(tuner);
283  result.append(s);
284  }
285  }
286 
287  delete xmlDoc;
288 
289  return result;
290 }
291 
292 
294 {
295  auto *result = new vbox_chan_map_t;
296  auto *xmlDoc = new QDomDocument();
297  QString query = QUERY_CHANNELS;
298 
299  query.replace("{URL}", m_url);
300 
301  if (!sendQuery(query, xmlDoc))
302  {
303  delete xmlDoc;
304  delete result;
305  return nullptr;
306  }
307 
308  QDomNodeList chanNodes = xmlDoc->elementsByTagName("channel");
309 
310  for (int x = 0; x < chanNodes.count(); x++)
311  {
312  QDomElement chanElem = chanNodes.at(x).toElement();
313  QString xmltvid = chanElem.attribute("id", "UNKNOWN_ID");
314  QString name = getStrValue(chanElem, "display-name", 0);
315  QString chanType = getStrValue(chanElem, "display-name", 1);
316  QString triplet = getStrValue(chanElem, "display-name", 2);
317  bool fta = (getStrValue(chanElem, "display-name", 3) == "Free");
318  QString lcn = getStrValue(chanElem, "display-name", 4);
319  uint serviceID = triplet.right(4).toUInt(nullptr, 16);
320 
321  QString transType = "UNKNOWN";
322  QStringList slist = triplet.split('-');
323  uint networkID = slist[2].left(4).toUInt(nullptr, 16);
324  uint transportID = slist[2].mid(4, 4).toUInt(nullptr, 16);
325  LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("NIT/TID/SID %1 %2 %3)").arg(networkID).arg(transportID).arg(serviceID));
326 
327  //sanity check - the triplet should look something like this: T-GER-111100020001
328  // where T is the tuner type, GER is the country, and the numbers are the NIT/TID/SID
329  if (slist.count() == 3)
330  transType = slist[0];
331 
332  QString icon = "";
333  QDomNodeList iconNodes = chanElem.elementsByTagName("icon");
334  if (iconNodes.count())
335  {
336  QDomElement iconElem = iconNodes.at(0).toElement();
337  icon = iconElem.attribute("src", "");
338  }
339 
340  QString url = "";
341  QDomNodeList urlNodes = chanElem.elementsByTagName("url");
342  if (urlNodes.count())
343  {
344  QDomElement urlElem = urlNodes.at(0).toElement();
345  url = urlElem.attribute("src", "");
346  }
347 
348  VBoxChannelInfo chanInfo(name, xmltvid, url, fta, chanType, transType, serviceID, networkID, transportID);
349  result->insert(lcn, chanInfo);
350  }
351 
352  return result;
353 }
354 
355 bool VBox::sendQuery(const QString& query, QDomDocument* xmlDoc)
356 {
357  QByteArray result;
358 
359  if (!GetMythDownloadManager()->download(query, &result, true))
360  return false;
361 
362  QString errorMsg;
363  int errorLine = 0;
364  int errorColumn = 0;
365 
366  if (!xmlDoc->setContent(result, false, &errorMsg, &errorLine, &errorColumn))
367  {
368  LOG(VB_GENERAL, LOG_ERR, LOC +
369  QString("Error parsing: %1\nat line: %2 column: %3 msg: %4").
370  arg(query).arg(errorLine).arg(errorColumn).arg(errorMsg));
371  return false;
372  }
373 
374  // check for a status or error element
375  QDomNodeList statusNodes = xmlDoc->elementsByTagName("Status");
376 
377  if (!statusNodes.count())
378  statusNodes = xmlDoc->elementsByTagName("Error");
379 
380  if (statusNodes.count())
381  {
382  QDomElement elem = statusNodes.at(0).toElement();
383  if (!elem.isNull())
384  {
385  ErrorCode errorCode = (ErrorCode)getIntValue(elem, "ErrorCode");
386  QString errorDesc = getStrValue(elem, "ErrorDescription");
387 
388  if (errorCode == SUCCESS)
389  return true;
390 
391  LOG(VB_GENERAL, LOG_ERR, LOC +
392  QString("API Error: %1 - %2, Query was: %3").arg(errorCode).arg(errorDesc).arg(query));
393 
394  return false;
395  }
396  }
397 
398  // no error detected so assume we got a valid xml result
399  return true;
400 }
401 
402 QString VBox::getStrValue(QDomElement &element, const QString &name, int index)
403 {
404  QDomNodeList nodes = element.elementsByTagName(name);
405  if (!nodes.isEmpty())
406  {
407  if (index >= nodes.count())
408  index = 0;
409  QDomElement e = nodes.at(index).toElement();
410  return getFirstText(e);
411  }
412 
413  return QString();
414 }
415 
416 int VBox::getIntValue(QDomElement &element, const QString &name, int index)
417 {
418  QString value = getStrValue(element, name, index);
419 
420  return value.toInt();
421 }
422 
423 QString VBox::getFirstText(QDomElement &element)
424 {
425  for (QDomNode dname = element.firstChild(); !dname.isNull();
426  dname = dname.nextSibling())
427  {
428  QDomText t = dname.toText();
429  if (!t.isNull())
430  return t.data();
431  }
432  return QString();
433 }
VBox::checkVersion
bool checkVersion(QString &version)
Definition: vboxutils.cpp:214
e
QDomElement e
Definition: mythplugins/mytharchive/mytharchivehelper/main.cpp:1417
VBox::getTuners
QStringList getTuners(void)
returns a list of tuners in the format 'TUNERNO TUNERTYPE' eg '1 DVBT/T2'
Definition: vboxutils.cpp:268
VBox::checkConnection
bool checkConnection(void)
Definition: vboxutils.cpp:208
ReferenceCounter::DecrRef
virtual int DecrRef(void)
Decrements reference count and deletes on 0.
Definition: referencecounter.cpp:125
vbox_chan_map_t
QMap< QString, VBoxChannelInfo > vbox_chan_map_t
Definition: vboxchannelfetcher.h:61
QUERY_CHANNELS
#define QUERY_CHANNELS
Definition: vboxutils.cpp:20
MythTimer
A QElapsedTimer based timer to replace use of QTime as a timer.
Definition: mythtimer.h:13
ssdp.h
arg
arg(title).arg(filename).arg(doDelete))
SSDP::Find
static SSDPCacheEntries * Find(const QString &sURI)
Definition: ssdp.h:126
VBOX_MIN_API_VERSION
#define VBOX_MIN_API_VERSION
Definition: vboxutils.h:11
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:84
VBox::getFirstText
static QString getFirstText(QDomElement &element)
Definition: vboxutils.cpp:423
SSDP::Instance
static SSDP * Instance()
Definition: ssdp.cpp:54
minor
#define minor(X)
Definition: compat.h:138
mythlogging.h
hardwareprofile.i18n.t
t
Definition: i18n.py:36
LOC
#define LOC
Definition: vboxutils.cpp:17
VBox::ErrorCode
ErrorCode
Definition: vboxutils.h:30
VBox::doUPNPSearch
static QStringList doUPNPSearch(void)
Definition: vboxutils.cpp:68
EntryMap
QMap< QString, DeviceLocation * > EntryMap
Key == Unique Service Name (USN)
Definition: ssdpcache.h:28
vboxutils.h
VBox::VBox
VBox(const QString &url)
Definition: vboxutils.cpp:26
SSDPCacheEntries
Definition: ssdpcache.h:34
uint
unsigned int uint
Definition: compat.h:140
VBOX_URI
#define VBOX_URI
Definition: vboxutils.cpp:24
VBox::getChannels
vbox_chan_map_t * getChannels(void)
Definition: vboxutils.cpp:293
MythTimer::elapsed
int elapsed(void)
Returns milliseconds elapsed since last start() or restart()
Definition: mythtimer.cpp:90
VBox::getBoardInfo
QDomDocument * getBoardInfo(void)
Definition: vboxutils.cpp:192
QUERY_BOARDINFO
#define QUERY_BOARDINFO
Definition: vboxutils.cpp:19
VBox
Definition: vboxutils.h:13
SSDP::PerformSearch
void PerformSearch(const QString &sST, uint timeout_secs=2)
Definition: ssdp.cpp:201
SEARCH_TIME
#define SEARCH_TIME
Definition: vboxutils.cpp:23
VBox::probeDevices
static QStringList probeDevices(void)
Definition: vboxutils.cpp:32
mythtimer.h
VBox::SUCCESS
@ SUCCESS
Definition: vboxutils.h:32
VBox::getStrValue
static QString getStrValue(QDomElement &element, const QString &name, int index=0)
Definition: vboxutils.cpp:402
VBox::m_url
QString m_url
Definition: vboxutils.h:45
VBox::sendQuery
static bool sendQuery(const QString &query, QDomDocument *xmlDoc)
Definition: vboxutils.cpp:355
mythdownloadmanager.h
SSDPCacheEntries::Count
uint Count(void) const
Definition: ssdpcache.h:44
query
MSqlQuery query(MSqlQuery::InitCon())
nv_python_libs.bbciplayer.bbciplayer_api.version
string version
Definition: bbciplayer_api.py:81
VBox::getIntValue
static int getIntValue(QDomElement &element, const QString &name, int index=0)
Definition: vboxutils.cpp:416
VBox::getIPFromVideoDevice
static QString getIPFromVideoDevice(const QString &dev)
Definition: vboxutils.cpp:149
MSqlQuery::at
int at(void) const
Definition: mythdbcon.h:210
GetMythDownloadManager
MythDownloadManager * GetMythDownloadManager(void)
Gets the pointer to the MythDownloadManager singleton.
Definition: mythdownloadmanager.cpp:161
VBoxChannelInfo
Definition: vboxchannelfetcher.h:22