Skip to content

Commit

Permalink
IPTV HLS Channel parsing fixes
Browse files Browse the repository at this point in the history
Retrieve channel number from tvg-chno or channel-number tags.
Retrieve channel name from tvg-name; if not present then
search for the channel name at the end of the line.
Retrieve channel logo url from tvg-logo but this is not yet processed.
Process channel names as UTF-8 supporting diacritical marks.

Refs #936
  • Loading branch information
kmdewaal committed Jan 1, 2025
1 parent 92fbe64 commit f247a66
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 88 deletions.
217 changes: 130 additions & 87 deletions mythtv/libs/libmythtv/channelscan/iptvchannelfetcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ static bool parse_chan_info(const QString &rawdata,

static bool parse_extinf(const QString &line,
QString &channum,
QString &name);
QString &name,
QString &logo);

IPTVChannelFetcher::IPTVChannelFetcher(
uint cardid, QString inputname, uint sourceid,
Expand Down Expand Up @@ -160,6 +161,7 @@ void IPTVChannelFetcher::run(void)
const QString& channum = it.key();
QString name = (*it).m_name;
QString xmltvid = (*it).m_xmltvid.isEmpty() ? "" : (*it).m_xmltvid;
QString logo = (*it).m_logo;
uint programnumber = (*it).m_programNumber;
//: %1 is the channel number, %2 is the channel name
QString msg = tr("Channel #%1 : %2").arg(channum, name);
Expand Down Expand Up @@ -241,7 +243,7 @@ QString IPTVChannelFetcher::DownloadPlaylist(const QString &url)
{
if (url.startsWith("file", Qt::CaseInsensitive))
{
QString ret = "";
QString ret;
QUrl qurl(url);
QFile file(qurl.toLocalFile());
if (!file.open(QIODevice::ReadOnly))
Expand All @@ -251,30 +253,31 @@ QString IPTVChannelFetcher::DownloadPlaylist(const QString &url)
return ret;
}

QTextStream stream(&file);
while (!stream.atEnd())
ret += stream.readLine() + "\n";
while (!file.atEnd())
{
QByteArray data = file.readLine();
ret += QString(data);
}

file.close();
return ret;
}

// Use MythDownloadManager for http URLs
QByteArray data;
QString tmp;
QString ret;

if (!GetMythDownloadManager()->download(url, &data))
if (GetMythDownloadManager()->download(url, &data))
{
LOG(VB_GENERAL, LOG_ERR, LOC +
QString("DownloadPlaylist failed to "
"download from %1").arg(url));
ret = QString(data);
}
else
{
tmp = QString(data);
LOG(VB_GENERAL, LOG_ERR, LOC +
QString("DownloadPlaylist failed to "
"download from %1").arg(url));
}

return tmp.isNull() ? tmp : QString::fromUtf8(tmp.toLatin1().constData());
return ret;
}

