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 pginfo->m_airdate = date.left(4).toUInt();
442 }
443 else if (xml.name() == QString("star-rating"))
444 {
445 QString stars;
446 float rating = 0.0;
447
448 // Use the first rating to appear in the xml, this should be
449 // the most important one.
450 //
451 // Averaging is not a good idea here, any subsequent ratings
452 // are likely to represent that days recommended programmes
453 // which on a bad night could given to an average programme.
454 // In the case of uk_rt it's not unknown for a recommendation
455 // to be given to programmes which are 'so bad, you have to
456 // watch!'
457 //
458 // XMLTV uses zero based ratings and signals no rating by absence.
459 // A rating from 1 to 5 is encoded as 0/4 to 4/4.
460 // MythTV uses zero to signal no rating!
461 // The same rating is encoded as 0.2 to 1.0 with steps of 0.2, it
462 // is not encoded as 0.0 to 1.0 with steps of 0.25 because
463 // 0 signals no rating!
464 // See http://xmltv.cvs.sourceforge.net/viewvc/xmltv/xmltv/xmltv.dtd?revision=1.47&view=markup#l539
465 stars = "0"; //no rating
466 while (!xml.isEndElement() || (xml.name() != QString("star-rating")))
467 {
468 if (!readNextWithErrorCheck(xml))
469 return false;
470 if (xml.isStartElement())
471 {
472 if (xml.name() == QString("value"))
473 {
474 stars=xml.readElementText(QXmlStreamReader::SkipChildElements);
475 }
476 }
477 }
478 if (pginfo->m_stars == 0.0F)
479 {
480 float num = stars.section('/', 0, 0).toFloat() + 1;
481 float den = stars.section('/', 1, 1).toFloat() + 1;
482 if (0.0F < den)
483 rating = num/den;
484 }
485 pginfo->m_stars = rating;
486 }
487 else if (xml.name() == QString("rating"))
488 {
489 // again, the structure of ratings seems poorly represented
490 // in the XML. no idea what we'd do with multiple values.
491 QString rat;
492 QString rating_system = xml.attributes().value("system").toString();
493 if (rating_system == nullptr)
494 rating_system = "";
495
496 while (!xml.isEndElement() || (xml.name() != QString("rating")))
497 {
498 if (!readNextWithErrorCheck(xml))
499 return false;
500 if (xml.isStartElement())
501 {
502 if (xml.name() == QString("value"))
503 {
504 rat=xml.readElementText(QXmlStreamReader::SkipChildElements);
505 }
506 }
507 }
508
509 if (!rat.isEmpty())
510 {
512 rating.m_system = rating_system;
513 rating.m_rating = rat;
514 pginfo->m_ratings.append(rating);
515 }
516 }
517 else if (xml.name() == QString("previously-shown"))
518 {
519 pginfo->m_previouslyshown = true;
520 QString prevdate = xml.attributes().value( "start").toString();
521 if ((!prevdate.isEmpty()) && (pginfo->m_originalairdate.isNull()))
522 {
523 QDateTime date;
524 fromXMLTVDate(prevdate, date);
525 pginfo->m_originalairdate = date.date();
526 }
527 }
528 else if (xml.name() == QString("credits"))
529 {
530 int priority = 1;
531 while (!xml.isEndElement() || (xml.name() != QString("credits")))
532 {
533 if (!readNextWithErrorCheck(xml))
534 return false;
535 if (xml.isStartElement())
536 {
537 // Character role in optional role attribute
538 QString character = xml.attributes()
539 .value("role").toString();
540 QString tagname = xml.name().toString();
541 if (tagname == "actor")
542 {
543 QString guest = xml.attributes()
544 .value("guest")
545 .toString();
546 if (guest == "yes")
547 tagname = "guest_star";
548 }
549 QString name = xml.readElementText(QXmlStreamReader::SkipChildElements);
550 QStringList characters = character.split("/", Qt::SkipEmptyParts);
551 if (characters.isEmpty())
552 {
553 pginfo->AddPerson(tagname, name,
554 priority, character);
555 ++priority;
556 }
557 else
558 {
559 for (auto & c : characters)
560 {
561 pginfo->AddPerson(tagname, name,
562 priority,
563 c.simplified());
564 ++priority;
565 }
566 }
567 }
568 }
569 }
570 else if (xml.name() == QString("audio"))
571 {
572 while (!xml.isEndElement() || (xml.name() != QString("audio")))
573 {
574 if (!readNextWithErrorCheck(xml))
575 return false;
576 if (xml.isStartElement())
577 {
578 if (xml.name() == QString("stereo"))
579 {
580 QString text2=xml.readElementText(QXmlStreamReader::SkipChildElements);
581 if (text2 == "mono")
582 {
583 pginfo->m_audioProps |= AUD_MONO;
584 }
585 else if (text2 == "stereo")
586 {
587 pginfo->m_audioProps |= AUD_STEREO;
588 }
589 else if (text2 == "dolby" || text2 == "dolby digital")
590 {
591 pginfo->m_audioProps |= AUD_DOLBY;
592 }
593 else if (text2 == "surround")
594 {
595 pginfo->m_audioProps |= AUD_SURROUND;
596 }
597 }
598 }
599 }
600 }
601 else if (xml.name() == QString("video"))
602 {
603 while (!xml.isEndElement() || (xml.name() != QString("video")))
604 {
605 if (!readNextWithErrorCheck(xml))
606 return false;
607 if (xml.isStartElement())
608 {
609 if (xml.name() == QString("quality"))
610 {
611 if (xml.readElementText(QXmlStreamReader::SkipChildElements) == "HDTV")
612 pginfo->m_videoProps |= VID_HDTV;
613 }
614 else if (xml.name() == QString("aspect"))
615 {
616 if (xml.readElementText(QXmlStreamReader::SkipChildElements) == "16:9")
617 pginfo->m_videoProps |= VID_WIDESCREEN;
618 }
619 }
620 }
621 }
622 else if (xml.name() == QString("episode-num"))
623 {
624 QString system = xml.attributes().value( "system").toString();
625 if (system == "dd_progid")
626 {
627 QString episodenum(xml.readElementText(QXmlStreamReader::SkipChildElements));
628 // if this field includes a dot, strip it out
629 int idx = episodenum.indexOf('.');
630 if (idx != -1)
631 episodenum.remove(idx, 1);
632 programid = episodenum;
633 // Only EPisodes and SHows are part of a series for SD
634 if (programid.startsWith(QString("EP")) ||
635 programid.startsWith(QString("SH")))
636 pginfo->m_seriesId = QString("EP") + programid.mid(2,8);
637 }
638 else if (system == "xmltv_ns")
639 {
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());
649 pginfo->m_categoryType = ProgramInfo::kCategorySeries;
650 if (!season.isEmpty())
651 {
652 int tmp = season.toUInt() + 1;
653 pginfo->m_season = tmp;
654 season = QString::number(tmp);
655 pginfo->m_syndicatedepisodenumber = 'S' + season;
656 }
657 if (!episode.isEmpty())
658 {
659 int tmp = episode.toUInt() + 1;
660 pginfo->m_episode = tmp;
661 episode = QString::number(tmp);
662 pginfo->m_syndicatedepisodenumber.append('E' + episode);
663 }
664 if (!totalepisodes.isEmpty())
665 {
666 pginfo->m_totalepisodes = totalepisodes.toUInt();
667 }
668 uint partno = 0;
669 if (!partnumber.isEmpty())
670 {
671 bool ok = false;
672 partno = partnumber.toUInt(&ok) + 1;
673 partno = (ok) ? partno : 0;
674 }
675 if (!parttotal.isEmpty() && partno > 0)
676 {
677 bool ok = false;
678 uint partto = parttotal.toUInt(&ok);
679 if (ok && partnumber <= parttotal)
680 {
681 pginfo->m_parttotal = partto;
682 pginfo->m_partnumber = partno;
683 }
684 }
685 }
686 else if (system == "onscreen")
687 {
688 pginfo->m_categoryType = ProgramInfo::kCategorySeries;
689 if (pginfo->m_subtitle.isEmpty())
690 {
691 pginfo->m_subtitle = xml.readElementText(QXmlStreamReader::SkipChildElements);
692 }
693 }
694 else if ((system == "themoviedb.org") && (m_movieGrabberPath.endsWith(QString("/tmdb3.py"))))
695 {
696 // text is movie/<inetref>
697 QString inetrefRaw(xml.readElementText(QXmlStreamReader::SkipChildElements));
698 if (inetrefRaw.startsWith(QString("movie/")))
699 {
700 QString inetref(QString ("tmdb3.py_") + inetrefRaw.section('/',1,1).trimmed());
701 pginfo->m_inetref = inetref;
702 }
703 }
704 else if ((system == "thetvdb.com") && (m_tvGrabberPath.endsWith(QString("/ttvdb4.py"))))
705 {
706 // text is series/<inetref>
707 QString inetrefRaw(xml.readElementText(QXmlStreamReader::SkipChildElements));
708 if (inetrefRaw.startsWith(QString("series/")))
709 {
710 QString inetref(QString ("ttvdb4.py_") + inetrefRaw.section('/',1,1).trimmed());
711 pginfo->m_inetref = inetref;
712 // ProgInfo does not have a collectionref, so we don't set any
713 }
714 }
715 else if (system == "schedulesdirect.org")
716 {
717 QString details(xml.readElementText(QXmlStreamReader::SkipChildElements));
718 if (details.startsWith(QString("originalAirDate/")))
719 {
720 QString value(details.section('/', 1, 1).trimmed());
721 QDateTime datetime;
722 fromXMLTVDate(value, datetime);
723 pginfo->m_originalairdate = datetime.date();
724 }
725 else if (details.startsWith(QString("newEpisode/")))
726 {
727 QString value(details.section('/', 1, 1).trimmed());
728 if (value == QString("true"))
729 {
730 pginfo->m_previouslyshown = false;
731 }
732 else if (value == QString("false"))
733 {
734 pginfo->m_previouslyshown = true;
735 }
736 }
737 }
738 }//episode-num
739 }
740
741 if (pginfo->m_category.isEmpty() && pginfo->m_categoryType != ProgramInfo::kCategoryNone)
742 pginfo->m_category = myth_category_type_to_string(pginfo->m_categoryType);
743
744 if (!pginfo->m_airdate && ProgramInfo::kCategorySeries != pginfo->m_categoryType)
745 pginfo->m_airdate = m_currentYear;
746
747 if (programid.isEmpty())
748 {
749 //Let's build ourself a programid
750 if (ProgramInfo::kCategoryMovie == pginfo->m_categoryType)
751 programid = "MV";
752 else if (ProgramInfo::kCategorySeries == pginfo->m_categoryType)
753 programid = "EP";
754 else if (ProgramInfo::kCategorySports == pginfo->m_categoryType)
755 programid = "SP";
756 else
757 programid = "SH";
758
759 QString seriesid = QString::number(ELFHash(pginfo->m_title.toUtf8()));
760 pginfo->m_seriesId = seriesid;
761 programid.append(seriesid);
762
763 if (!episode.isEmpty() && !season.isEmpty())
764 {
765 /* Append unpadded episode and season number to the seriesid (to
766 maintain consistency with historical encoding), but limit the
767 season number representation to a single base-36 character to
768 ensure unique programid generation. */
769 int season_int = season.toInt();
770 if (season_int > 35)
771 {
772 // Cannot represent season as a single base-36 character, so
773 // remove the programid and fall back to normal dup matching.
774 if (ProgramInfo::kCategoryMovie != pginfo->m_categoryType)
775 programid.clear();
776 }
777 else
778 {
779 programid.append(episode);
780 programid.append(QString::number(season_int, 36));
781 if (pginfo->m_partnumber && pginfo->m_parttotal)
782 {
783 programid += QString::number(pginfo->m_partnumber);
784 programid += QString::number(pginfo->m_parttotal);
785 }
786 }
787 }
788 else
789 {
790 /* No ep/season info? Well then remove the programid and rely on
791 normal dupchecking methods instead. */
792 if (ProgramInfo::kCategoryMovie != pginfo->m_categoryType)
793 programid.clear();
794 }
795 }
796 pginfo->m_programId = programid;
797 if (!(pginfo->m_starttime.isValid()))
798 {
799 LOG(VB_GENERAL, LOG_WARNING, QString("Invalid programme (%1), " "invalid start time, " "skipping").arg(pginfo->m_title));
800 }
801 else if (pginfo->m_channel.isEmpty())
802 {
803 LOG(VB_GENERAL, LOG_WARNING, QString("Invalid programme (%1), " "missing channel, " "skipping").arg(pginfo->m_title));
804 }
805 else if (pginfo->m_startts == pginfo->m_endts)
806 {
807 LOG(VB_GENERAL, LOG_WARNING, QString("Invalid programme (%1), " "identical start and end " "times, skipping").arg(pginfo->m_title));
808 }
809 else
810 {
811 // so we have a (relatively) clean program element now, which is good enough to process or to store
812 if (pginfo->m_clumpidx.isEmpty())
813 (*proglist)[pginfo->m_channel].push_back(*pginfo);
814 else
815 {
816 /* append all titles/descriptions from one clump */
817 if (pginfo->m_clumpidx.toInt() == 0)
818 {
819 aggregatedTitle.clear();
820 aggregatedDesc.clear();
821 }
822 if (!pginfo->m_title.isEmpty())
823 {
824 if (!aggregatedTitle.isEmpty())
825 aggregatedTitle.append(" | ");
826 aggregatedTitle.append(pginfo->m_title);
827 }
828 if (!pginfo->m_description.isEmpty())
829 {
830 if (!aggregatedDesc.isEmpty())
831 aggregatedDesc.append(" | ");
832 aggregatedDesc.append(pginfo->m_description);
833 }
834 if (pginfo->m_clumpidx.toInt() == pginfo->m_clumpmax.toInt() - 1)
835 {
836 pginfo->m_title = aggregatedTitle;
837 pginfo->m_description = aggregatedDesc;
838 (*proglist)[pginfo->m_channel].push_back(*pginfo);
839 }
840 }
841 }
842 delete pginfo;
843 }//if programme
844 }//if readNextStartElement
845 }//while loop
846 if (! (xml.isEndElement() && xml.name() == QString("tv")))
847 {
848 LOG(VB_GENERAL, LOG_ERR, QString("Malformed XML file, missing </tv> element, at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString()));
849 return false;
850 }
851 //TODO add code for adding data on the run
852 f.close();
853
854 return true;
855}
std::vector< ChannelInfo > ChannelInfoList
Definition: channelinfo.h:131
QString m_xmltvId
Definition: channelinfo.h:97
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
bool dash_open(QFile &file, const QString &filename, int m, FILE *handle)
Definition: fillutil.cpp:11
unsigned int uint
Definition: freesurround.h:24
static guint32 * tmp
Definition: goom_core.cpp:26
#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:18
@ ISODate
Default UTC.
Definition: mythdate.h:17
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