MythTV master
xmltvparser.cpp
Go to the documentation of this file.
1#include "xmltvparser.h"
2
3// C++ headers
4#include <cstdlib>
5#include <iostream>
6
7// Qt headers
8#include <QDateTime>
9#include <QDomDocument>
10#include <QFile>
11#include <QFileInfo>
12#include <QStringList>
13#include <QUrl>
14#include <QXmlStreamReader>
15
16// MythTV headers
25
26// filldata headers
27#include "channeldata.h"
28#include "fillutil.h"
29
31{
32 m_currentYear = MythDate::current().date().toString("yyyy").toUInt();
33}
34
35static uint ELFHash(const QByteArray &ba)
36{
37 const auto *k = (const uchar *)ba.data();
38 uint h = 0;
39
40 if (k)
41 {
42 while (*k)
43 {
44 h = (h << 4) + *k++;
45 uint g = (h & 0xf0000000);
46 if (g != 0)
47 h ^= g >> 24;
48 h &= ~g;
49 }
50 }
51
52 return h;
53}
54
55static void fromXMLTVDate(QString &timestr, QDateTime &dt)
56{
57 // The XMLTV spec requires dates to either be in UTC/GMT or to specify a
58 // valid timezone. We are sticking to the spec and require all grabbers
59 // to comply.
60
61 if (timestr.isEmpty())
62 {
63 LOG(VB_XMLTV, LOG_ERR, "Found empty Date/Time in XMLTV data, ignoring");
64 return;
65 }
66
67 QStringList split = timestr.split(" ", Qt::SkipEmptyParts);
68 QString ts = split[0];
69 QDate tmpDate;
70 QTime tmpTime;
71 QString tzoffset;
72
73 // Process the TZ offset (if any)
74 if (split.size() > 1)
75 {
76 tzoffset = split[1];
77 // These shouldn't be required and they aren't ISO 8601 but the
78 // xmltv spec mentions these and just these so handle them just in
79 // case
80 if (tzoffset == "GMT" || tzoffset == "UTC")
81 tzoffset = "+0000";
82 else if (tzoffset == "BST")
83 tzoffset = "+0100";
84 }
85 else
86 {
87 // We will accept a datetime with a trailing Z as being explicit
88 if (ts.endsWith('Z'))
89 {
90 tzoffset = "+0000";
91 ts.truncate(ts.length()-1);
92 }
93 else
94 {
95 tzoffset = "+0000";
96 static bool s_warnedOnceOnImplicitUtc = false;
97 if (!s_warnedOnceOnImplicitUtc)
98 {
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 "
102 "time instead.");
103 s_warnedOnceOnImplicitUtc = true;
104 }
105 }
106 }
107
108 // Process the date part
109 QString tsDate = ts.left(8);
110 if (tsDate.length() == 8)
111 tmpDate = QDate::fromString(tsDate, "yyyyMMdd");
112 else if (tsDate.length() == 6)
113 tmpDate = QDate::fromString(tsDate, "yyyyMM");
114 else if (tsDate.length() == 4)
115 tmpDate = QDate::fromString(tsDate, "yyyy");
116 if (!tmpDate.isValid())
117 {
118 LOG(VB_XMLTV, LOG_ERR,
119 QString("Invalid datetime (date) in XMLTV data, ignoring: %1")
120 .arg(timestr));
121 return;
122 }
123
124 // Process the time part (if any)
125 if (ts.length() > 8)
126 {
127 QString tsTime = ts.mid(8);
128 if (tsTime.length() == 6)
129 {
130 if (tsTime == "235960")
131 tsTime = "235959";
132 tmpTime = QTime::fromString(tsTime, "HHmmss");
133 }
134 else if (tsTime.length() == 4)
135 {
136 tmpTime = QTime::fromString(tsTime, "HHmm");
137 }
138 else if (tsTime.length() == 2)
139 {
140 tmpTime = QTime::fromString(tsTime, "HH");
141 }
142 if (!tmpTime.isValid())
143 {
144 // Time part exists, but is (somehow) invalid
145 LOG(VB_XMLTV, LOG_ERR,
146 QString("Invalid datetime (time) in XMLTV data, ignoring: %1")
147 .arg(timestr));
148 return;
149 }
150 }
151
152#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
153 QDateTime tmpDT = QDateTime(tmpDate, tmpTime, Qt::UTC);
154#else
155 QDateTime tmpDT = QDateTime(tmpDate, tmpTime, QTimeZone(QTimeZone::UTC));
156#endif
157 if (!tmpDT.isValid())
158 {
159 LOG(VB_XMLTV, LOG_ERR,
160 QString("Invalid datetime (combination of date/time) "
161 "in XMLTV data, ignoring: %1").arg(timestr));
162 return;
163 }
164
165 // While this seems like a hack, it's better than what was done before
166 QString isoDateString = tmpDT.toString(Qt::ISODate);
167 if (isoDateString.endsWith('Z')) // Should always be Z, but ...
168 isoDateString.truncate(isoDateString.length()-1);
169 isoDateString += tzoffset;
170 dt = QDateTime::fromString(isoDateString, Qt::ISODate).toUTC();
171
172 if (!dt.isValid())
173 {
174 LOG(VB_XMLTV, LOG_ERR,
175 QString("Invalid datetime (zone offset) in XMLTV data, "
176 "ignoring: %1").arg(timestr));
177 return;
178 }
179
181}
182
183static bool readNextWithErrorCheck(QXmlStreamReader &xml)
184{
185 xml.readNext();
186 if (xml.hasError())
187 {
188 LOG(VB_GENERAL, LOG_ERR, QString("Malformed XML file at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString()));
189 return false;
190 }
191 return true;
192}
193
195 const QString& filename, ChannelInfoList *chanlist,
196 QMap<QString, QList<ProgInfo> > *proglist)
197{
200 QFile f;
201 if (!dash_open(f, filename, QIODevice::ReadOnly))
202 {
203 LOG(VB_GENERAL, LOG_ERR,
204 QString("Error unable to open '%1' for reading.") .arg(filename));
205 return false;
206 }
207
208 if (filename != "-")
209 {
210 QFileInfo info(f);
211 if (info.size() == 0)
212 {
213 LOG(VB_GENERAL, LOG_WARNING,
214 QString("File %1 exists but is empty. Did the grabber fail?").arg(filename));
215 f.close();
216 return false;
217 }
218 }
219
220 QXmlStreamReader xml(&f);
221 QUrl baseUrl;
222// QUrl sourceUrl;
223 QString aggregatedTitle;
224 QString aggregatedDesc;
225 bool haveReadTV = false;
226 while (!xml.atEnd() && !xml.hasError() && (! (xml.isEndElement() && xml.name() == QString("tv"))))
227 {
228#if 0
229 if (xml.isDTD())
230 {
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();
237
238 QString msg = QString("DTD %1 name %2 PublicId %3 SystemId %4")
239 .arg(text).arg(name).arg(publicId).arg(systemId);
240
241 if (!entities.isEmpty())
242 {
243 msg += " Entities";
244 for (const auto entity : entities)
245 msg += QString(":name %1 PublicId %2 SystemId %3 ")
246 .arg(entity.name())
247 .arg(entity.publicId())
248 .arg(entity.systemId());
249 }
250
251 if (!notations.isEmpty())
252 {
253 msg += " Notations";
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());
259 }
260
261 LOG(VB_XMLTV, LOG_INFO, msg);
262 }
263#endif
264
265 if (xml.readNextStartElement())
266 {
267 if (xml.name() == QString("tv"))
268 {
269// sourceUrl = QUrl(xml.attributes().value("source-info-url").toString());
270 baseUrl = QUrl(xml.attributes().value("source-data-url").toString());
271 haveReadTV = true;
272 }
273 if (xml.name() == QString("channel"))
274 {
275 if (!haveReadTV)
276 {
277 LOG(VB_GENERAL, LOG_ERR, QString("Malformed XML file, no <tv> element found, at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString()));
278 return false;
279 }
280
281 //get id attribute
282 QString xmltvid;
283 xmltvid = xml.attributes().value( "id").toString();
284 auto *chaninfo = new ChannelInfo;
285 chaninfo->m_xmltvId = xmltvid;
286 chaninfo->m_tvFormat = "Default";
287
288 //readNextStartElement says it reads for the next start element WITHIN the current element; but it doesnt; so we use readNext()
289 while (!xml.isEndElement() || (xml.name() != QString("channel")))
290 {
291 if (!readNextWithErrorCheck(xml))
292 {
293 delete chaninfo;
294 return false;
295 }
296 if (xml.name() == QString("icon"))
297 {
298 if (chaninfo->m_icon.isEmpty())
299 {
300 QString path = xml.attributes().value("src").toString();
301 if (!path.isEmpty() && !path.contains("://"))
302 {
303 QString base = baseUrl.toString(QUrl::StripTrailingSlash);
304 chaninfo->m_icon = base +
305 ((path.startsWith("/")) ? path : QString("/") + path);
306 }
307 else if (!path.isEmpty())
308 {
309 QUrl url(path);
310 if (url.isValid())
311 chaninfo->m_icon = url.toString();
312 }
313 }
314 }
315 else if (xml.name() == QString("display-name"))
316 {
317 //now get text
318 QString text;
319 text = xml.readElementText(QXmlStreamReader::SkipChildElements);
320 if (!text.isEmpty())
321 {
322 if (chaninfo->m_name.isEmpty())
323 {
324 chaninfo->m_name = text;
325 }
326 else if (chaninfo->m_callSign.isEmpty())
327 {
328 chaninfo->m_callSign = text;
329 }
330 else if (chaninfo->m_chanNum.isEmpty())
331 {
332 chaninfo->m_chanNum = text;
333 }
334 }
335 }
336 }
337 chaninfo->m_freqId = chaninfo->m_chanNum;
338 //TODO optimize this, no use to do al this parsing if xmltvid is empty; but make sure you will read until the next channel!!
339 if (!chaninfo->m_xmltvId.isEmpty())
340 chanlist->push_back(*chaninfo);
341 delete chaninfo;
342 }//channel
343 else if (xml.name() == QString("programme"))
344 {
345 if (!haveReadTV)
346 {
347 LOG(VB_GENERAL, LOG_ERR, QString("Malformed XML file, no <tv> element found, at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString()));
348 return false;
349 }
350
351 QString programid;
352 QString season;
353 QString episode;
354 QString totalepisodes;
355 auto *pginfo = new ProgInfo();
356
357 QString text = xml.attributes().value("start").toString();
358 fromXMLTVDate(text, pginfo->m_starttime);
359 pginfo->m_startts = text;
360
361 text = xml.attributes().value("stop").toString();
362 //not a mandatory attribute according to XMLTV DTD https://github.com/XMLTV/xmltv/blob/master/xmltv.dtd
363 fromXMLTVDate(text, pginfo->m_endtime);
364 pginfo->m_endts = text;
365
366 text = xml.attributes().value("channel").toString();
367 QStringList split = text.split(" ");
368 pginfo->m_channel = split[0];
369
370 text = xml.attributes().value("clumpidx").toString();
371 if (!text.isEmpty())
372 {
373 split = text.split('/');
374 pginfo->m_clumpidx = split[0];
375 pginfo->m_clumpmax = split[1];
376 }
377
378 while (!xml.isEndElement() || (xml.name() != QString("programme")))
379 {
380 if (!readNextWithErrorCheck(xml))
381 {
382 delete pginfo;
383 return false;
384 }
385 if (xml.name() == QString("title"))
386 {
387 QString text2=xml.readElementText(QXmlStreamReader::SkipChildElements);
388 if (xml.attributes().value("lang").toString() == "ja_JP")
389 { // NOLINT(bugprone-branch-clone)
390 pginfo->m_title = text2;
391 }
392 else if (xml.attributes().value("lang").toString() == "ja_JP@kana")
393 {
394 pginfo->m_title_pronounce = text2;
395 }
396 else if (pginfo->m_title.isEmpty())
397 {
398 pginfo->m_title = text2;
399 }
400 }
401 else if (xml.name() == QString("sub-title") && pginfo->m_subtitle.isEmpty())
402 {
403 pginfo->m_subtitle = xml.readElementText(QXmlStreamReader::SkipChildElements);
404 }
405 else if (xml.name() == QString("subtitles"))
406 {
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;
413 }
414 else if (xml.name() == QString("desc") && pginfo->m_description.isEmpty())
415 {
416 pginfo->m_description = xml.readElementText(QXmlStreamReader::SkipChildElements);
417 }
418 else if (xml.name() == QString("category"))
419 {
420 const QString cat = xml.readElementText(QXmlStreamReader::SkipChildElements);
421
423 {
424 pginfo->m_categoryType = string_to_myth_category_type(cat);
425 }
426 else if (pginfo->m_category.isEmpty())
427 {
428 pginfo->m_category = cat;
429 }
430 if ((cat.compare(QObject::tr("movie"),Qt::CaseInsensitive) == 0) || (cat.compare(QObject::tr("film"),Qt::CaseInsensitive) == 0))
431 {
432 // Hack for tv_grab_uk_rt
433 pginfo->m_categoryType = ProgramInfo::kCategoryMovie;
434 }
435 pginfo->m_genres.append(cat);
436 }
437 else if (xml.name() == QString("date") && (pginfo->m_airdate == 0U))
438 {
439 // Movie production year
440 QString date = xml.readElementText(QXmlStreamReader::SkipChildElements);
441#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
442 pginfo->m_airdate = date.leftRef(4).toUInt();
443#else
444 pginfo->m_airdate = QStringView(date).left(4).toUInt();
445#endif
446 }
447 else if (xml.name() == QString("star-rating"))
448 {
449 QString stars;
450 float rating = 0.0;
451
452 // Use the first rating to appear in the xml, this should be
453 // the most important one.
454 //
455 // Averaging is not a good idea here, any subsequent ratings
456 // are likely to represent that days recommended programmes
457 // which on a bad night could given to an average programme.
458 // In the case of uk_rt it's not unknown for a recommendation
459 // to be given to programmes which are 'so bad, you have to
460 // watch!'
461 //
462 // XMLTV uses zero based ratings and signals no rating by absence.
463 // A rating from 1 to 5 is encoded as 0/4 to 4/4.
464 // MythTV uses zero to signal no rating!
465 // The same rating is encoded as 0.2 to 1.0 with steps of 0.2, it
466 // is not encoded as 0.0 to 1.0 with steps of 0.25 because
467 // 0 signals no rating!
468 // See http://xmltv.cvs.sourceforge.net/viewvc/xmltv/xmltv/xmltv.dtd?revision=1.47&view=markup#l539
469 stars = "0"; //no rating
470 while (!xml.isEndElement() || (xml.name() != QString("star-rating")))
471 {
472 if (!readNextWithErrorCheck(xml))
473 return false;
474 if (xml.isStartElement())
475 {
476 if (xml.name() == QString("value"))
477 {
478 stars=xml.readElementText(QXmlStreamReader::SkipChildElements);
479 }
480 }
481 }
482 if (pginfo->m_stars == 0.0F)
483 {
484 float num = stars.section('/', 0, 0).toFloat() + 1;
485 float den = stars.section('/', 1, 1).toFloat() + 1;
486 if (0.0F < den)
487 rating = num/den;
488 }
489 pginfo->m_stars = rating;
490 }
491 else if (xml.name() == QString("rating"))
492 {
493 // again, the structure of ratings seems poorly represented
494 // in the XML. no idea what we'd do with multiple values.
495 QString rat;
496 QString rating_system = xml.attributes().value("system").toString();
497 if (rating_system == nullptr)
498 rating_system = "";
499
500 while (!xml.isEndElement() || (xml.name() != QString("rating")))
501 {
502 if (!readNextWithErrorCheck(xml))
503 return false;
504 if (xml.isStartElement())
505 {
506 if (xml.name() == QString("value"))
507 {
508 rat=xml.readElementText(QXmlStreamReader::SkipChildElements);
509 }
510 }
511 }
512
513 if (!rat.isEmpty())
514 {
516 rating.m_system = rating_system;
517 rating.m_rating = rat;
518 pginfo->m_ratings.append(rating);
519 }
520 }
521 else if (xml.name() == QString("previously-shown"))
522 {
523 pginfo->m_previouslyshown = true;
524 QString prevdate = xml.attributes().value( "start").toString();
525 if ((!prevdate.isEmpty()) && (pginfo->m_originalairdate.isNull()))
526 {
527 QDateTime date;
528 fromXMLTVDate(prevdate, date);
529 pginfo->m_originalairdate = date.date();
530 }
531 }
532 else if (xml.name() == QString("credits"))
533 {
534 int priority = 1;
535 while (!xml.isEndElement() || (xml.name() != QString("credits")))
536 {
537 if (!readNextWithErrorCheck(xml))
538 return false;
539 if (xml.isStartElement())
540 {
541 // Character role in optional role attribute
542 QString character = xml.attributes()
543 .value("role").toString();
544 QString tagname = xml.name().toString();
545 if (tagname == "actor")
546 {
547 QString guest = xml.attributes()
548 .value("guest")
549 .toString();
550 if (guest == "yes")
551 tagname = "guest_star";
552 }
553 QString name = xml.readElementText(QXmlStreamReader::SkipChildElements);
554 QStringList characters = character.split("/", Qt::SkipEmptyParts);
555 if (characters.isEmpty())
556 {
557 pginfo->AddPerson(tagname, name,
558 priority, character);
559 ++priority;
560 }
561 else
562 {
563 for (auto & c : characters)
564 {
565 pginfo->AddPerson(tagname, name,
566 priority,
567 c.simplified());
568 ++priority;
569 }
570 }
571 }
572 }
573 }
574 else if (xml.name() == QString("audio"))
575 {
576 while (!xml.isEndElement() || (xml.name() != QString("audio")))
577 {
578 if (!readNextWithErrorCheck(xml))
579 return false;
580 if (xml.isStartElement())
581 {
582 if (xml.name() == QString("stereo"))
583 {
584 QString text2=xml.readElementText(QXmlStreamReader::SkipChildElements);
585 if (text2 == "mono")
586 {
587 pginfo->m_audioProps |= AUD_MONO;
588 }
589 else if (text2 == "stereo")
590 {
591 pginfo->m_audioProps |= AUD_STEREO;
592 }
593 else if (text2 == "dolby" || text2 == "dolby digital")
594 {
595 pginfo->m_audioProps |= AUD_DOLBY;
596 }
597 else if (text2 == "surround")
598 {
599 pginfo->m_audioProps |= AUD_SURROUND;
600 }
601 }
602 }
603 }
604 }
605 else if (xml.name() == QString("video"))
606 {
607 while (!xml.isEndElement() || (xml.name() != QString("video")))
608 {
609 if (!readNextWithErrorCheck(xml))
610 return false;
611 if (xml.isStartElement())
612 {
613 if (xml.name() == QString("quality"))
614 {
615 if (xml.readElementText(QXmlStreamReader::SkipChildElements) == "HDTV")
616 pginfo->m_videoProps |= VID_HDTV;
617 }
618 else if (xml.name() == QString("aspect"))
619 {
620 if (xml.readElementText(QXmlStreamReader::SkipChildElements) == "16:9")
621 pginfo->m_videoProps |= VID_WIDESCREEN;
622 }
623 }
624 }
625 }
626 else if (xml.name() == QString("episode-num"))
627 {
628 QString system = xml.attributes().value( "system").toString();
629 if (system == "dd_progid")
630 {
631 QString episodenum(xml.readElementText(QXmlStreamReader::SkipChildElements));
632 // if this field includes a dot, strip it out
633 int idx = episodenum.indexOf('.');
634 if (idx != -1)
635 episodenum.remove(idx, 1);
636 programid = episodenum;
637 // Only EPisodes and SHows are part of a series for SD
638 if (programid.startsWith(QString("EP")) ||
639 programid.startsWith(QString("SH")))
640 pginfo->m_seriesId = QString("EP") + programid.mid(2,8);
641 }
642 else if (system == "xmltv_ns")
643 {
644 QString episodenum(xml.readElementText(QXmlStreamReader::SkipChildElements));
645 episode = episodenum.section('.',1,1);
646 totalepisodes = episode.section('/',1,1).trimmed();
647 episode = episode.section('/',0,0).trimmed();
648 season = episodenum.section('.',0,0).trimmed();
649 season = season.section('/',0,0).trimmed();
650 QString part(episodenum.section('.',2,2));
651 QString partnumber(part.section('/',0,0).trimmed());
652 QString parttotal(part.section('/',1,1).trimmed());
653 pginfo->m_categoryType = ProgramInfo::kCategorySeries;
654 if (!season.isEmpty())
655 {
656 int tmp = season.toUInt() + 1;
657 pginfo->m_season = tmp;
658 season = QString::number(tmp);
659 pginfo->m_syndicatedepisodenumber = 'S' + season;
660 }
661 if (!episode.isEmpty())
662 {
663 int tmp = episode.toUInt() + 1;
664 pginfo->m_episode = tmp;
665 episode = QString::number(tmp);
666 pginfo->m_syndicatedepisodenumber.append('E' + episode);
667 }
668 if (!totalepisodes.isEmpty())
669 {
670 pginfo->m_totalepisodes = totalepisodes.toUInt();
671 }
672 uint partno = 0;
673 if (!partnumber.isEmpty())
674 {
675 bool ok = false;
676 partno = partnumber.toUInt(&ok) + 1;
677 partno = ok ? partno : 0;
678 }
679 if (!parttotal.isEmpty() && partno > 0)
680 {
681 bool ok = false;
682 uint partto = parttotal.toUInt(&ok);
683 if (ok && partnumber <= parttotal)
684 {
685 pginfo->m_parttotal = partto;
686 pginfo->m_partnumber = partno;
687 }
688 }
689 }
690 else if (system == "onscreen")
691 {
692 pginfo->m_categoryType = ProgramInfo::kCategorySeries;
693 if (pginfo->m_subtitle.isEmpty())
694 {
695 pginfo->m_subtitle = xml.readElementText(QXmlStreamReader::SkipChildElements);
696 }
697 }
698 else if ((system == "themoviedb.org") && (m_movieGrabberPath.endsWith(QString("/tmdb3.py"))))
699 {
700 // text is movie/<inetref>
701 QString inetrefRaw(xml.readElementText(QXmlStreamReader::SkipChildElements));
702 if (inetrefRaw.startsWith(QString("movie/")))
703 {
704 QString inetref(QString ("tmdb3.py_") + inetrefRaw.section('/',1,1).trimmed());
705 pginfo->m_inetref = inetref;
706 }
707 }
708 else if ((system == "thetvdb.com") && (m_tvGrabberPath.endsWith(QString("/ttvdb4.py"))))
709 {
710 // text is series/<inetref>
711 QString inetrefRaw(xml.readElementText(QXmlStreamReader::SkipChildElements));
712 if (inetrefRaw.startsWith(QString("series/")))
713 {
714 QString inetref(QString ("ttvdb4.py_") + inetrefRaw.section('/',1,1).trimmed());
715 pginfo->m_inetref = inetref;
716 // ProgInfo does not have a collectionref, so we don't set any
717 }
718 }
719 else if (system == "schedulesdirect.org")
720 {
721 QString details(xml.readElementText(QXmlStreamReader::SkipChildElements));
722 if (details.startsWith(QString("originalAirDate/")))
723 {
724 QString value(details.section('/', 1, 1).trimmed());
725 QDateTime datetime;
726 fromXMLTVDate(value, datetime);
727 pginfo->m_originalairdate = datetime.date();
728 }
729 else if (details.startsWith(QString("newEpisode/")))
730 {
731 QString value(details.section('/', 1, 1).trimmed());
732 if (value == QString("true"))
733 {
734 pginfo->m_previouslyshown = false;
735 }
736 else if (value == QString("false"))
737 {
738 pginfo->m_previouslyshown = true;
739 }
740 }
741 }
742 }//episode-num
743 }
744
745 if (pginfo->m_category.isEmpty() && pginfo->m_categoryType != ProgramInfo::kCategoryNone)
746 pginfo->m_category = myth_category_type_to_string(pginfo->m_categoryType);
747
748 if (!pginfo->m_airdate && ProgramInfo::kCategorySeries != pginfo->m_categoryType)
749 pginfo->m_airdate = m_currentYear;
750
751 if (programid.isEmpty())
752 {
753 //Let's build ourself a programid
754 if (ProgramInfo::kCategoryMovie == pginfo->m_categoryType)
755 programid = "MV";
756 else if (ProgramInfo::kCategorySeries == pginfo->m_categoryType)
757 programid = "EP";
758 else if (ProgramInfo::kCategorySports == pginfo->m_categoryType)
759 programid = "SP";
760 else
761 programid = "SH";
762
763 QString seriesid = QString::number(ELFHash(pginfo->m_title.toUtf8()));
764 pginfo->m_seriesId = seriesid;
765 programid.append(seriesid);
766
767 if (!episode.isEmpty() && !season.isEmpty())
768 {
769 /* Append unpadded episode and season number to the seriesid (to
770 maintain consistency with historical encoding), but limit the
771 season number representation to a single base-36 character to
772 ensure unique programid generation. */
773 int season_int = season.toInt();
774 if (season_int > 35)
775 {
776 // Cannot represent season as a single base-36 character, so
777 // remove the programid and fall back to normal dup matching.
778 if (ProgramInfo::kCategoryMovie != pginfo->m_categoryType)
779 programid.clear();
780 }
781 else
782 {
783 programid.append(episode);
784 programid.append(QString::number(season_int, 36));
785 if (pginfo->m_partnumber && pginfo->m_parttotal)
786 {
787 programid += QString::number(pginfo->m_partnumber);
788 programid += QString::number(pginfo->m_parttotal);
789 }
790 }
791 }
792 else
793 {
794 /* No ep/season info? Well then remove the programid and rely on
795 normal dupchecking methods instead. */
796 if (ProgramInfo::kCategoryMovie != pginfo->m_categoryType)
797 programid.clear();
798 }
799 }
800 pginfo->m_programId = programid;
801 if (!(pginfo->m_starttime.isValid()))
802 {
803 LOG(VB_GENERAL, LOG_WARNING, QString("Invalid programme (%1), " "invalid start time, " "skipping").arg(pginfo->m_title));
804 }
805 else if (pginfo->m_channel.isEmpty())
806 {
807 LOG(VB_GENERAL, LOG_WARNING, QString("Invalid programme (%1), " "missing channel, " "skipping").arg(pginfo->m_title));
808 }
809 else if (pginfo->m_startts == pginfo->m_endts)
810 {
811 LOG(VB_GENERAL, LOG_WARNING, QString("Invalid programme (%1), " "identical start and end " "times, skipping").arg(pginfo->m_title));
812 }
813 else
814 {
815 // so we have a (relatively) clean program element now, which is good enough to process or to store
816 if (pginfo->m_clumpidx.isEmpty())
817 {
818 (*proglist)[pginfo->m_channel].push_back(*pginfo);
819 }
820 else
821 {
822 /* append all titles/descriptions from one clump */
823 if (pginfo->m_clumpidx.toInt() == 0)
824 {
825 aggregatedTitle.clear();
826 aggregatedDesc.clear();
827 }
828 if (!pginfo->m_title.isEmpty())
829 {
830 if (!aggregatedTitle.isEmpty())
831 aggregatedTitle.append(" | ");
832 aggregatedTitle.append(pginfo->m_title);
833 }
834 if (!pginfo->m_description.isEmpty())
835 {
836 if (!aggregatedDesc.isEmpty())
837 aggregatedDesc.append(" | ");
838 aggregatedDesc.append(pginfo->m_description);
839 }
840 if (pginfo->m_clumpidx.toInt() == pginfo->m_clumpmax.toInt() - 1)
841 {
842 pginfo->m_title = aggregatedTitle;
843 pginfo->m_description = aggregatedDesc;
844 (*proglist)[pginfo->m_channel].push_back(*pginfo);
845 }
846 }
847 }
848 delete pginfo;
849 }//if programme
850 }//if readNextStartElement
851 }//while loop
852 if (! (xml.isEndElement() && xml.name() == QString("tv")))
853 {
854 LOG(VB_GENERAL, LOG_ERR, QString("Malformed XML file, missing </tv> element, at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString()));
855 return false;
856 }
857 //TODO add code for adding data on the run
858 f.close();
859
860 return true;
861}
std::vector< ChannelInfo > ChannelInfoList
Definition: channelinfo.h:130
QString m_xmltvId
Definition: channelinfo.h:96
static QString GetTelevisionGrabber()
static QString GetMovieGrabber()
unsigned int m_currentYear
Definition: xmltvparser.h:24
QString m_tvGrabberPath
Definition: xmltvparser.h:26
bool parseFile(const QString &filename, ChannelInfoList *chanlist, QMap< QString, QList< ProgInfo > > *proglist)
QString m_movieGrabberPath
Definition: xmltvparser.h:25
unsigned int uint
Definition: compat.h:60
bool dash_open(QFile &file, const QString &filename, QIODevice::OpenMode m, FILE *handle)
Definition: fillutil.cpp:11
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
QString toString(const QDateTime &raw_dt, uint format)
Returns formatted string representing the time.
Definition: mythdate.cpp:93
@ kFilename
Default UTC, "yyyyMMddhhmmss".
Definition: mythdate.h:19
@ ISODate
Default UTC.
Definition: mythdate.h:18
QDateTime fromString(const QString &dtstr)
Converts kFilename && kISODate formats to QDateTime.
Definition: mythdate.cpp:39
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:15
dictionary info
Definition: azlyrics.py:7
def rating(profile, smoonURL, gate)
Definition: scan.py:36
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 &timestr, QDateTime &dt)
Definition: xmltvparser.cpp:55
static bool readNextWithErrorCheck(QXmlStreamReader &xml)
static uint ELFHash(const QByteArray &ba)
Definition: xmltvparser.cpp:35