9 #include <QDomDocument>
11 #include <QStringList>
35 const auto *k = (
const uchar *)ba.data();
44 if ((g = (h & 0xf0000000)) != 0)
59 if (timestr.isEmpty())
61 LOG(VB_XMLTV, LOG_ERR,
"Found empty Date/Time in XMLTV data, ignoring");
65 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
66 QStringList split = timestr.split(
" ", QString::SkipEmptyParts);
68 QStringList split = timestr.split(
" ", Qt::SkipEmptyParts);
70 QString ts = split[0];
82 if (tzoffset ==
"GMT" || tzoffset ==
"UTC")
84 else if (tzoffset ==
"BST")
93 ts.truncate(ts.length()-1);
98 static bool s_warnedOnceOnImplicitUtc =
false;
99 if (!s_warnedOnceOnImplicitUtc)
101 LOG(VB_XMLTV, LOG_WARNING,
"No explicit time zone found, "
102 "guessing implicit UTC! Please consider enhancing "
103 "the guide source to provide explicit UTC or local "
105 s_warnedOnceOnImplicitUtc =
true;
111 QString tsDate = ts.left(8);
112 if (tsDate.length() == 8)
114 else if (tsDate.length() == 6)
116 else if (tsDate.length() == 4)
118 if (!tmpDate.isValid())
120 LOG(VB_XMLTV, LOG_ERR,
121 QString(
"Invalid datetime (date) in XMLTV data, ignoring: %1")
129 QString tsTime = ts.mid(8);
130 if (tsTime.length() == 6)
132 if (tsTime ==
"235960")
136 else if (tsTime.length() == 4)
138 else if (tsTime.length() == 2)
140 if (!tmpTime.isValid())
143 LOG(VB_XMLTV, LOG_ERR,
144 QString(
"Invalid datetime (time) in XMLTV data, ignoring: %1")
150 QDateTime tmpDT = QDateTime(tmpDate, tmpTime, Qt::UTC);
151 if (!tmpDT.isValid())
153 LOG(VB_XMLTV, LOG_ERR,
154 QString(
"Invalid datetime (combination of date/time) "
155 "in XMLTV data, ignoring: %1").arg(timestr));
160 QString isoDateString = tmpDT.toString(
Qt::ISODate);
161 if (isoDateString.endsWith(
'Z'))
162 isoDateString.truncate(isoDateString.length()-1);
163 isoDateString += tzoffset;
168 LOG(VB_XMLTV, LOG_ERR,
169 QString(
"Invalid datetime (zone offset) in XMLTV data, "
170 "ignoring: %1").arg(timestr));
182 LOG(VB_GENERAL, LOG_ERR, QString(
"Malformed XML file at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString()));
190 QMap<QString, QList<ProgInfo> > *proglist)
197 LOG(VB_GENERAL, LOG_ERR,
198 QString(
"Error unable to open '%1' for reading.") .arg(
filename));
202 QXmlStreamReader xml(&f);
205 QString aggregatedTitle;
206 QString aggregatedDesc;
207 bool haveReadTV =
false;
208 while (!xml.atEnd() && !xml.hasError() && (! (xml.isEndElement() && xml.name() == QString(
"tv"))))
213 QStringRef text = xml.text();
214 QStringRef name = xml.dtdName();
215 QStringRef publicId = xml.dtdPublicId();
216 QStringRef systemId = xml.dtdSystemId();
217 QXmlStreamEntityDeclarations entities = xml.entityDeclarations();
218 QXmlStreamNotationDeclarations notations = xml.notationDeclarations();
220 QString msg = QString(
"DTD %1 name %2 PublicId %3 SystemId %4")
221 .arg(text).arg(name).arg(publicId).arg(systemId);
223 if (!entities.isEmpty())
226 for (
const auto entity : entities)
227 msg += QString(
":name %1 PublicId %2 SystemId %3 ")
229 .arg(entity.publicId())
230 .arg(entity.systemId());
233 if (!notations.isEmpty())
236 for (
const auto notation : notations)
237 msg += QString(
": name %1 PublicId %2 SystemId %3 ")
238 .arg(notation.name())
239 .arg(notation.publicId())
240 .arg(notation.systemId());
243 LOG(VB_XMLTV, LOG_INFO, msg);
247 if (xml.readNextStartElement())
249 if (xml.name() == QString(
"tv"))
252 baseUrl = QUrl(xml.attributes().value(
"source-data-url").toString());
255 if (xml.name() == QString(
"channel"))
259 LOG(VB_GENERAL, LOG_ERR, QString(
"Malformed XML file, no <tv> element found, at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString()));
265 xmltvid = xml.attributes().value(
"id").toString();
268 chaninfo->m_tvFormat =
"Default";
278 if (xml.name() == QString(
"icon"))
280 if (chaninfo->m_icon.isEmpty())
282 QString path = xml.attributes().value(
"src").toString();
283 if (!path.isEmpty() && !path.contains(
"://"))
285 QString base = baseUrl.toString(QUrl::StripTrailingSlash);
286 chaninfo->m_icon = base +
287 ((path.startsWith(
"/")) ? path : QString(
"/") + path);
289 else if (!path.isEmpty())
293 chaninfo->m_icon = url.toString();
297 else if (xml.name() == QString(
"display-name"))
301 text = xml.readElementText(QXmlStreamReader::SkipChildElements);
304 if (chaninfo->m_name.isEmpty())
306 chaninfo->m_name = text;
308 else if (chaninfo->m_callSign.isEmpty())
310 chaninfo->m_callSign = text;
312 else if (chaninfo->m_chanNum.isEmpty())
314 chaninfo->m_chanNum = text;
319 while (! (xml.isEndElement() && xml.name() == QString(
"channel")));
320 chaninfo->m_freqId = chaninfo->m_chanNum;
322 if (!chaninfo->m_xmltvId.isEmpty())
323 chanlist->push_back(*chaninfo);
326 else if (xml.name() == QString(
"programme"))
330 LOG(VB_GENERAL, LOG_ERR, QString(
"Malformed XML file, no <tv> element found, at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString()));
337 QString totalepisodes;
340 QString text = xml.attributes().value(
"start").toString();
342 pginfo->m_startts = text;
344 text = xml.attributes().value(
"stop").toString();
347 pginfo->m_endts = text;
349 text = xml.attributes().value(
"channel").toString();
350 QStringList split = text.split(
" ");
351 pginfo->m_channel = split[0];
353 text = xml.attributes().value(
"clumpidx").toString();
356 split = text.split(
'/');
357 pginfo->m_clumpidx = split[0];
358 pginfo->m_clumpmax = split[1];
368 if (xml.name() == QString(
"title"))
370 QString text2=xml.readElementText(QXmlStreamReader::SkipChildElements);
371 if (xml.attributes().value(
"lang").toString() ==
"ja_JP")
373 pginfo->m_title = text2;
375 else if (xml.attributes().value(
"lang").toString() ==
"ja_JP@kana")
377 pginfo->m_title_pronounce = text2;
379 else if (pginfo->m_title.isEmpty())
381 pginfo->m_title = text2;
384 else if (xml.name() == QString(
"sub-title") && pginfo->m_subtitle.isEmpty())
386 pginfo->m_subtitle = xml.readElementText(QXmlStreamReader::SkipChildElements);
388 else if (xml.name() == QString(
"subtitles"))
390 if (xml.attributes().value(
"type").toString() ==
"teletext")
391 pginfo->m_subtitleType |= SUB_NORMAL;
392 else if (xml.attributes().value(
"type").toString() ==
"onscreen")
393 pginfo->m_subtitleType |= SUB_ONSCREEN;
394 else if (xml.attributes().value(
"type").toString() ==
"deaf-signed")
395 pginfo->m_subtitleType |= SUB_SIGNED;
397 else if (xml.name() == QString(
"desc") && pginfo->m_description.isEmpty())
399 pginfo->m_description = xml.readElementText(QXmlStreamReader::SkipChildElements);
401 else if (xml.name() == QString(
"category"))
403 const QString
cat = xml.readElementText(QXmlStreamReader::SkipChildElements);
409 else if (pginfo->m_category.isEmpty())
411 pginfo->m_category =
cat;
413 if ((
cat.compare(QObject::tr(
"movie"),Qt::CaseInsensitive) == 0) || (
cat.compare(QObject::tr(
"film"),Qt::CaseInsensitive) == 0))
418 pginfo->m_genres.append(
cat);
420 else if (xml.name() == QString(
"date") && (pginfo->m_airdate == 0U))
423 QString date = xml.readElementText(QXmlStreamReader::SkipChildElements);
424 pginfo->m_airdate = date.left(4).toUInt();
426 else if (xml.name() == QString(
"star-rating"))
453 if (xml.isStartElement())
455 if (xml.name() == QString(
"value"))
457 stars=xml.readElementText(QXmlStreamReader::SkipChildElements);
461 while (! (xml.isEndElement() && xml.name() == QString(
"star-rating")));
462 if (pginfo->m_stars == 0.0F)
464 float num = stars.section(
'/', 0, 0).toFloat() + 1;
465 float den = stars.section(
'/', 1, 1).toFloat() + 1;
471 else if (xml.name() == QString(
"rating"))
476 QString rating_system = xml.attributes().value(
"system").toString();
477 if (rating_system ==
nullptr)
484 if (xml.isStartElement())
486 if (xml.name() == QString(
"value"))
488 rat=xml.readElementText(QXmlStreamReader::SkipChildElements);
492 while (! (xml.isEndElement() && xml.name() == QString(
"rating")));
497 rating.m_system = rating_system;
499 pginfo->m_ratings.append(
rating);
502 else if (xml.name() == QString(
"previously-shown"))
504 pginfo->m_previouslyshown =
true;
505 QString prevdate = xml.attributes().value(
"start").toString();
506 if ((!prevdate.isEmpty()) && (pginfo->m_originalairdate.isNull()))
510 pginfo->m_originalairdate = date.date();
513 else if (xml.name() == QString(
"credits"))
520 if (xml.isStartElement())
523 QString character = xml.attributes()
524 .value(
"role").toString();
525 QString tagname = xml.name().toString();
526 if (tagname ==
"actor")
528 QString guest = xml.attributes()
532 tagname =
"guest_star";
534 QString name = xml.readElementText(QXmlStreamReader::SkipChildElements);
535 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
536 QStringList characters = character.split(
"/", QString::SkipEmptyParts);
538 QStringList characters = character.split(
"/", Qt::SkipEmptyParts);
540 if (characters.isEmpty())
542 pginfo->AddPerson(tagname, name,
543 priority, character);
548 for (
auto & c : characters)
550 pginfo->AddPerson(tagname, name,
558 while (! (xml.isEndElement() && xml.name() == QString(
"credits")));
560 else if (xml.name() == QString(
"audio"))
566 if (xml.isStartElement())
568 if (xml.name() == QString(
"stereo"))
570 QString text2=xml.readElementText(QXmlStreamReader::SkipChildElements);
573 pginfo->m_audioProps |= AUD_MONO;
575 else if (text2 ==
"stereo")
577 pginfo->m_audioProps |= AUD_STEREO;
579 else if (text2 ==
"dolby" || text2 ==
"dolby digital")
581 pginfo->m_audioProps |= AUD_DOLBY;
583 else if (text2 ==
"surround")
585 pginfo->m_audioProps |= AUD_SURROUND;
590 while (! (xml.isEndElement() && xml.name() == QString(
"audio")));
592 else if (xml.name() == QString(
"video"))
598 if (xml.isStartElement())
600 if (xml.name() == QString(
"quality"))
602 if (xml.readElementText(QXmlStreamReader::SkipChildElements) ==
"HDTV")
603 pginfo->m_videoProps |= VID_HDTV;
605 else if (xml.name() == QString(
"aspect"))
607 if (xml.readElementText(QXmlStreamReader::SkipChildElements) ==
"16:9")
608 pginfo->m_videoProps |= VID_WIDESCREEN;
612 while (! (xml.isEndElement() && xml.name() == QString(
"video")));
614 else if (xml.name() == QString(
"episode-num"))
616 QString system = xml.attributes().value(
"system").toString();
617 if (system ==
"dd_progid")
619 QString episodenum(xml.readElementText(QXmlStreamReader::SkipChildElements));
621 int idx = episodenum.indexOf(
'.');
623 episodenum.remove(idx, 1);
624 programid = episodenum;
626 if (programid.startsWith(QString(
"EP")) ||
627 programid.startsWith(QString(
"SH")))
628 pginfo->m_seriesId = QString(
"EP") + programid.mid(2,8);
630 else if (system ==
"xmltv_ns")
632 QString episodenum(xml.readElementText(QXmlStreamReader::SkipChildElements));
633 episode = episodenum.section(
'.',1,1);
634 totalepisodes = episode.section(
'/',1,1).trimmed();
635 episode = episode.section(
'/',0,0).trimmed();
636 season = episodenum.section(
'.',0,0).trimmed();
637 season = season.section(
'/',0,0).trimmed();
638 QString part(episodenum.section(
'.',2,2));
639 QString partnumber(part.section(
'/',0,0).trimmed());
640 QString parttotal(part.section(
'/',1,1).trimmed());
642 if (!season.isEmpty())
644 int tmp = season.toUInt() + 1;
645 pginfo->m_season =
tmp;
646 season = QString::number(
tmp);
647 pginfo->m_syndicatedepisodenumber =
'S' + season;
649 if (!episode.isEmpty())
651 int tmp = episode.toUInt() + 1;
652 pginfo->m_episode =
tmp;
653 episode = QString::number(
tmp);
654 pginfo->m_syndicatedepisodenumber.append(
'E' + episode);
656 if (!totalepisodes.isEmpty())
658 pginfo->m_totalepisodes = totalepisodes.toUInt();
661 if (!partnumber.isEmpty())
664 partno = partnumber.toUInt(&ok) + 1;
665 partno = (ok) ? partno : 0;
667 if (!parttotal.isEmpty() && partno > 0)
670 uint partto = parttotal.toUInt(&ok);
671 if (ok && partnumber <= parttotal)
673 pginfo->m_parttotal = partto;
674 pginfo->m_partnumber = partno;
678 else if (system ==
"onscreen")
681 if (pginfo->m_subtitle.isEmpty())
683 pginfo->m_subtitle = xml.readElementText(QXmlStreamReader::SkipChildElements);
686 else if ((system ==
"themoviedb.org") && (
m_movieGrabberPath.endsWith(QString(
"/tmdb3.py"))))
689 QString inetrefRaw(xml.readElementText(QXmlStreamReader::SkipChildElements));
690 if (inetrefRaw.startsWith(QString(
"movie/")))
692 QString inetref(QString (
"tmdb3.py_") + inetrefRaw.section(
'/',1,1).trimmed());
693 pginfo->m_inetref = inetref;
696 else if ((system ==
"thetvdb.com") && (
m_tvGrabberPath.endsWith(QString(
"/ttvdb4.py"))))
699 QString inetrefRaw(xml.readElementText(QXmlStreamReader::SkipChildElements));
700 if (inetrefRaw.startsWith(QString(
"series/")))
702 QString inetref(QString (
"ttvdb4.py_") + inetrefRaw.section(
'/',1,1).trimmed());
703 pginfo->m_inetref = inetref;
707 else if (system ==
"schedulesdirect.org")
709 QString details(xml.readElementText(QXmlStreamReader::SkipChildElements));
710 if (details.startsWith(QString(
"originalAirDate/")))
712 QString value(details.section(
'/', 1, 1).trimmed());
715 pginfo->m_originalairdate = datetime.date();
717 else if (details.startsWith(QString(
"newEpisode/")))
719 QString value(details.section(
'/', 1, 1).trimmed());
720 if (value == QString(
"true"))
722 pginfo->m_previouslyshown =
false;
724 else if (value == QString(
"false"))
726 pginfo->m_previouslyshown =
true;
732 while (! (xml.isEndElement() && xml.name() == QString(
"programme")));
740 if (programid.isEmpty())
752 QString seriesid = QString::number(
ELFHash(pginfo->m_title.toUtf8()));
753 pginfo->m_seriesId = seriesid;
754 programid.append(seriesid);
756 if (!episode.isEmpty() && !season.isEmpty())
762 int season_int = season.toInt();
772 programid.append(episode);
773 programid.append(QString::number(season_int, 36));
774 if (pginfo->m_partnumber && pginfo->m_parttotal)
776 programid += QString::number(pginfo->m_partnumber);
777 programid += QString::number(pginfo->m_parttotal);
789 pginfo->m_programId = programid;
790 if (!(pginfo->m_starttime.isValid()))
792 LOG(VB_GENERAL, LOG_WARNING, QString(
"Invalid programme (%1), " "invalid start time, " "skipping").arg(pginfo->m_title));
794 else if (pginfo->m_channel.isEmpty())
796 LOG(VB_GENERAL, LOG_WARNING, QString(
"Invalid programme (%1), " "missing channel, " "skipping").arg(pginfo->m_title));
798 else if (pginfo->m_startts == pginfo->m_endts)
800 LOG(VB_GENERAL, LOG_WARNING, QString(
"Invalid programme (%1), " "identical start and end " "times, skipping").arg(pginfo->m_title));
805 if (pginfo->m_clumpidx.isEmpty())
806 (*proglist)[pginfo->m_channel].push_back(*pginfo);
810 if (pginfo->m_clumpidx.toInt() == 0)
812 aggregatedTitle.clear();
813 aggregatedDesc.clear();
815 if (!pginfo->m_title.isEmpty())
817 if (!aggregatedTitle.isEmpty())
818 aggregatedTitle.append(
" | ");
819 aggregatedTitle.append(pginfo->m_title);
821 if (!pginfo->m_description.isEmpty())
823 if (!aggregatedDesc.isEmpty())
824 aggregatedDesc.append(
" | ");
825 aggregatedDesc.append(pginfo->m_description);
827 if (pginfo->m_clumpidx.toInt() == pginfo->m_clumpmax.toInt() - 1)
829 pginfo->m_title = aggregatedTitle;
830 pginfo->m_description = aggregatedDesc;
831 (*proglist)[pginfo->m_channel].push_back(*pginfo);
839 if (! (xml.isEndElement() && xml.name() == QString(
"tv")))
841 LOG(VB_GENERAL, LOG_ERR, QString(
"Malformed XML file, missing </tv> element, at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString()));