14#include <QXmlStreamReader>
37 const auto *k = (
const uchar *)ba.data();
45 uint g = (h & 0xf0000000);
61 if (timestr.isEmpty())
63 LOG(VB_XMLTV, LOG_ERR,
"Found empty Date/Time in XMLTV data, ignoring");
67 QStringList split = timestr.split(
" ", Qt::SkipEmptyParts);
68 QString ts = split[0];
80 if (tzoffset ==
"GMT" || tzoffset ==
"UTC")
82 else if (tzoffset ==
"BST")
91 ts.truncate(ts.length()-1);
96 static bool s_warnedOnceOnImplicitUtc =
false;
97 if (!s_warnedOnceOnImplicitUtc)
99 LOG(VB_XMLTV, LOG_WARNING,
"No explicit time zone found, "
100 "guessing implicit UTC! Please consider enhancing "
101 "the guide source to provide explicit UTC or local "
103 s_warnedOnceOnImplicitUtc =
true;
109 QString tsDate = ts.left(8);
110 if (tsDate.length() == 8)
112 else if (tsDate.length() == 6)
114 else if (tsDate.length() == 4)
116 if (!tmpDate.isValid())
118 LOG(VB_XMLTV, LOG_ERR,
119 QString(
"Invalid datetime (date) in XMLTV data, ignoring: %1")
127 QString tsTime = ts.mid(8);
128 if (tsTime.length() == 6)
130 if (tsTime ==
"235960")
134 else if (tsTime.length() == 4)
138 else if (tsTime.length() == 2)
142 if (!tmpTime.isValid())
145 LOG(VB_XMLTV, LOG_ERR,
146 QString(
"Invalid datetime (time) in XMLTV data, ignoring: %1")
152#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
153 QDateTime tmpDT = QDateTime(tmpDate, tmpTime, Qt::UTC);
155 QDateTime tmpDT = QDateTime(tmpDate, tmpTime, QTimeZone(QTimeZone::UTC));
157 if (!tmpDT.isValid())
159 LOG(VB_XMLTV, LOG_ERR,
160 QString(
"Invalid datetime (combination of date/time) "
161 "in XMLTV data, ignoring: %1").arg(timestr));
166 QString isoDateString = tmpDT.toString(
Qt::ISODate);
167 if (isoDateString.endsWith(
'Z'))
168 isoDateString.truncate(isoDateString.length()-1);
169 isoDateString += tzoffset;
174 LOG(VB_XMLTV, LOG_ERR,
175 QString(
"Invalid datetime (zone offset) in XMLTV data, "
176 "ignoring: %1").arg(timestr));
188 LOG(VB_GENERAL, LOG_ERR, QString(
"Malformed XML file at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString()));
196 QMap<QString, QList<ProgInfo> > *proglist)
203 LOG(VB_GENERAL, LOG_ERR,
204 QString(
"Error unable to open '%1' for reading.") .arg(
filename));
211 if (
info.size() == 0)
213 LOG(VB_GENERAL, LOG_WARNING,
214 QString(
"File %1 exists but is empty. Did the grabber fail?").arg(
filename));
220 QXmlStreamReader xml(&f);
223 QString aggregatedTitle;
224 QString aggregatedDesc;
225 bool haveReadTV =
false;
226 while (!xml.atEnd() && !xml.hasError() && (! (xml.isEndElement() && xml.name() == QString(
"tv"))))
231 QStringRef text = xml.text();
232 QStringRef name = xml.dtdName();
233 QStringRef publicId = xml.dtdPublicId();
234 QStringRef systemId = xml.dtdSystemId();
235 QXmlStreamEntityDeclarations entities = xml.entityDeclarations();
236 QXmlStreamNotationDeclarations notations = xml.notationDeclarations();
238 QString msg = QString(
"DTD %1 name %2 PublicId %3 SystemId %4")
239 .arg(text).arg(name).arg(publicId).arg(systemId);
241 if (!entities.isEmpty())
244 for (
const auto entity : entities)
245 msg += QString(
":name %1 PublicId %2 SystemId %3 ")
247 .arg(entity.publicId())
248 .arg(entity.systemId());
251 if (!notations.isEmpty())
254 for (
const auto notation : notations)
255 msg += QString(
": name %1 PublicId %2 SystemId %3 ")
256 .arg(notation.name())
257 .arg(notation.publicId())
258 .arg(notation.systemId());
261 LOG(VB_XMLTV, LOG_INFO, msg);
265 if (xml.readNextStartElement())
267 if (xml.name() == QString(
"tv"))
270 baseUrl = QUrl(xml.attributes().value(
"source-data-url").toString());
273 if (xml.name() == QString(
"channel"))
277 LOG(VB_GENERAL, LOG_ERR, QString(
"Malformed XML file, no <tv> element found, at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString()));
283 xmltvid = xml.attributes().value(
"id").toString();
286 chaninfo->m_tvFormat =
"Default";
289 while (!xml.isEndElement() || (xml.name() != QString(
"channel")))
296 if (xml.name() == QString(
"icon"))
298 if (chaninfo->m_icon.isEmpty())
300 QString path = xml.attributes().value(
"src").toString();
301 if (!path.isEmpty() && !path.contains(
"://"))
303 QString base = baseUrl.toString(QUrl::StripTrailingSlash);
304 chaninfo->m_icon = base +
305 ((path.startsWith(
"/")) ? path : QString(
"/") + path);
307 else if (!path.isEmpty())
311 chaninfo->m_icon = url.toString();
315 else if (xml.name() == QString(
"display-name"))
319 text = xml.readElementText(QXmlStreamReader::SkipChildElements);
322 if (chaninfo->m_name.isEmpty())
324 chaninfo->m_name = text;
326 else if (chaninfo->m_callSign.isEmpty())
328 chaninfo->m_callSign = text;
330 else if (chaninfo->m_chanNum.isEmpty())
332 chaninfo->m_chanNum = text;
337 chaninfo->m_freqId = chaninfo->m_chanNum;
339 if (!chaninfo->m_xmltvId.isEmpty())
340 chanlist->push_back(*chaninfo);
343 else if (xml.name() == QString(
"programme"))
347 LOG(VB_GENERAL, LOG_ERR, QString(
"Malformed XML file, no <tv> element found, at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString()));
354 QString totalepisodes;
357 QString text = xml.attributes().value(
"start").toString();
359 pginfo->m_startts = text;
361 text = xml.attributes().value(
"stop").toString();
364 pginfo->m_endts = text;
366 text = xml.attributes().value(
"channel").toString();
367 QStringList split = text.split(
" ");
368 pginfo->m_channel = split[0];
370 text = xml.attributes().value(
"clumpidx").toString();
373 split = text.split(
'/');
374 pginfo->m_clumpidx = split[0];
375 pginfo->m_clumpmax = split[1];
378 while (!xml.isEndElement() || (xml.name() != QString(
"programme")))
385 if (xml.name() == QString(
"title"))
387 QString text2=xml.readElementText(QXmlStreamReader::SkipChildElements);
388 if (xml.attributes().value(
"lang").toString() ==
"ja_JP")
390 pginfo->m_title = text2;
392 else if (xml.attributes().value(
"lang").toString() ==
"ja_JP@kana")
394 pginfo->m_title_pronounce = text2;
396 else if (pginfo->m_title.isEmpty())
398 pginfo->m_title = text2;
401 else if (xml.name() == QString(
"sub-title") && pginfo->m_subtitle.isEmpty())
403 pginfo->m_subtitle = xml.readElementText(QXmlStreamReader::SkipChildElements);
405 else if (xml.name() == QString(
"subtitles"))
407 if (xml.attributes().value(
"type").toString() ==
"teletext")
408 pginfo->m_subtitleType |= SUB_NORMAL;
409 else if (xml.attributes().value(
"type").toString() ==
"onscreen")
410 pginfo->m_subtitleType |= SUB_ONSCREEN;
411 else if (xml.attributes().value(
"type").toString() ==
"deaf-signed")
412 pginfo->m_subtitleType |= SUB_SIGNED;
414 else if (xml.name() == QString(
"desc") && pginfo->m_description.isEmpty())
416 pginfo->m_description = xml.readElementText(QXmlStreamReader::SkipChildElements);
418 else if (xml.name() == QString(
"category"))
420 const QString
cat = xml.readElementText(QXmlStreamReader::SkipChildElements);
426 else if (pginfo->m_category.isEmpty())
428 pginfo->m_category =
cat;
430 if ((
cat.compare(QObject::tr(
"movie"),Qt::CaseInsensitive) == 0) || (
cat.compare(QObject::tr(
"film"),Qt::CaseInsensitive) == 0))
435 pginfo->m_genres.append(
cat);
437 else if (xml.name() == QString(
"date") && (pginfo->m_airdate == 0U))
440 QString date = xml.readElementText(QXmlStreamReader::SkipChildElements);
441 pginfo->m_airdate = date.left(4).toUInt();
443 else if (xml.name() == QString(
"star-rating"))
466 while (!xml.isEndElement() || (xml.name() != QString(
"star-rating")))
470 if (xml.isStartElement())
472 if (xml.name() == QString(
"value"))
474 stars=xml.readElementText(QXmlStreamReader::SkipChildElements);
478 if (pginfo->m_stars == 0.0F)
480 float num = stars.section(
'/', 0, 0).toFloat() + 1;
481 float den = stars.section(
'/', 1, 1).toFloat() + 1;
487 else if (xml.name() == QString(
"rating"))
492 QString rating_system = xml.attributes().value(
"system").toString();
493 if (rating_system ==
nullptr)
496 while (!xml.isEndElement() || (xml.name() != QString(
"rating")))
500 if (xml.isStartElement())
502 if (xml.name() == QString(
"value"))
504 rat=xml.readElementText(QXmlStreamReader::SkipChildElements);
512 rating.m_system = rating_system;
514 pginfo->m_ratings.append(
rating);
517 else if (xml.name() == QString(
"previously-shown"))
519 pginfo->m_previouslyshown =
true;
520 QString prevdate = xml.attributes().value(
"start").toString();
521 if ((!prevdate.isEmpty()) && (pginfo->m_originalairdate.isNull()))
525 pginfo->m_originalairdate = date.date();
528 else if (xml.name() == QString(
"credits"))
531 while (!xml.isEndElement() || (xml.name() != QString(
"credits")))
535 if (xml.isStartElement())
538 QString character = xml.attributes()
539 .value(
"role").toString();
540 QString tagname = xml.name().toString();
541 if (tagname ==
"actor")
543 QString guest = xml.attributes()
547 tagname =
"guest_star";
549 QString name = xml.readElementText(QXmlStreamReader::SkipChildElements);
550 QStringList characters = character.split(
"/", Qt::SkipEmptyParts);
551 if (characters.isEmpty())
553 pginfo->AddPerson(tagname, name,
554 priority, character);
559 for (
auto & c : characters)
561 pginfo->AddPerson(tagname, name,
570 else if (xml.name() == QString(
"audio"))
572 while (!xml.isEndElement() || (xml.name() != QString(
"audio")))
576 if (xml.isStartElement())
578 if (xml.name() == QString(
"stereo"))
580 QString text2=xml.readElementText(QXmlStreamReader::SkipChildElements);
583 pginfo->m_audioProps |= AUD_MONO;
585 else if (text2 ==
"stereo")
587 pginfo->m_audioProps |= AUD_STEREO;
589 else if (text2 ==
"dolby" || text2 ==
"dolby digital")
591 pginfo->m_audioProps |= AUD_DOLBY;
593 else if (text2 ==
"surround")
595 pginfo->m_audioProps |= AUD_SURROUND;
601 else if (xml.name() == QString(
"video"))
603 while (!xml.isEndElement() || (xml.name() != QString(
"video")))
607 if (xml.isStartElement())
609 if (xml.name() == QString(
"quality"))
611 if (xml.readElementText(QXmlStreamReader::SkipChildElements) ==
"HDTV")
612 pginfo->m_videoProps |= VID_HDTV;
614 else if (xml.name() == QString(
"aspect"))
616 if (xml.readElementText(QXmlStreamReader::SkipChildElements) ==
"16:9")
617 pginfo->m_videoProps |= VID_WIDESCREEN;
622 else if (xml.name() == QString(
"episode-num"))
624 QString system = xml.attributes().value(
"system").toString();
625 if (system ==
"dd_progid")
627 QString episodenum(xml.readElementText(QXmlStreamReader::SkipChildElements));
629 int idx = episodenum.indexOf(
'.');
631 episodenum.remove(idx, 1);
632 programid = episodenum;
634 if (programid.startsWith(QString(
"EP")) ||
635 programid.startsWith(QString(
"SH")))
636 pginfo->m_seriesId = QString(
"EP") + programid.mid(2,8);
638 else if (system ==
"xmltv_ns")
640 QString episodenum(xml.readElementText(QXmlStreamReader::SkipChildElements));
641 episode = episodenum.section(
'.',1,1);
642 totalepisodes = episode.section(
'/',1,1).trimmed();
643 episode = episode.section(
'/',0,0).trimmed();
644 season = episodenum.section(
'.',0,0).trimmed();
645 season = season.section(
'/',0,0).trimmed();
646 QString part(episodenum.section(
'.',2,2));
647 QString partnumber(part.section(
'/',0,0).trimmed());
648 QString parttotal(part.section(
'/',1,1).trimmed());
650 if (!season.isEmpty())
652 int tmp = season.toUInt() + 1;
653 pginfo->m_season =
tmp;
654 season = QString::number(
tmp);
655 pginfo->m_syndicatedepisodenumber =
'S' + season;
657 if (!episode.isEmpty())
659 int tmp = episode.toUInt() + 1;
660 pginfo->m_episode =
tmp;
661 episode = QString::number(
tmp);
662 pginfo->m_syndicatedepisodenumber.append(
'E' + episode);
664 if (!totalepisodes.isEmpty())
666 pginfo->m_totalepisodes = totalepisodes.toUInt();
669 if (!partnumber.isEmpty())
672 partno = partnumber.toUInt(&ok) + 1;
673 partno = (ok) ? partno : 0;
675 if (!parttotal.isEmpty() && partno > 0)
678 uint partto = parttotal.toUInt(&ok);
679 if (ok && partnumber <= parttotal)
681 pginfo->m_parttotal = partto;
682 pginfo->m_partnumber = partno;
686 else if (system ==
"onscreen")
689 if (pginfo->m_subtitle.isEmpty())
691 pginfo->m_subtitle = xml.readElementText(QXmlStreamReader::SkipChildElements);
694 else if ((system ==
"themoviedb.org") && (
m_movieGrabberPath.endsWith(QString(
"/tmdb3.py"))))
697 QString inetrefRaw(xml.readElementText(QXmlStreamReader::SkipChildElements));
698 if (inetrefRaw.startsWith(QString(
"movie/")))
700 QString inetref(QString (
"tmdb3.py_") + inetrefRaw.section(
'/',1,1).trimmed());
701 pginfo->m_inetref = inetref;
704 else if ((system ==
"thetvdb.com") && (
m_tvGrabberPath.endsWith(QString(
"/ttvdb4.py"))))
707 QString inetrefRaw(xml.readElementText(QXmlStreamReader::SkipChildElements));
708 if (inetrefRaw.startsWith(QString(
"series/")))
710 QString inetref(QString (
"ttvdb4.py_") + inetrefRaw.section(
'/',1,1).trimmed());
711 pginfo->m_inetref = inetref;
715 else if (system ==
"schedulesdirect.org")
717 QString details(xml.readElementText(QXmlStreamReader::SkipChildElements));
718 if (details.startsWith(QString(
"originalAirDate/")))
720 QString value(details.section(
'/', 1, 1).trimmed());
723 pginfo->m_originalairdate = datetime.date();
725 else if (details.startsWith(QString(
"newEpisode/")))
727 QString value(details.section(
'/', 1, 1).trimmed());
728 if (value == QString(
"true"))
730 pginfo->m_previouslyshown =
false;
732 else if (value == QString(
"false"))
734 pginfo->m_previouslyshown =
true;
747 if (programid.isEmpty())
759 QString seriesid = QString::number(
ELFHash(pginfo->m_title.toUtf8()));
760 pginfo->m_seriesId = seriesid;
761 programid.append(seriesid);
763 if (!episode.isEmpty() && !season.isEmpty())
769 int season_int = season.toInt();
779 programid.append(episode);
780 programid.append(QString::number(season_int, 36));
781 if (pginfo->m_partnumber && pginfo->m_parttotal)
783 programid += QString::number(pginfo->m_partnumber);
784 programid += QString::number(pginfo->m_parttotal);
796 pginfo->m_programId = programid;
797 if (!(pginfo->m_starttime.isValid()))
799 LOG(VB_GENERAL, LOG_WARNING, QString(
"Invalid programme (%1), " "invalid start time, " "skipping").arg(pginfo->m_title));
801 else if (pginfo->m_channel.isEmpty())
803 LOG(VB_GENERAL, LOG_WARNING, QString(
"Invalid programme (%1), " "missing channel, " "skipping").arg(pginfo->m_title));
805 else if (pginfo->m_startts == pginfo->m_endts)
807 LOG(VB_GENERAL, LOG_WARNING, QString(
"Invalid programme (%1), " "identical start and end " "times, skipping").arg(pginfo->m_title));
812 if (pginfo->m_clumpidx.isEmpty())
813 (*proglist)[pginfo->m_channel].push_back(*pginfo);
817 if (pginfo->m_clumpidx.toInt() == 0)
819 aggregatedTitle.clear();
820 aggregatedDesc.clear();
822 if (!pginfo->m_title.isEmpty())
824 if (!aggregatedTitle.isEmpty())
825 aggregatedTitle.append(
" | ");
826 aggregatedTitle.append(pginfo->m_title);
828 if (!pginfo->m_description.isEmpty())
830 if (!aggregatedDesc.isEmpty())
831 aggregatedDesc.append(
" | ");
832 aggregatedDesc.append(pginfo->m_description);
834 if (pginfo->m_clumpidx.toInt() == pginfo->m_clumpmax.toInt() - 1)
836 pginfo->m_title = aggregatedTitle;
837 pginfo->m_description = aggregatedDesc;
838 (*proglist)[pginfo->m_channel].push_back(*pginfo);
846 if (! (xml.isEndElement() && xml.name() == QString(
"tv")))
848 LOG(VB_GENERAL, LOG_ERR, QString(
"Malformed XML file, missing </tv> element, at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString()));
std::vector< ChannelInfo > ChannelInfoList
unsigned int m_currentYear
bool parseFile(const QString &filename, ChannelInfoList *chanlist, QMap< QString, QList< ProgInfo > > *proglist)
QString m_movieGrabberPath
bool dash_open(QFile &file, const QString &filename, int m, FILE *handle)
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
QString toString(const QDateTime &raw_dt, uint format)
Returns formatted string representing the time.
@ kFilename
Default UTC, "yyyyMMddhhmmss".
QDateTime fromString(const QString &dtstr)
Converts kFilename && kISODate formats to QDateTime.
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
def rating(profile, smoonURL, gate)
QString myth_category_type_to_string(ProgramInfo::CategoryType category_type)
ProgramInfo::CategoryType string_to_myth_category_type(const QString &category_type)
static void fromXMLTVDate(QString ×tr, QDateTime &dt)
static bool readNextWithErrorCheck(QXmlStreamReader &xml)
static uint ELFHash(const QByteArray &ba)