9 #include <QDomDocument>
12 #include <QStringList>
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";
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 while (! (xml.isEndElement() && xml.name() == QString(
"channel")));
338 chaninfo->m_freqId = chaninfo->m_chanNum;
340 if (!chaninfo->m_xmltvId.isEmpty())
341 chanlist->push_back(*chaninfo);
344 else if (xml.name() == QString(
"programme"))
348 LOG(VB_GENERAL, LOG_ERR, QString(
"Malformed XML file, no <tv> element found, at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString()));
355 QString totalepisodes;
358 QString text = xml.attributes().value(
"start").toString();
360 pginfo->m_startts = text;
362 text = xml.attributes().value(
"stop").toString();
365 pginfo->m_endts = text;
367 text = xml.attributes().value(
"channel").toString();
368 QStringList split = text.split(
" ");
369 pginfo->m_channel = split[0];
371 text = xml.attributes().value(
"clumpidx").toString();
374 split = text.split(
'/');
375 pginfo->m_clumpidx = split[0];
376 pginfo->m_clumpmax = split[1];
386 if (xml.name() == QString(
"title"))
388 QString text2=xml.readElementText(QXmlStreamReader::SkipChildElements);
389 if (xml.attributes().value(
"lang").toString() ==
"ja_JP")
391 pginfo->m_title = text2;
393 else if (xml.attributes().value(
"lang").toString() ==
"ja_JP@kana")
395 pginfo->m_title_pronounce = text2;
397 else if (pginfo->m_title.isEmpty())
399 pginfo->m_title = text2;
402 else if (xml.name() == QString(
"sub-title") && pginfo->m_subtitle.isEmpty())
404 pginfo->m_subtitle = xml.readElementText(QXmlStreamReader::SkipChildElements);
406 else if (xml.name() == QString(
"subtitles"))
408 if (xml.attributes().value(
"type").toString() ==
"teletext")
409 pginfo->m_subtitleType |= SUB_NORMAL;
410 else if (xml.attributes().value(
"type").toString() ==
"onscreen")
411 pginfo->m_subtitleType |= SUB_ONSCREEN;
412 else if (xml.attributes().value(
"type").toString() ==
"deaf-signed")
413 pginfo->m_subtitleType |= SUB_SIGNED;
415 else if (xml.name() == QString(
"desc") && pginfo->m_description.isEmpty())
417 pginfo->m_description = xml.readElementText(QXmlStreamReader::SkipChildElements);
419 else if (xml.name() == QString(
"category"))
421 const QString
cat = xml.readElementText(QXmlStreamReader::SkipChildElements);
427 else if (pginfo->m_category.isEmpty())
429 pginfo->m_category =
cat;
431 if ((
cat.compare(QObject::tr(
"movie"),Qt::CaseInsensitive) == 0) || (
cat.compare(QObject::tr(
"film"),Qt::CaseInsensitive) == 0))
436 pginfo->m_genres.append(
cat);
438 else if (xml.name() == QString(
"date") && (pginfo->m_airdate == 0U))
441 QString date = xml.readElementText(QXmlStreamReader::SkipChildElements);
442 pginfo->m_airdate = date.left(4).toUInt();
444 else if (xml.name() == QString(
"star-rating"))
471 if (xml.isStartElement())
473 if (xml.name() == QString(
"value"))
475 stars=xml.readElementText(QXmlStreamReader::SkipChildElements);
479 while (! (xml.isEndElement() && xml.name() == QString(
"star-rating")));
480 if (pginfo->m_stars == 0.0F)
482 float num = stars.section(
'/', 0, 0).toFloat() + 1;
483 float den = stars.section(
'/', 1, 1).toFloat() + 1;
489 else if (xml.name() == QString(
"rating"))
494 QString rating_system = xml.attributes().value(
"system").toString();
495 if (rating_system ==
nullptr)
502 if (xml.isStartElement())
504 if (xml.name() == QString(
"value"))
506 rat=xml.readElementText(QXmlStreamReader::SkipChildElements);
510 while (! (xml.isEndElement() && xml.name() == QString(
"rating")));
515 rating.m_system = rating_system;
517 pginfo->m_ratings.append(
rating);
520 else if (xml.name() == QString(
"previously-shown"))
522 pginfo->m_previouslyshown =
true;
523 QString prevdate = xml.attributes().value(
"start").toString();
524 if ((!prevdate.isEmpty()) && (pginfo->m_originalairdate.isNull()))
528 pginfo->m_originalairdate = date.date();
531 else if (xml.name() == QString(
"credits"))
538 if (xml.isStartElement())
541 QString character = xml.attributes()
542 .value(
"role").toString();
543 QString tagname = xml.name().toString();
544 if (tagname ==
"actor")
546 QString guest = xml.attributes()
550 tagname =
"guest_star";
552 QString name = xml.readElementText(QXmlStreamReader::SkipChildElements);
553 QStringList characters = character.split(
"/", Qt::SkipEmptyParts);
554 if (characters.isEmpty())
556 pginfo->AddPerson(tagname, name,
557 priority, character);
562 for (
auto & c : characters)
564 pginfo->AddPerson(tagname, name,
572 while (! (xml.isEndElement() && xml.name() == QString(
"credits")));
574 else if (xml.name() == QString(
"audio"))
580 if (xml.isStartElement())
582 if (xml.name() == QString(
"stereo"))
584 QString text2=xml.readElementText(QXmlStreamReader::SkipChildElements);
587 pginfo->m_audioProps |= AUD_MONO;
589 else if (text2 ==
"stereo")
591 pginfo->m_audioProps |= AUD_STEREO;
593 else if (text2 ==
"dolby" || text2 ==
"dolby digital")
595 pginfo->m_audioProps |= AUD_DOLBY;
597 else if (text2 ==
"surround")
599 pginfo->m_audioProps |= AUD_SURROUND;
604 while (! (xml.isEndElement() && xml.name() == QString(
"audio")));
606 else if (xml.name() == QString(
"video"))
612 if (xml.isStartElement())
614 if (xml.name() == QString(
"quality"))
616 if (xml.readElementText(QXmlStreamReader::SkipChildElements) ==
"HDTV")
617 pginfo->m_videoProps |= VID_HDTV;
619 else if (xml.name() == QString(
"aspect"))
621 if (xml.readElementText(QXmlStreamReader::SkipChildElements) ==
"16:9")
622 pginfo->m_videoProps |= VID_WIDESCREEN;
626 while (! (xml.isEndElement() && xml.name() == QString(
"video")));
628 else if (xml.name() == QString(
"episode-num"))
630 QString system = xml.attributes().value(
"system").toString();
631 if (system ==
"dd_progid")
633 QString episodenum(xml.readElementText(QXmlStreamReader::SkipChildElements));
635 int idx = episodenum.indexOf(
'.');
637 episodenum.remove(idx, 1);
638 programid = episodenum;
640 if (programid.startsWith(QString(
"EP")) ||
641 programid.startsWith(QString(
"SH")))
642 pginfo->m_seriesId = QString(
"EP") + programid.mid(2,8);
644 else if (system ==
"xmltv_ns")
646 QString episodenum(xml.readElementText(QXmlStreamReader::SkipChildElements));
647 episode = episodenum.section(
'.',1,1);
648 totalepisodes = episode.section(
'/',1,1).trimmed();
649 episode = episode.section(
'/',0,0).trimmed();
650 season = episodenum.section(
'.',0,0).trimmed();
651 season = season.section(
'/',0,0).trimmed();
652 QString part(episodenum.section(
'.',2,2));
653 QString partnumber(part.section(
'/',0,0).trimmed());
654 QString parttotal(part.section(
'/',1,1).trimmed());
656 if (!season.isEmpty())
658 int tmp = season.toUInt() + 1;
659 pginfo->m_season =
tmp;
660 season = QString::number(
tmp);
661 pginfo->m_syndicatedepisodenumber =
'S' + season;
663 if (!episode.isEmpty())
665 int tmp = episode.toUInt() + 1;
666 pginfo->m_episode =
tmp;
667 episode = QString::number(
tmp);
668 pginfo->m_syndicatedepisodenumber.append(
'E' + episode);
670 if (!totalepisodes.isEmpty())
672 pginfo->m_totalepisodes = totalepisodes.toUInt();
675 if (!partnumber.isEmpty())
678 partno = partnumber.toUInt(&ok) + 1;
679 partno = (ok) ? partno : 0;
681 if (!parttotal.isEmpty() && partno > 0)
684 uint partto = parttotal.toUInt(&ok);
685 if (ok && partnumber <= parttotal)
687 pginfo->m_parttotal = partto;
688 pginfo->m_partnumber = partno;
692 else if (system ==
"onscreen")
695 if (pginfo->m_subtitle.isEmpty())
697 pginfo->m_subtitle = xml.readElementText(QXmlStreamReader::SkipChildElements);
700 else if ((system ==
"themoviedb.org") && (
m_movieGrabberPath.endsWith(QString(
"/tmdb3.py"))))
703 QString inetrefRaw(xml.readElementText(QXmlStreamReader::SkipChildElements));
704 if (inetrefRaw.startsWith(QString(
"movie/")))
706 QString inetref(QString (
"tmdb3.py_") + inetrefRaw.section(
'/',1,1).trimmed());
707 pginfo->m_inetref = inetref;
710 else if ((system ==
"thetvdb.com") && (
m_tvGrabberPath.endsWith(QString(
"/ttvdb4.py"))))
713 QString inetrefRaw(xml.readElementText(QXmlStreamReader::SkipChildElements));
714 if (inetrefRaw.startsWith(QString(
"series/")))
716 QString inetref(QString (
"ttvdb4.py_") + inetrefRaw.section(
'/',1,1).trimmed());
717 pginfo->m_inetref = inetref;
721 else if (system ==
"schedulesdirect.org")
723 QString details(xml.readElementText(QXmlStreamReader::SkipChildElements));
724 if (details.startsWith(QString(
"originalAirDate/")))
726 QString value(details.section(
'/', 1, 1).trimmed());
729 pginfo->m_originalairdate = datetime.date();
731 else if (details.startsWith(QString(
"newEpisode/")))
733 QString value(details.section(
'/', 1, 1).trimmed());
734 if (value == QString(
"true"))
736 pginfo->m_previouslyshown =
false;
738 else if (value == QString(
"false"))
740 pginfo->m_previouslyshown =
true;
746 while (! (xml.isEndElement() && xml.name() == QString(
"programme")));
754 if (programid.isEmpty())
766 QString seriesid = QString::number(
ELFHash(pginfo->m_title.toUtf8()));
767 pginfo->m_seriesId = seriesid;
768 programid.append(seriesid);
770 if (!episode.isEmpty() && !season.isEmpty())
776 int season_int = season.toInt();
786 programid.append(episode);
787 programid.append(QString::number(season_int, 36));
788 if (pginfo->m_partnumber && pginfo->m_parttotal)
790 programid += QString::number(pginfo->m_partnumber);
791 programid += QString::number(pginfo->m_parttotal);
803 pginfo->m_programId = programid;
804 if (!(pginfo->m_starttime.isValid()))
806 LOG(VB_GENERAL, LOG_WARNING, QString(
"Invalid programme (%1), " "invalid start time, " "skipping").arg(pginfo->m_title));
808 else if (pginfo->m_channel.isEmpty())
810 LOG(VB_GENERAL, LOG_WARNING, QString(
"Invalid programme (%1), " "missing channel, " "skipping").arg(pginfo->m_title));
812 else if (pginfo->m_startts == pginfo->m_endts)
814 LOG(VB_GENERAL, LOG_WARNING, QString(
"Invalid programme (%1), " "identical start and end " "times, skipping").arg(pginfo->m_title));
819 if (pginfo->m_clumpidx.isEmpty())
820 (*proglist)[pginfo->m_channel].push_back(*pginfo);
824 if (pginfo->m_clumpidx.toInt() == 0)
826 aggregatedTitle.clear();
827 aggregatedDesc.clear();
829 if (!pginfo->m_title.isEmpty())
831 if (!aggregatedTitle.isEmpty())
832 aggregatedTitle.append(
" | ");
833 aggregatedTitle.append(pginfo->m_title);
835 if (!pginfo->m_description.isEmpty())
837 if (!aggregatedDesc.isEmpty())
838 aggregatedDesc.append(
" | ");
839 aggregatedDesc.append(pginfo->m_description);
841 if (pginfo->m_clumpidx.toInt() == pginfo->m_clumpmax.toInt() - 1)
843 pginfo->m_title = aggregatedTitle;
844 pginfo->m_description = aggregatedDesc;
845 (*proglist)[pginfo->m_channel].push_back(*pginfo);
853 if (! (xml.isEndElement() && xml.name() == QString(
"tv")))
855 LOG(VB_GENERAL, LOG_ERR, QString(
"Malformed XML file, missing </tv> element, at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString()));