static uint estimate_number_of_channels(const QString &rawdata)
Expand Down Expand Up @@ -470,6 +473,7 @@ static bool parse_chan_info(const QString &rawdata,
// rtsp://maiptv.iptv.fr/iptvtv/201 <-- url

QString name;
QString logo;
QMap<QString,QString> values;

while (true)
Expand All @@ -485,7 +489,7 @@ static bool parse_chan_info(const QString &rawdata,
{
if (line.startsWith("#EXTINF:"))
{
parse_extinf(line.mid(line.indexOf(':')+1), channum, name);
parse_extinf(line.mid(line.indexOf(':')+1), channum, name, logo);
}
else if (line.startsWith("#EXTMYTHTV:"))
{
Expand Down Expand Up @@ -515,14 +519,16 @@ static bool parse_chan_info(const QString &rawdata,
QString("parse_chan_info name='%2'").arg(name));
LOG(VB_CHANNEL, LOG_DEBUG, LOC +
QString("parse_chan_info channum='%2'").arg(channum));
LOG(VB_CHANNEL, LOG_DEBUG, LOC +
QString("parse_chan_info logo='%2'").arg(logo));
for (auto it = values.cbegin(); it != values.cend(); ++it)
{
LOG(VB_CHANNEL, LOG_DEBUG, LOC +
QString("parse_chan_info [%1]='%2'")
.arg(it.key(), *it));
}
info = IPTVChannelInfo(
name, values["xmltvid"],
name, values["xmltvid"], logo,
line, values["bitrate"].toUInt(),
values["fectype"],
values["fecurl0"], values["fecbitrate0"].toUInt(),
Expand All @@ -532,111 +538,148 @@ static bool parse_chan_info(const QString &rawdata,
}
}

static bool parse_extinf(const QString &line,
QString &channum,
QString &name)
// Search for channel name in last part of the EXTINF line
static QString parse_extinf_name_trailing(const QString line)
{
// Parse extension portion, Freebox or SAT>IP format
static const QRegularExpression chanNumName1
{ R"(^-?\d+,(\d+)(?:\.\s|\s-\s)(.*)$)" };
auto match = chanNumName1.match(line);
QString result;
static const QRegularExpression re_name { R"([^\,+],(.*)$)" };
auto match = re_name.match(line);
if (match.hasMatch())
{
channum = match.captured(1);
name = match.captured(2);
return true;
result = match.captured(1).simplified();
}
return result;
}

// Parse extension portion, A1 TV format
static const QRegularExpression chanNumName2
{ "^-?\\d+\\s+[^,]*tvg-num=\"(\\d+)\"[^,]*,(.*)$" };
match = chanNumName2.match(line);
if (match.hasMatch())
// Search for field value, e.g. field="value", in EXTINF line
static QString parse_extinf_field(QString line, QString field)
{
QString result;
auto pos = line.indexOf(field, 0, Qt::CaseInsensitive);
if (pos > 0)
{
channum = match.captured(1);
name = match.captured(2);
return true;
}
auto lastpart = line.remove(0, pos);

// Parse extension portion, Moviestar TV number then name
static const QRegularExpression chanNumName3
{ R"(^-?\d+,\[(\d+)\]\s+(.*)$)" };
match = chanNumName3.match(line);
if (match.hasMatch())
{
channum = match.captured(1);
name = match.captured(2);
return true;
static const QRegularExpression re { R"(\"([^\"]+)\"(.*)$)" };
auto match = re.match(lastpart);
if (match.hasMatch())
{
result = match.captured(1).simplified();
}
}
return result;
}

// Parse extension portion, Moviestar TV name then number
static const QRegularExpression chanNumName4
{ R"(^-?\d+,(.*)\s+\[(\d+)\]$)" };
match = chanNumName4.match(line);
if (match.hasMatch())
// Search for channel number, channel name and channel logo in EXTINF line
static bool parse_extinf(const QString &line,
QString &channum,
QString &name,
QString &logo)
{

// Parse EXTINF line with TVG fields, Zatto style
// EG. #EXTINF:0001 tvg-id="ITV1London.uk" tvg-chno="90001" group-title="General Interest" tvg-logo="https://images.zattic.com/logos/ee3c9d2ac083eb2154b5/black/210x120.png", ITV 1 HD

// Parse EXTINF line, HDHomeRun style
// EG. #EXTINF:-1 channel-id="22" channel-number="22" tvg-name="Omroep Brabant",22 Omroep Brabant
// #EXTINF:-1 channel-id="2.1" channel-number="2.1" tvg-name="CBS2-HD",2.1 CBS2-HD

// Parse EXTINF line, https://github.com/iptv-org/iptv/blob/master/channels/ style
// EG. #EXTINF:-1 tvg-id="" tvg-name="" tvg-logo="https://i.imgur.com/VejnhiB.png" group-title="News",BBC News
{
channum = match.captured(2);
name = match.captured(1);
return true;
channum = parse_extinf_field(line, "tvg-chno");
if (channum.isEmpty())
{
channum = parse_extinf_field(line, "channel-number");
}
logo = parse_extinf_field(line, "tvg-logo");
name = parse_extinf_field(line, "tvg-name");
if (name.isEmpty())
{
name = parse_extinf_name_trailing(line);
}

// If we only have the name then it might be another format
if (!name.isEmpty() && !channum.isEmpty())
{
return true;
}
}

// Parse extension portion, russion iptv plugin style
static const QRegularExpression chanNumName5
{ R"(^(-?\d+)\s+[^,]*,\s*(.*)$)" };
match = chanNumName5.match(line);
if (match.hasMatch())
// Freebox or SAT>IP format
{
channum = match.captured(1).simplified();
name = match.captured(2).simplified();
bool ok = false;
int channel_number = channum.toInt (&ok);
if (ok && (channel_number > 0))
static const QRegularExpression re
{ R"(^-?\d+,(\d+)(?:\.\s|\s-\s)(.*)$)" };
auto match = re.match(line);
if (match.hasMatch())
{
return true;
channum = match.captured(1);
name = match.captured(2);
if (!channum.isEmpty() && !name.isEmpty())
{
return true;
}
}
}

// Parse extension, HDHomeRun style
// EG. #EXTINF:-1 channel-id="22" channel-number="22" tvg-name="Omroep Brabant",22 Omroep Brabant
// #EXTINF:-1 channel-id="2.1" channel-number="2.1" tvg-name="CBS2-HD",2.1 CBS2-HD
static const QRegularExpression chanNumName6
{ R"(^-?\d+\s+channel-id=\"([^\"]+)\"\s+channel-number=\"([^\"]+)\"\s+tvg-name=\"([^\"]+)\".*$)" };
match = chanNumName6.match(line);
if (match.hasMatch())
// Moviestar TV number then name
{
channum = match.captured(2).simplified();
name = match.captured(3).simplified();
if (!channum.isEmpty() && !name.isEmpty())
static const QRegularExpression re
{ R"(^-?\d+,\[(\d+)\]\s+(.*)$)" };
auto match = re.match(line);
if (match.hasMatch())
{
return true;
channum = match.captured(1);
name = match.captured(2);
if (!channum.isEmpty() && !name.isEmpty())
{
return true;
}
}
}

// Parse extension portion, https://github.com/iptv-org/iptv/blob/master/channels/ style
// EG. #EXTINF:-1 tvg-id="" tvg-name="" tvg-logo="https://i.imgur.com/VejnhiB.png" group-title="News",BBC News
static const QRegularExpression chanNumName7
{ "(^-?\\d+)\\s+[^,]*[^,]*,(.*)$" };
match = chanNumName7.match(line);
if (match.hasMatch())
// Moviestar TV name then number
{
channum = match.captured(1).simplified();
name = match.captured(2).simplified();
return !name.isEmpty();
static const QRegularExpression re
{ R"(^-?\d+,(.*)\s+\[(\d+)\]$)" };
auto match = re.match(line);
if (match.hasMatch())
{
channum = match.captured(2);
name = match.captured(1);
if (!channum.isEmpty() && !name.isEmpty())
{
return true;
}
}
}

// #EXTINF:0,Channel Title
// Parse russion iptv plugin style
{
static const QRegularExpression chanNumName
{ "^(\\d+),(.*)$" };
match = chanNumName.match(line);
static const QRegularExpression re
{ R"(^(-?\d+)\s+[^,]*,\s*(.*)$)" };
auto match = re.match(line);
if (match.hasMatch())
{
channum = match.captured(1).simplified();
name = match.captured(2).simplified();
return !name.isEmpty();
bool ok = false;
int channel_number = channum.toInt (&ok);
if (ok && (channel_number > 0) && !name.isEmpty())
{
return true;
}
}
}

// Almost a catchall: just get the name from the end of the line
// EG. #EXTINF:-1,Channel Title
name = parse_extinf_name_trailing(line);
if (!name.isEmpty())
{
return true;
}

// Not one of the formats we support
QString msg = LOC + QString("Invalid header in channel list line \n\t\t\tEXTINF:%1").arg(line);
LOG(VB_GENERAL, LOG_ERR, msg);
Expand Down
5 changes: 4 additions & 1 deletion mythtv/libs/libmythtv/channelscan/iptvchannelfetcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class IPTVChannelInfo
IPTVChannelInfo() = default;
IPTVChannelInfo(QString name,
QString xmltvid,
QString logo,
const QString &data_url,
uint data_bitrate,
const QString &fec_type,
Expand All @@ -39,7 +40,8 @@ class IPTVChannelInfo
const QString &fec_url1,
uint fec_bitrate1,
uint programnumber) :
m_name(std::move(name)), m_xmltvid(std::move(xmltvid)), m_programNumber(programnumber),
m_name(std::move(name)), m_xmltvid(std::move(xmltvid)), m_logo(std::move(logo)),
m_programNumber(programnumber),
m_tuning(data_url, data_bitrate,
fec_type, fec_url0, fec_bitrate0, fec_url1, fec_bitrate1,
IPTVTuningData::inValid)
Expand All @@ -56,6 +58,7 @@ class IPTVChannelInfo
public:
QString m_name;
QString m_xmltvid;
QString m_logo;
uint m_programNumber {0};
IPTVTuningData m_tuning;
};
Expand Down

0 comments on commit f247a66

Please sign in to comment.