11#include <QRegularExpression>
25#define LOC QString("IPTVChanFetch: ")
38 uint cardid, QString inputname,
uint sourceid,
40 m_scanMonitor(monitor),
41 m_cardId(cardid), m_inputName(std::move(inputname)),
42 m_sourceId(sourceid), m_isMpts(is_mpts),
43 m_thread(new
MThread(
"IPTVChannelFetcher", this))
45 LOG(VB_CHANNEL, LOG_INFO,
LOC + QString(
"Has ScanMonitor %1")
46 .arg(monitor?
"true":
"false"));
81 LOG(VB_CHANNEL, LOG_INFO,
LOC + QString(
"Found %1 channels")
99 if (!logoUrl.isEmpty())
104 QString iconDir =
GetConfDir() +
"/channels/";
105 QString filepath = iconDir +
filename;
106 QFile
file(filepath);
107 if (
file.open(QIODevice::WriteOnly))
109 if (
file.write(data) > 0)
112 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
113 QString(
"DownloadLogo to file %1").arg(filepath));
117 LOG(VB_CHANNEL, LOG_ERR,
LOC +
118 QString(
"DownloadLogo failed to write to file %1").arg(filepath));
124 LOG(VB_CHANNEL, LOG_ERR,
LOC +
125 QString(
"DownloadLogo failed to open file %1").arg(filepath));
130 LOG(VB_CHANNEL, LOG_ERR,
LOC +
131 QString(
"DownloadLogo failed to download %1").arg(logoUrl));
136 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
"DownloadLogo empty logoUrl");
152 LOG(VB_CHANNEL, LOG_INFO,
LOC +
"Playlist URL is empty");
153 QMutexLocker locker(&
m_lock);
159 LOG(VB_CHANNEL, LOG_INFO,
LOC + QString(
"Playlist URL: %1").arg(url));
175 QCoreApplication::translate(
"(Common)",
"Error"));
179 QMutexLocker locker(&
m_lock);
185 LOG(VB_CHANNEL, LOG_DEBUG,
LOC + QString(
"Playlist file size: %1").arg(playlist.length()));
202 LOG(VB_CHANNEL, LOG_INFO,
LOC + QString(
"Found %1 channels")
208 std::vector<fbox_chan_map_t::const_iterator> acno (
m_channels.size());
210 fbox_chan_map_t::const_iterator it =
m_channels.cbegin();
215 std::ranges::sort(acno,
216 [] (fbox_chan_map_t::const_iterator s1, fbox_chan_map_t::const_iterator s2) ->
bool
218 return s1.key().toInt() < s2.key().toInt();
224 for (
uint i = 0; i < acno.size(); ++i)
226 fbox_chan_map_t::const_iterator it = acno[i];
227 const QString& channum = it.key();
228 QString name = (*it).m_name;
229 QString xmltvid = (*it).m_xmltvid.isEmpty() ?
"" : (*it).m_xmltvid;
230 QString logoUrl = (*it).m_logo;
234 if (!logoUrl.isEmpty())
243 uint programnumber = (*it).m_programNumber;
245 QString msg = tr(
"Channel #%1 : %2").arg(channum, name);
247 LOG(VB_CHANNEL, LOG_INFO, QString(
"Handling channel %1 %2")
248 .arg(channum, name));
259 channum, programnumber, 0, 0,
261 logo,
"Default", xmltvid);
269 tr(
"Updating %1").arg(msg));
272 channum, programnumber, 0, 0,
274 logo,
"Default", xmltvid);
290 QMutexLocker locker(&
m_lock);
298 uint range = 70 - minval;
307 uint range = 100 - minval;
322 if (url.startsWith(
"file", Qt::CaseInsensitive))
326 QFile
file(qurl.toLocalFile());
327 if (!
file.open(QIODevice::ReadOnly))
329 LOG(VB_GENERAL, LOG_ERR,
LOC + QString(
"Opening '%1'")
330 .arg(qurl.toLocalFile()) +
ENO);
334 while (!
file.atEnd())
336 QByteArray data =
file.readLine();
337 ret += QString(data);
354 LOG(VB_GENERAL, LOG_ERR,
LOC +
355 QString(
"DownloadPlaylist failed to "
356 "download from %1").arg(url));
367 QString url = rawdata.section(
"\n", numLine, numLine, QString::SectionSkipEmpty);
372 if (!url.startsWith(
"#"))
383 QString rawdata = reallyrawdata;
384 rawdata.replace(
"\r\n",
"\n");
387 QString header = rawdata.section(
"\n", 0, 0);
388 if (!header.startsWith(
"#EXTM3U"))
390 LOG(VB_GENERAL, LOG_ERR,
LOC +
391 QString(
"Invalid channel list header (%1)").arg(header));
396 tr(
"ERROR: M3U channel list is malformed"));
408 LOG(VB_CHANNEL, LOG_INFO,
LOC +
409 QString(
"Estimating there are %1 channels in playlist")
417 QString sql =
"SELECT MAX(CONVERT(channum, UNSIGNED INTEGER)) FROM channel "
418 "WHERE sourceid = :SOURCEID AND deleted IS NULL";
431 nextChanNum = query.
value(0).toInt() + 1;
432 LOG(VB_CHANNEL, LOG_INFO,
LOC +
433 QString(
"Next available channel number from DB is: %1").arg(nextChanNum));
438 LOG(VB_CHANNEL, LOG_INFO,
LOC +
439 QString(
"No channels found for this source, using default channel number: %1").arg(nextChanNum));
446 for (
uint i = 1;
true; ++i)
455 if (!channum.isEmpty())
458 int channel_number = channum.toInt(&ok);
459 if (ok && channel_number > 0)
473 if (channum.isEmpty())
477 if (!channum_db.isEmpty())
479 LOG(VB_RECORD, LOG_INFO,
LOC +
480 QString(
"Found channel in database, channel number: %1 for channel: %2")
481 .arg(channum_db,
info.m_name));
483 channum = channum_db;
489 if (!channum.isEmpty())
492 int channel_number = channum.toInt(&ok);
493 if (ok && channel_number > 0)
495 if (channel_number >= nextChanNum)
497 nextChanNum = channel_number + 1;
503 if (channum.isEmpty())
505 LOG(VB_CHANNEL, LOG_INFO, QString(
"No channel number found, using next available: %1 for channel: %2")
506 .arg(nextChanNum).arg(
info.m_name));
507 channum = QString::number(nextChanNum);
511 QString msg = tr(
"Encountered malformed channel");
512 if (!channum.isEmpty())
514 chanmap[channum] =
info;
516 msg = QString(
"Parsing Channel #%1: '%2' %3")
517 .arg(channum,
info.m_name,
518 info.m_tuning.GetDataURL().toString());
519 LOG(VB_CHANNEL, LOG_INFO,
LOC + msg);
555 QMap<QString,QString> values;
559 QString line = rawdata.section(
"\n", lineNum, lineNum, QString::SectionSkipEmpty);
563 LOG(VB_CHANNEL, LOG_INFO,
LOC + line);
566 if (line.startsWith(
"#"))
568 if (line.startsWith(
"#EXTINF:"))
570 parse_extinf(line.mid(line.indexOf(
':')+1), channum, name, logo);
572 else if (line.startsWith(
"#EXTMYTHTV:"))
574 QString data = line.mid(line.indexOf(
':')+1);
575 QString key = data.left(data.indexOf(
'='));
579 name = data.mid(data.indexOf(
'=')+1);
580 else if (key ==
"channum")
581 channum = data.mid(data.indexOf(
'=')+1);
583 values[key] = data.mid(data.indexOf(
'=')+1);
586 else if (line.startsWith(
"#EXTVLCOPT:program="))
588 values[
"programnumber"] = line.mid(line.indexOf(
'=')+1);
596 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
597 QString(
"parse_chan_info name='%2'").arg(name));
598 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
599 QString(
"parse_chan_info channum='%2'").arg(channum));
600 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
601 QString(
"parse_chan_info logo='%2'").arg(logo));
602 for (
auto it = values.cbegin(); it != values.cend(); ++it)
604 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
605 QString(
"parse_chan_info [%1]='%2'")
606 .arg(it.key(), *it));
609 name, values[
"xmltvid"], logo,
610 line, values[
"bitrate"].toUInt(),
612 values[
"fecurl0"], values[
"fecbitrate0"].toUInt(),
613 values[
"fecurl1"], values[
"fecbitrate1"].toUInt(),
614 values[
"programnumber"].toUInt());
623 static const QRegularExpression re_name { R
"(.*,(.*)$)" };
624 auto match = re_name.match(line);
625 if (match.hasMatch())
627 result = match.captured(1).simplified();
636 auto pos = line.indexOf(field, 0, Qt::CaseInsensitive);
640 static const QRegularExpression re { R
"(\"([^\"]*)\"(.*)$)" };
641 auto match = re.match(line);
642 if (match.hasMatch())
644 result = match.captured(1).simplified();
648 LOG(VB_CHANNEL, LOG_INFO,
LOC + QString(
"line:%1 field:%2 result:%3")
649 .arg(line, field, result));
672 if (channum.isEmpty())
684 if (!name.isEmpty() && !channum.isEmpty())
692 static const QRegularExpression re
693 { R
"(^-?\d+,(\d+)(?:\.\s|\s-\s)(.*)$)" };
694 auto match = re.match(line);
695 if (match.hasMatch())
697 channum = match.captured(1);
698 name = match.captured(2);
699 if (!channum.isEmpty() && !name.isEmpty())
708 static const QRegularExpression re
709 { R
"(^-?\d+,\[(\d+)\]\s+(.*)$)" };
710 auto match = re.match(line);
711 if (match.hasMatch())
713 channum = match.captured(1);
714 name = match.captured(2);
715 if (!channum.isEmpty() && !name.isEmpty())
724 static const QRegularExpression re
725 { R
"(^-?\d+,(.*)\s+\[(\d+)\]$)" };
726 auto match = re.match(line);
727 if (match.hasMatch())
729 channum = match.captured(2);
730 name = match.captured(1);
731 if (!channum.isEmpty() && !name.isEmpty())
740 static const QRegularExpression re
741 { R
"(^(-?\d+)\s+[^,]*,\s*(.*)$)" };
742 auto match = re.match(line);
743 if (match.hasMatch())
745 channum = match.captured(1).simplified();
746 name = match.captured(2).simplified();
748 int channel_number = channum.toInt (&ok);
749 if (ok && (channel_number > 0) && !name.isEmpty())
765 QString msg =
LOC + QString(
"Invalid header in channel list line \n\t\t\tEXTINF:%1").arg(line);
766 LOG(VB_GENERAL, LOG_ERR, msg);
static QString GetVideoDevice(uint inputid)
static int GetChanID(int db_mplexid, int service_transport_id, int major_channel, int minor_channel, int program_number)
static QString GetChannelNumber(uint sourceid, const QString &channel_name)
static bool CreateIPTVTuningData(uint channel_id, const IPTVTuningData &tuning)
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 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)
void SetMessage(const QString &status)
static QString DownloadPlaylist(const QString &url)
~IPTVChannelFetcher() override
void SetNumChannelsInserted(uint val)
void Stop(void)
Stops the scanning thread running.
fbox_chan_map_t m_channels
fbox_chan_map_t GetChannels(void)
void SetTotalNumChannels(uint val)
static MTV_PUBLIC fbox_chan_map_t ParsePlaylist(const QString &rawdata, IPTVChannelFetcher *fetcher=nullptr)
ScanMonitor * m_scanMonitor
IPTVChannelFetcher(uint cardid, QString inputname, uint sourceid, bool is_mpts, ScanMonitor *monitor=nullptr)
void SetNumChannelsParsed(uint val)
void Scan(void)
Scans the given frequency list.
QSqlQuery wrapper that fetches a DB connection from the connection pool.
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
bool first(void)
Wrap QSqlQuery::first() so we can display the query results.
QVariant value(int i) const
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
static MSqlQueryInfo InitCon(ConnectionReuse _reuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
This is a wrapper around QThread that does several additional things.
bool isFinished(void) const
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
bool wait(std::chrono::milliseconds time=std::chrono::milliseconds::max())
Wait for the MThread to exit, with a maximum timeout.
static void DBError(const QString &where, const MSqlQuery &query)
void ScanPercentComplete(int pct)
void ScanAppendTextToLog(const QString &status)
void ScanErrored(const QString &error)
static uint estimate_number_of_channels(const QString &rawdata)
static bool parse_chan_info(const QString &rawdata, IPTVChannelInfo &info, QString &channum, uint &lineNum)
static bool parse_extinf(const QString &line, QString &channum, QString &name, QString &logo)
static QString parse_extinf_name_trailing(const QString &line)
static QString parse_extinf_field(QString line, const QString &field)
static bool download_logo(const QString &logoUrl, const QString &filename)
QMap< QString, IPTVChannelInfo > fbox_chan_map_t
MythDownloadManager * GetMythDownloadManager(void)
Gets the pointer to the MythDownloadManager singleton.
#define ENO
This can be appended to the LOG args with "+".
#define LOG(_MASK_, _LEVEL_, _QSTRING_)