MythTV master
hdhrchannelfetcher.cpp
Go to the documentation of this file.
1#include "libmythbase/mythconfig.h"
2
3// Std C headers
4#include <cmath>
5#include <unistd.h>
6#include <utility>
7
8// Qt headers
9#include <QFile>
10#include <QRegularExpression>
11#include <QTextStream>
12#include <QString>
13#include <QStringList>
14#include <QDomDocument>
15
16#if CONFIG_HDHOMERUN
17#include HDHOMERUN_HEADERFILE
18#endif
19
20// MythTV headers
23#include "cardutil.h"
24#include "channelutil.h"
25#include "scanmonitor.h"
26#include "hdhrchannelfetcher.h"
27
28#define LOC QString("HDHRChanFetch: ")
29
30namespace {
31
32constexpr const char* QUERY_CHANNELS
33{ "http://{IP}/lineup.xml?tuning" };
34
35QString getFirstText(QDomElement &element)
36{
37 for (QDomNode dname = element.firstChild(); !dname.isNull();
38 dname = dname.nextSibling())
39 {
40 QDomText t = dname.toText();
41 if (!t.isNull())
42 return t.data();
43 }
44 return {};
45}
46
47QString getStrValue(const QDomElement &element, const QString &name, int index=0)
48{
49 QDomNodeList nodes = element.elementsByTagName(name);
50 if (!nodes.isEmpty())
51 {
52 if (index >= nodes.count())
53 index = 0;
54 QDomElement e = nodes.at(index).toElement();
55 return getFirstText(e);
56 }
57 return {};
58}
59
60int getIntValue(const QDomElement &element, const QString &name, int index=0)
61{
62 QString value = getStrValue(element, name, index);
63 return value.toInt();
64}
65
66bool sendQuery(const QString& query, QDomDocument* xmlDoc)
67{
68 QByteArray result;
69
70 if (!GetMythDownloadManager()->download(query, &result, true))
71 return false;
72
73#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
74 QString errorMsg;
75 int errorLine = 0;
76 int errorColumn = 0;
77
78 if (!xmlDoc->setContent(result, false, &errorMsg, &errorLine, &errorColumn))
79 {
80 LOG(VB_GENERAL, LOG_ERR, LOC +
81 QString("Error parsing: %1\nat line: %2 column: %3 msg: %4").
82 arg(query).arg(errorLine).arg(errorColumn).arg(errorMsg));
83 return false;
84 }
85#else
86 auto parseResult = xmlDoc->setContent(result);
87 if (!parseResult)
88 {
89 LOG(VB_GENERAL, LOG_ERR, LOC +
90 QString("Error parsing: %1\nat line: %2 column: %3 msg: %4").
91 arg(query).arg(parseResult.errorLine).arg(parseResult.errorColumn)
92 .arg(parseResult.errorMessage));
93 return false;
94 }
95#endif
96
97 // Check for a status or error element
98 QDomNodeList statusNodes = xmlDoc->elementsByTagName("Status");
99
100 if (!statusNodes.count())
101 statusNodes = xmlDoc->elementsByTagName("Error");
102
103 if (statusNodes.count())
104 {
105 QDomElement elem = statusNodes.at(0).toElement();
106 if (!elem.isNull())
107 {
108 int errorCode = getIntValue(elem, "ErrorCode");
109 QString errorDesc = getStrValue(elem, "ErrorDescription");
110
111 if (errorCode == 0 /* SUCCESS */)
112 return true;
113
114 LOG(VB_GENERAL, LOG_ERR, LOC +
115 QString("API Error: %1 - %2, Query was: %3").arg(errorCode).arg(errorDesc, query));
116
117 return false;
118 }
119 }
120
121 // No error detected so assume we got a valid xml result
122 return true;
123}
124
125hdhr_chan_map_t *getChannels(const QString& ip)
126{
127 auto *result = new hdhr_chan_map_t;
128 auto *xmlDoc = new QDomDocument();
129 QString query = QUERY_CHANNELS;
130
131 query.replace("{IP}", ip);
132
133 if (!sendQuery(query, xmlDoc))
134 {
135 delete xmlDoc;
136 delete result;
137 return nullptr;
138 }
139
140 QDomNodeList chanNodes = xmlDoc->elementsByTagName("Program");
141
142 for (int x = 0; x < chanNodes.count(); x++)
143 {
144 QDomElement chanElem = chanNodes.at(x).toElement();
145 QString guideName = getStrValue(chanElem, "GuideName");
146 QString guideNumber = getStrValue(chanElem, "GuideNumber");
147 QString url = getStrValue(chanElem, "URL");
148 QString modulation = getStrValue(chanElem, "Modulation");
149 QString videoCodec = getStrValue(chanElem, "VideoCodec");
150 QString audioCodec = getStrValue(chanElem, "AudioCodec");
151 uint frequency = getStrValue(chanElem, "Frequency").toUInt();
152 uint serviceID = getStrValue(chanElem, "ProgramNumber").toUInt();
153 uint transportID = getStrValue(chanElem, "TransportStreamID").toUInt();
154 QString onid = getStrValue(chanElem, "OriginalNetworkID");
155
156 // Fixup: Remove leading colon in network ID:
157 // <OriginalNetworkID>:8720</OriginalNetworkID>
158 // This looks a bug in the XML representation, N.B. bug not present in JSON.
159 onid.replace(":", "");
160 uint originalNetworkID = onid.toUInt();
161
162 LOG(VB_CHANSCAN, LOG_DEBUG, LOC + QString("ONID/TID/SID %1 %2 %3")
163 .arg(originalNetworkID).arg(transportID).arg(serviceID));
164
165 HDHRChannelInfo chanInfo(guideName, guideNumber, url, modulation, videoCodec,
166 audioCodec, frequency, serviceID, originalNetworkID, transportID);
167
168 result->insert(guideNumber, chanInfo);
169 }
170 return result;
171}
172
173QString HDHRIPv4Address([[maybe_unused]] const QString &device)
174{
175#if CONFIG_HDHOMERUN
176 hdhomerun_device_t *hdhr =
177 hdhomerun_device_create_from_str(device.toLatin1(), nullptr);
178 if (!hdhr)
179 return {};
180
181 uint32_t ipv4 = hdhomerun_device_get_device_ip(hdhr);
182 hdhomerun_device_destroy(hdhr);
183
184 if (!ipv4)
185 return {};
186
187 return QString("%1.%2.%3.%4").arg(ipv4>>24&0xff).arg(ipv4>>16&0xff).arg(ipv4>>8&0xff).arg(ipv4&0xff);
188#else
189 return {};
190#endif
191}
192
193// Examples of hdhrmod values: a8qam64-6875 a8qam256-6900 t8dvbt2 8vsb
194DTVModulationSystem HDHRMod2Modsys(const QString& hdhrmod)
195{
196 if (hdhrmod.contains("dvbt2"))
198 if (hdhrmod.contains("dvbt"))
200 if (hdhrmod.startsWith("a8qam"))
202 if (hdhrmod.contains("vsb"))
204 if (hdhrmod.contains("psk"))
207}
208
209signed char HDHRMod2Bandwidth(const QString& hdhrmod)
210{
211 if (hdhrmod.startsWith("t8") || hdhrmod.startsWith("a8"))
212 return '8';
213 if (hdhrmod.startsWith("t7") || hdhrmod.startsWith("a7"))
214 return '7';
215 if (hdhrmod.startsWith("t6") || hdhrmod.startsWith("a6"))
216 return '6';
217 return 'a';
218}
219
220uint HDHRMod2SymbolRate(const QString& hdhrmod)
221{
222 static const QRegularExpression re(R"(^(a8qam\d+-)(\d+))");
223 QRegularExpressionMatch match = re.match(hdhrmod);
224 if (match.hasMatch())
225 {
226 QString matched = match.captured(2);
227 return matched.toUInt() * 1000;
228 }
229 return 0;
230}
231
232QString HDHRMod2Modulation(const QString& hdhrmod)
233{
234 if (hdhrmod.contains("qam256"))
236 if (hdhrmod.contains("qam128"))
238 if (hdhrmod.contains("qam64"))
240 if (hdhrmod.contains("qam16"))
242 if (hdhrmod.contains("qpsk"))
244 if (hdhrmod.contains("8vsb"))
247}
248
249void HDHRMajorMinorChannel(QString channum, uint &atsc_major_channel, uint &atsc_minor_channel)
250{
251 if (channum.contains("."))
252 {
253 QChar dot;
254 QTextStream(&channum) >> atsc_major_channel >> dot >> atsc_minor_channel;
255 }
256}
257
258} // namespace
259
260HDHRChannelFetcher::HDHRChannelFetcher(uint cardid, QString inputname, uint sourceid,
261 ServiceRequirements serviceType, ScanMonitor *monitor) :
262 m_scanMonitor(monitor),
263 m_cardId(cardid),
264 m_inputName(std::move(inputname)),
265 m_sourceId(sourceid),
266 m_serviceType(serviceType),
267 m_thread(new MThread("HDHRChannelFetcher", this))
268{
269 LOG(VB_CHANSCAN, LOG_INFO, LOC + QString("Has ScanMonitor %1")
270 .arg(monitor?"true":"false"));
271}
272
274{
275 Stop();
276 delete m_thread;
277 m_thread = nullptr;
278 delete m_channels;
279 m_channels = nullptr;
280}
281
286{
287 m_lock.lock();
288
289 while (m_threadRunning)
290 {
291 m_stopNow = true;
292 m_lock.unlock();
293 m_thread->wait(5ms);
294 m_lock.lock();
295 }
296
297 m_lock.unlock();
298
299 m_thread->wait();
300}
301
303{
304 while (!m_thread->isFinished())
305 m_thread->wait(500ms);
306
307 LOG(VB_CHANSCAN, LOG_INFO, LOC + QString("Found %1 channels")
308 .arg(m_channels->size()));
309 return *m_channels;
310}
311
313{
314 Stop();
315 m_stopNow = false;
316 m_thread->start();
317}
318
320{
321 m_lock.lock();
322 m_threadRunning = true;
323 m_lock.unlock();
324
325 bool usingCableCard = CardUtil::IsCableCardPresent(m_cardId, QString("HDHOMERUN"));
326
327 // Step 1/3 : Get the IP of the HDHomeRun to query
328 QString dev = CardUtil::GetVideoDevice(m_cardId);
329 QString ip = HDHRIPv4Address(dev);
330
331 if (m_stopNow || ip.isEmpty())
332 {
333 LOG(VB_CHANNEL, LOG_INFO, LOC +
334 QString("Failed to get IP address from videodev (%1)").arg(dev));
335 QMutexLocker locker(&m_lock);
336 m_threadRunning = false;
337 m_stopNow = true;
338 return;
339 }
340 LOG(VB_CHANNEL, LOG_INFO, LOC + QString("HDHomeRun IP: %1").arg(ip));
341
342 // Step 2/3 : Download
343 if (m_scanMonitor)
344 {
346 m_scanMonitor->ScanAppendTextToLog(tr("Downloading Channel List"));
347 }
348
349 delete m_channels;
351
352 if (m_stopNow || !m_channels)
353 {
355 {
356 m_scanMonitor->ScanAppendTextToLog(QCoreApplication::translate("(Common)", "Error"));
358 m_scanMonitor->ScanErrored(tr("Downloading Channel List Failed"));
359 }
360 QMutexLocker locker(&m_lock);
361 m_threadRunning = false;
362 m_stopNow = true;
363 return;
364 }
365
366 // Step 3/3 : Process
367 if (m_scanMonitor)
368 {
370 m_scanMonitor->ScanAppendTextToLog(tr("Adding Channels"));
371 }
373 LOG(VB_CHANSCAN, LOG_INFO, LOC + QString("Found %1 channels").arg(m_channels->size()));
374
375 // Add the channels to the DB
376 hdhr_chan_map_t::const_iterator it = m_channels->cbegin();
377 for (uint i = 1; it != m_channels->cend(); ++it, ++i)
378 {
379 const QString& channum = it.key();
380 QString name = (*it).m_name;
381 uint serviceID = (*it).m_serviceID;
382 QString channelType = (*it).m_channelType;
383 QString hdhrmod = (*it).m_modulation;
384 uint networkID = (*it).m_networkID;
385 uint transportID = (*it).m_transportID;
386 uint frequency = (*it).m_frequency;
387
388 DTVModulationSystem modsys = HDHRMod2Modsys(hdhrmod);
389 QString modulation = HDHRMod2Modulation(hdhrmod);
390 uint symbolrate = HDHRMod2SymbolRate(hdhrmod);
391 signed char bandwidth = HDHRMod2Bandwidth(hdhrmod);
392 uint atsc_major_channel = 0;
393 uint atsc_minor_channel = 0;
394 HDHRMajorMinorChannel(channum, atsc_major_channel, atsc_minor_channel);
395 QString sistandard = (atsc_major_channel > 0 && atsc_minor_channel > 0) ? "atsc" : "dvb";
396
397 bool use_on_air_guide = true;
398
399 QString msg = tr("%1 channel %2: %3").arg(channelType).arg(channum, -5, QChar(' ')).arg(name, -15, QChar(' '));
400 LOG(VB_CHANSCAN, LOG_INFO, QString("Found %1").arg(msg));
401
402 if ((channelType == "Radio") && (m_serviceType & kRequireVideo))
403 { // NOLINT(bugprone-branch-clone)
404 // Ignore this radio channel
405 if (m_scanMonitor)
406 {
407 m_scanMonitor->ScanAppendTextToLog(tr("Ignoring %1").arg(msg));
408 }
409 }
410 else if ((channelType == "Data") && (m_serviceType != kRequireNothing))
411 {
412 // Ignore this data channel
413 if (m_scanMonitor)
414 {
415 m_scanMonitor->ScanAppendTextToLog(tr("Ignoring %1").arg(msg));
416 }
417 }
418 else
419 {
420 // This is a TV channel or another channel type that we want
421 int chanid = ChannelUtil::GetChanID(m_sourceId, channum);
422 bool adding_channel = chanid <= 0;
423
424 if (adding_channel)
425 chanid = ChannelUtil::CreateChanID(m_sourceId, channum);
426
427 if (m_scanMonitor)
428 {
429 if (adding_channel)
430 {
431 m_scanMonitor->ScanAppendTextToLog(tr("Adding %1").arg(msg));
432 }
433 else
434 {
435 m_scanMonitor->ScanAppendTextToLog(tr("Updating %1").arg(msg));
436 }
437 }
438
439 uint mplexID {0};
440 QString freqID;
441
442 if (usingCableCard)
443 {
444 // With a CableCard, we're going to use virtual
445 // channel tuning, so no dtv_multiplex is needed.
446 mplexID = 0;
447 // When virtual channel tuning, vchan is acquired from freqid field
448 freqID = channum;
449 }
450 else
451 {
452 // A new dtv_multiplex entry will be created if necessary, otherwise an existing one is returned
453 mplexID = ChannelUtil::CreateMultiplex(m_sourceId, sistandard, frequency, modulation,
454 transportID, networkID, symbolrate, bandwidth,
455 'v', 'a', 'a', QString(), QString(), 'a', QString(),
456 QString(), QString(), modsys.toString(), "0.35");
457 if (mplexID == 0)
458 {
459 LOG(VB_GENERAL, LOG_ERR, QString("No multiplex for %1 sid:%2 freq:%3 url:%4")
460 .arg(msg).arg(serviceID, -5, 10, QChar(' ')).arg(frequency).arg((*it).m_tuning.GetDataURL().toString()));
461 continue;
462 }
463 }
464
465 if (adding_channel)
466 {
467 ChannelUtil::CreateChannel(mplexID, m_sourceId, chanid, name, name,
468 channum, serviceID, atsc_major_channel, atsc_minor_channel,
469 use_on_air_guide, kChannelVisible, freqID,
470 QString(), "Default", QString());
471
472 ChannelUtil::CreateIPTVTuningData(chanid, (*it).m_tuning);
473 }
474 else
475 {
476 ChannelUtil::UpdateChannel(mplexID, m_sourceId, chanid, name, name,
477 channum, serviceID, atsc_major_channel, atsc_minor_channel,
478 use_on_air_guide, kChannelVisible, freqID,
479 QString(), "Default", QString());
480
481 ChannelUtil::UpdateIPTVTuningData(chanid, (*it).m_tuning);
482 }
483 LOG(VB_GENERAL, LOG_INFO, QString("%1 sid:%2 freq:%3 url:%4")
484 .arg(msg).arg(serviceID, -5, 10, QChar(' ')).arg(frequency).arg((*it).m_tuning.GetDataURL().toString()));
485 }
487 }
488
489 if (m_scanMonitor)
490 {
494 }
495
496 QMutexLocker locker(&m_lock);
497 m_threadRunning = false;
498 m_stopNow = true;
499}
500
502{
503 uint minval = 70;
504 uint range = 100 - minval;
505 uint pct = minval + (uint) truncf((((float)val) / m_chanCnt) * range);
506 if (m_scanMonitor)
508}
@ kChannelVisible
Definition: channelinfo.h:23
ServiceRequirements
@ kRequireNothing
@ kRequireVideo
static QString GetVideoDevice(uint inputid)
Definition: cardutil.h:294
static bool IsCableCardPresent(uint inputid, const QString &inputType)
Definition: cardutil.cpp:109
static int GetChanID(int db_mplexid, int service_transport_id, int major_channel, int minor_channel, int program_number)
static bool CreateIPTVTuningData(uint channel_id, const IPTVTuningData &tuning)
Definition: channelutil.h:155
static bool UpdateIPTVTuningData(uint channel_id, const IPTVTuningData &tuning)
static bool UpdateChannel(uint db_mplexid, uint source_id, uint channel_id, const QString &callsign, const QString &service_name, const QString &chan_num, uint service_id, uint atsc_major_channel, uint atsc_minor_channel, bool use_on_air_guide, ChannelVisibleType visible, const QString &freqid=QString(), const QString &icon=QString(), QString format=QString(), const QString &xmltvid=QString(), const QString &default_authority=QString(), uint service_type=0, int recpriority=INT_MIN, int tmOffset=INT_MIN, int commMethod=INT_MIN)
static uint CreateMultiplex(int sourceid, const QString &sistandard, uint64_t frequency, const QString &modulation, int transport_id=-1, int network_id=-1)
static int CreateChanID(uint sourceid, const QString &chan_num)
Creates a unique channel ID for database use.
static bool CreateChannel(uint db_mplexid, uint db_sourceid, uint new_channel_id, const QString &callsign, const QString &service_name, const QString &chan_num, uint service_id, uint atsc_major_channel, uint atsc_minor_channel, bool use_on_air_guide, ChannelVisibleType visible, const QString &freqid, const QString &icon=QString(), QString format="Default", const QString &xmltvid=QString(), const QString &default_authority=QString(), uint service_type=0, int recpriority=0, int tmOffset=0, int commMethod=-1)
QString toString() const
void run(void) override
void SetTotalNumChannels(uint val)
void SetNumChannelsInserted(uint val)
HDHRChannelFetcher(uint cardid, QString inputname, uint sourceid, ServiceRequirements serviceType, ScanMonitor *monitor=nullptr)
hdhr_chan_map_t * m_channels
void Stop(void)
Stops the scanning thread running.
hdhr_chan_map_t GetChannels(void)
ScanMonitor * m_scanMonitor
ServiceRequirements m_serviceType
This is a wrapper around QThread that does several additional things.
Definition: mthread.h:49
bool isFinished(void) const
Definition: mthread.cpp:258
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:283
bool wait(std::chrono::milliseconds time=std::chrono::milliseconds::max())
Wait for the MThread to exit, with a maximum timeout.
Definition: mthread.cpp:300
void ScanPercentComplete(int pct)
void ScanComplete(void)
Definition: scanmonitor.cpp:98
void ScanAppendTextToLog(const QString &status)
void ScanErrored(const QString &error)
unsigned int uint
Definition: freesurround.h:24
#define LOC
QMap< QString, HDHRChannelInfo > hdhr_chan_map_t
MythDownloadManager * GetMythDownloadManager(void)
Gets the pointer to the MythDownloadManager singleton.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
bool sendQuery(const QString &query, QDomDocument *xmlDoc)
hdhr_chan_map_t * getChannels(const QString &ip)
void HDHRMajorMinorChannel(QString channum, uint &atsc_major_channel, uint &atsc_minor_channel)
int getIntValue(const QDomElement &element, const QString &name, int index=0)
QString getStrValue(const QDomElement &element, const QString &name, int index=0)
QString HDHRMod2Modulation(const QString &hdhrmod)
QString HDHRIPv4Address(const QString &device)
signed char HDHRMod2Bandwidth(const QString &hdhrmod)
DTVModulationSystem HDHRMod2Modsys(const QString &hdhrmod)
STL namespace.
static constexpr const char * QUERY_CHANNELS
Definition: vboxutils.cpp:21