00001 #include "xmltvparser.h"
00002
00003
00004 #include <QFile>
00005 #include <QStringList>
00006 #include <QDateTime>
00007 #include <QDomDocument>
00008 #include <QUrl>
00009
00010
00011 #include <iostream>
00012 #include <cstdlib>
00013
00014
00015 #include "exitcodes.h"
00016 #include "mythcorecontext.h"
00017 #include "mythmiscutil.h"
00018
00019
00020 #include "programinfo.h"
00021 #include "programdata.h"
00022 #include "dvbdescriptors.h"
00023
00024
00025 #include "channeldata.h"
00026 #include "fillutil.h"
00027
00028 XMLTVParser::XMLTVParser() : isJapan(false), current_year(0)
00029 {
00030 current_year = QDate::currentDate().toString("yyyy").toUInt();
00031 }
00032
00033 static uint ELFHash(const QByteArray &ba)
00034 {
00035 const uchar *k = (const uchar *)ba.data();
00036 uint h = 0;
00037 uint g;
00038
00039 if (k)
00040 {
00041 while (*k)
00042 {
00043 h = (h << 4) + *k++;
00044 if ((g = (h & 0xf0000000)) != 0)
00045 h ^= g >> 24;
00046 h &= ~g;
00047 }
00048 }
00049
00050 return h;
00051 }
00052
00053 static QString getFirstText(QDomElement element)
00054 {
00055 for (QDomNode dname = element.firstChild(); !dname.isNull();
00056 dname = dname.nextSibling())
00057 {
00058 QDomText t = dname.toText();
00059 if (!t.isNull())
00060 return t.data();
00061 }
00062 return QString();
00063 }
00064
00065 ChanInfo *XMLTVParser::parseChannel(QDomElement &element, QUrl &baseUrl)
00066 {
00067 ChanInfo *chaninfo = new ChanInfo;
00068
00069 QString xmltvid = element.attribute("id", "");
00070 QStringList split = xmltvid.simplified().split(" ");
00071
00072 chaninfo->xmltvid = xmltvid;
00073 chaninfo->tvformat = "Default";
00074
00075 for (QDomNode child = element.firstChild(); !child.isNull();
00076 child = child.nextSibling())
00077 {
00078 QDomElement info = child.toElement();
00079 if (!info.isNull())
00080 {
00081 if (info.tagName() == "icon")
00082 {
00083 QString path = info.attribute("src", "");
00084 if (!path.isEmpty() && !path.contains("://"))
00085 {
00086 QString base = baseUrl.toString(QUrl::StripTrailingSlash);
00087 chaninfo->iconpath = base +
00088 ((path.left(1) == "/") ? path : QString("/") + path);
00089 }
00090 else if (!path.isEmpty())
00091 {
00092 QUrl url(path);
00093 if (url.isValid())
00094 chaninfo->iconpath = url.toString();
00095 }
00096 }
00097 else if (info.tagName() == "display-name")
00098 {
00099 if (chaninfo->name.isEmpty())
00100 {
00101 chaninfo->name = info.text();
00102 }
00103 else if (isJapan && chaninfo->callsign.isEmpty())
00104 {
00105 chaninfo->callsign = info.text();
00106 }
00107 else if (chaninfo->chanstr.isEmpty())
00108 {
00109 chaninfo->chanstr = info.text();
00110 }
00111 }
00112 }
00113 }
00114
00115 chaninfo->freqid = chaninfo->chanstr;
00116 return chaninfo;
00117 }
00118
00119 static int TimezoneToInt (QString timezone)
00120 {
00121
00122 int result = 841;
00123
00124 if (timezone.toUpper() == "UTC" || timezone.toUpper() == "GMT")
00125 return 0;
00126
00127 if (timezone.length() == 5)
00128 {
00129 bool ok;
00130
00131 result = timezone.mid(1,2).toInt(&ok, 10);
00132
00133 if (!ok)
00134 result = 841;
00135 else
00136 {
00137 result *= 60;
00138
00139 int min = timezone.right(2).toInt(&ok, 10);
00140
00141 if (!ok)
00142 result = 841;
00143 else
00144 {
00145 result += min;
00146 if (timezone.left(1) == "-")
00147 result *= -1;
00148 }
00149 }
00150 }
00151 return result;
00152 }
00153
00154
00155 static void fromXMLTVDate(QString ×tr, QDateTime &dt, int localTimezoneOffset = 841)
00156 {
00157 if (timestr.isEmpty())
00158 {
00159 LOG(VB_XMLTV, LOG_ERR, "Found empty Date/Time in XMLTV data, ignoring");
00160 return;
00161 }
00162
00163 QStringList split = timestr.split(" ");
00164 QString ts = split[0];
00165 bool ok;
00166 int year = 0, month = 0, day = 0, hour = 0, min = 0, sec = 0;
00167
00168 if (ts.length() == 14)
00169 {
00170 year = ts.left(4).toInt(&ok, 10);
00171 month = ts.mid(4,2).toInt(&ok, 10);
00172 day = ts.mid(6,2).toInt(&ok, 10);
00173 hour = ts.mid(8,2).toInt(&ok, 10);
00174 min = ts.mid(10,2).toInt(&ok, 10);
00175 sec = ts.mid(12,2).toInt(&ok, 10);
00176 }
00177 else if (ts.length() == 12)
00178 {
00179 year = ts.left(4).toInt(&ok, 10);
00180 month = ts.mid(4,2).toInt(&ok, 10);
00181 day = ts.mid(6,2).toInt(&ok, 10);
00182 hour = ts.mid(8,2).toInt(&ok, 10);
00183 min = ts.mid(10,2).toInt(&ok, 10);
00184 sec = 0;
00185 }
00186 else
00187 {
00188 LOG(VB_GENERAL, LOG_ERR,
00189 QString("Ignoring unknown timestamp format: %1")
00190 .arg(ts));
00191 return;
00192 }
00193
00194 dt = QDateTime(QDate(year, month, day),QTime(hour, min, sec));
00195
00196 if ((split.size() > 1) && (localTimezoneOffset <= 840))
00197 {
00198 QString tmp = split[1].trimmed();
00199
00200 int ts_offset = TimezoneToInt(tmp);
00201 if (abs(ts_offset) > 840)
00202 {
00203 ts_offset = 0;
00204 localTimezoneOffset = 841;
00205 }
00206 dt = dt.addSecs(-ts_offset * 60);
00207 }
00208
00209 if (localTimezoneOffset < -840)
00210 {
00211 dt = MythUTCToLocal(dt);
00212 }
00213 else if (abs(localTimezoneOffset) <= 840)
00214 {
00215 dt = dt.addSecs(localTimezoneOffset * 60 );
00216 }
00217
00218 timestr = dt.toString("yyyyMMddhhmmss");
00219 }
00220
00221 static void parseCredits(QDomElement &element, ProgInfo *pginfo)
00222 {
00223 for (QDomNode child = element.firstChild(); !child.isNull();
00224 child = child.nextSibling())
00225 {
00226 QDomElement info = child.toElement();
00227 if (!info.isNull())
00228 pginfo->AddPerson(info.tagName(), getFirstText(info));
00229 }
00230 }
00231
00232 static void parseVideo(QDomElement &element, ProgInfo *pginfo)
00233 {
00234 for (QDomNode child = element.firstChild(); !child.isNull();
00235 child = child.nextSibling())
00236 {
00237 QDomElement info = child.toElement();
00238 if (!info.isNull())
00239 {
00240 if (info.tagName() == "quality")
00241 {
00242 if (getFirstText(info) == "HDTV")
00243 pginfo->videoProps |= VID_HDTV;
00244 }
00245 else if (info.tagName() == "aspect")
00246 {
00247 if (getFirstText(info) == "16:9")
00248 pginfo->videoProps |= VID_WIDESCREEN;
00249 }
00250 }
00251 }
00252 }
00253
00254 static void parseAudio(QDomElement &element, ProgInfo *pginfo)
00255 {
00256 for (QDomNode child = element.firstChild(); !child.isNull();
00257 child = child.nextSibling())
00258 {
00259 QDomElement info = child.toElement();
00260 if (!info.isNull())
00261 {
00262 if (info.tagName() == "stereo")
00263 {
00264 if (getFirstText(info) == "mono")
00265 {
00266 pginfo->audioProps |= AUD_MONO;
00267 }
00268 else if (getFirstText(info) == "stereo")
00269 {
00270 pginfo->audioProps |= AUD_STEREO;
00271 }
00272 else if (getFirstText(info) == "dolby" ||
00273 getFirstText(info) == "dolby digital")
00274 {
00275 pginfo->audioProps |= AUD_DOLBY;
00276 }
00277 else if (getFirstText(info) == "surround")
00278 {
00279 pginfo->audioProps |= AUD_SURROUND;
00280 }
00281 }
00282 }
00283 }
00284 }
00285
00286 ProgInfo *XMLTVParser::parseProgram(
00287 QDomElement &element, int localTimezoneOffset)
00288 {
00289 QString uniqueid, season, episode;
00290 int dd_progid_done = 0;
00291 ProgInfo *pginfo = new ProgInfo();
00292
00293 QString text = element.attribute("start", "");
00294 fromXMLTVDate(text, pginfo->starttime, localTimezoneOffset);
00295 pginfo->startts = text;
00296
00297 text = element.attribute("stop", "");
00298 fromXMLTVDate(text, pginfo->endtime, localTimezoneOffset);
00299 pginfo->endts = text;
00300
00301 text = element.attribute("channel", "");
00302 QStringList split = text.split(" ");
00303
00304 pginfo->channel = split[0];
00305
00306 text = element.attribute("clumpidx", "");
00307 if (!text.isEmpty())
00308 {
00309 split = text.split('/');
00310 pginfo->clumpidx = split[0];
00311 pginfo->clumpmax = split[1];
00312 }
00313
00314 for (QDomNode child = element.firstChild(); !child.isNull();
00315 child = child.nextSibling())
00316 {
00317 QDomElement info = child.toElement();
00318 if (!info.isNull())
00319 {
00320 if (info.tagName() == "title")
00321 {
00322 if (isJapan)
00323 {
00324 if (info.attribute("lang") == "ja_JP")
00325 {
00326 pginfo->title = getFirstText(info);
00327 }
00328 else if (info.attribute("lang") == "ja_JP@kana")
00329 {
00330 pginfo->title_pronounce = getFirstText(info);
00331 }
00332 }
00333 else if (pginfo->title.isEmpty())
00334 {
00335 pginfo->title = getFirstText(info);
00336 }
00337 }
00338 else if (info.tagName() == "sub-title" &&
00339 pginfo->subtitle.isEmpty())
00340 {
00341 pginfo->subtitle = getFirstText(info);
00342 }
00343 else if (info.tagName() == "desc" && pginfo->description.isEmpty())
00344 {
00345 pginfo->description = getFirstText(info);
00346 }
00347 else if (info.tagName() == "category")
00348 {
00349 const QString cat = getFirstText(info).toLower();
00350
00351 if (kCategoryNone == pginfo->categoryType &&
00352 string_to_myth_category_type(cat) != kCategoryNone)
00353 {
00354 pginfo->categoryType = string_to_myth_category_type(cat);
00355 }
00356 else if (pginfo->category.isEmpty())
00357 {
00358 pginfo->category = cat;
00359 }
00360
00361 if (cat == "film")
00362 {
00363
00364 pginfo->categoryType = kCategoryMovie;
00365 }
00366 }
00367 else if (info.tagName() == "date" && !pginfo->airdate)
00368 {
00369
00370 QString date = getFirstText(info);
00371 pginfo->airdate = date.left(4).toUInt();
00372 }
00373 else if (info.tagName() == "star-rating" && pginfo->stars.isEmpty())
00374 {
00375 QDomNodeList values = info.elementsByTagName("value");
00376 QDomElement item;
00377 QString stars, num, den;
00378 float rating = 0.0;
00379
00380
00381
00382
00383
00384
00385
00386
00387
00388
00389 item = values.item(0).toElement();
00390 if (!item.isNull())
00391 {
00392 stars = getFirstText(item);
00393 num = stars.section('/', 0, 0);
00394 den = stars.section('/', 1, 1);
00395 if (0.0 < den.toFloat())
00396 rating = num.toFloat()/den.toFloat();
00397 }
00398
00399 pginfo->stars.setNum(rating);
00400 }
00401 else if (info.tagName() == "rating")
00402 {
00403
00404
00405 QDomNodeList values = info.elementsByTagName("value");
00406 QDomElement item = values.item(0).toElement();
00407 if (item.isNull())
00408 continue;
00409 EventRating rating;
00410 rating.system = info.attribute("system", "");
00411 rating.rating = getFirstText(item);
00412 pginfo->ratings.append(rating);
00413 }
00414 else if (info.tagName() == "previously-shown")
00415 {
00416 pginfo->previouslyshown = true;
00417
00418 QString prevdate = info.attribute("start");
00419 if (!prevdate.isEmpty())
00420 {
00421 QDateTime date;
00422 fromXMLTVDate(prevdate, date,
00423 localTimezoneOffset);
00424 pginfo->originalairdate = date.date();
00425 }
00426 }
00427 else if (info.tagName() == "credits")
00428 {
00429 parseCredits(info, pginfo);
00430 }
00431 else if (info.tagName() == "subtitles")
00432 {
00433 if (info.attribute("type") == "teletext")
00434 pginfo->subtitleType |= SUB_NORMAL;
00435 else if (info.attribute("type") == "onscreen")
00436 pginfo->subtitleType |= SUB_ONSCREEN;
00437 else if (info.attribute("type") == "deaf-signed")
00438 pginfo->subtitleType |= SUB_SIGNED;
00439 }
00440 else if (info.tagName() == "audio")
00441 {
00442 parseAudio(info, pginfo);
00443 }
00444 else if (info.tagName() == "video")
00445 {
00446 parseVideo(info, pginfo);
00447 }
00448 else if (info.tagName() == "episode-num")
00449 {
00450 if (info.attribute("system") == "dd_progid")
00451 {
00452 QString episodenum(getFirstText(info));
00453
00454 int idx = episodenum.indexOf('.');
00455 if (idx != -1)
00456 episodenum.remove(idx, 1);
00457 pginfo->programId = episodenum;
00458 dd_progid_done = 1;
00459 }
00460 else if (info.attribute("system") == "xmltv_ns")
00461 {
00462 int tmp;
00463 QString episodenum(getFirstText(info));
00464 episode = episodenum.section('.',1,1);
00465 episode = episode.section('/',0,0).trimmed();
00466 season = episodenum.section('.',0,0).trimmed();
00467 QString part(episodenum.section('.',2,2));
00468 QString partnumber(part.section('/',0,0).trimmed());
00469 QString parttotal(part.section('/',1,1).trimmed());
00470
00471 pginfo->categoryType = kCategorySeries;
00472
00473 if (!episode.isEmpty())
00474 {
00475 tmp = episode.toInt() + 1;
00476 episode = QString::number(tmp);
00477 pginfo->syndicatedepisodenumber = QString('E' + episode);
00478 }
00479
00480 if (!season.isEmpty())
00481 {
00482 tmp = season.toInt() + 1;
00483 season = QString::number(tmp);
00484 pginfo->syndicatedepisodenumber.append(QString('S' + season));
00485 }
00486
00487 uint partno = 0;
00488 if (!partnumber.isEmpty())
00489 {
00490 bool ok;
00491 partno = partnumber.toUInt(&ok) + 1;
00492 partno = (ok) ? partno : 0;
00493 }
00494
00495 if (!parttotal.isEmpty() && partno > 0)
00496 {
00497 bool ok;
00498 uint partto = parttotal.toUInt(&ok) + 1;
00499 if (ok && partnumber <= parttotal)
00500 {
00501 pginfo->parttotal = partto;
00502 pginfo->partnumber = partno;
00503 }
00504 }
00505 }
00506 else if (info.attribute("system") == "onscreen" &&
00507 pginfo->subtitle.isEmpty())
00508 {
00509 pginfo->categoryType = kCategorySeries;
00510 pginfo->subtitle = getFirstText(info);
00511 }
00512 }
00513 }
00514 }
00515
00516 if (pginfo->category.isEmpty() && pginfo->categoryType != kCategoryNone)
00517 pginfo->category = myth_category_type_to_string(pginfo->categoryType);
00518
00519 if (!pginfo->airdate)
00520 pginfo->airdate = current_year;
00521
00522
00523 QString programid;
00524
00525 if (kCategoryMovie == pginfo->categoryType)
00526 programid = "MV";
00527 else if (kCategorySeries == pginfo->categoryType)
00528 programid = "EP";
00529 else if (kCategorySports == pginfo->categoryType)
00530 programid = "SP";
00531 else
00532 programid = "SH";
00533
00534 if (!uniqueid.isEmpty())
00535 programid.append(uniqueid);
00536 else
00537 {
00538 QString seriesid = QString::number(ELFHash(pginfo->title.toUtf8()));
00539 pginfo->seriesId = seriesid;
00540 programid.append(seriesid);
00541
00542 if (!episode.isEmpty() && !season.isEmpty())
00543 {
00544
00545
00546
00547
00548 int season_int = season.toInt();
00549 if (season_int > 35)
00550 {
00551
00552
00553 if (kCategoryMovie != pginfo->categoryType)
00554 programid.clear();
00555 }
00556 else
00557 {
00558 programid.append(episode);
00559 programid.append(QString::number(season_int, 36));
00560 if (pginfo->partnumber && pginfo->parttotal)
00561 {
00562 programid += QString::number(pginfo->partnumber);
00563 programid += QString::number(pginfo->parttotal);
00564 }
00565 }
00566 }
00567 else
00568 {
00569
00570
00571 if (kCategoryMovie != pginfo->categoryType)
00572 programid.clear();
00573 }
00574 }
00575 if (dd_progid_done == 0)
00576 pginfo->programId = programid;
00577
00578 return pginfo;
00579 }
00580
00581 bool XMLTVParser::parseFile(
00582 QString filename, QList<ChanInfo> *chanlist,
00583 QMap<QString, QList<ProgInfo> > *proglist)
00584 {
00585 QDomDocument doc;
00586 QFile f;
00587
00588 if (!dash_open(f, filename, QIODevice::ReadOnly))
00589 {
00590 LOG(VB_GENERAL, LOG_ERR,
00591 QString("Error unable to open '%1' for reading.") .arg(filename));
00592 return false;
00593 }
00594
00595 QString errorMsg = "unknown";
00596 int errorLine = 0;
00597 int errorColumn = 0;
00598
00599 if (!doc.setContent(&f, &errorMsg, &errorLine, &errorColumn))
00600 {
00601 LOG(VB_GENERAL, LOG_ERR, QString("Error in %1:%2: %3")
00602 .arg(errorLine).arg(errorColumn).arg(errorMsg));
00603
00604 f.close();
00605 return true;
00606 }
00607
00608 f.close();
00609
00610
00611
00612 QString config_offset = gCoreContext->GetSetting("TimeOffset", "None");
00613
00614 int localTimezoneOffset = 841;
00615
00616 if (config_offset == "Auto")
00617 {
00618
00619 localTimezoneOffset = -841;
00620 }
00621 else if (config_offset != "None")
00622 {
00623 localTimezoneOffset = TimezoneToInt(config_offset);
00624 if (abs(localTimezoneOffset) > 840)
00625 {
00626 LOG(VB_XMLTV, LOG_ERR, QString("Ignoring invalid TimeOffset %1")
00627 .arg(config_offset));
00628 localTimezoneOffset = 841;
00629 }
00630 }
00631
00632 QDomElement docElem = doc.documentElement();
00633
00634 QUrl baseUrl(docElem.attribute("source-data-url", ""));
00635
00636 QUrl sourceUrl(docElem.attribute("source-info-url", ""));
00637 if (sourceUrl.toString() == "http://labs.zap2it.com/")
00638 {
00639 LOG(VB_GENERAL, LOG_ERR, "Don't use tv_grab_na_dd, use the"
00640 "internal datadirect grabber.");
00641 exit(GENERIC_EXIT_SETUP_ERROR);
00642 }
00643
00644 QString aggregatedTitle;
00645 QString aggregatedDesc;
00646 QString groupingTitle;
00647 QString groupingDesc;
00648
00649 QDomNode n = docElem.firstChild();
00650 while (!n.isNull())
00651 {
00652 QDomElement e = n.toElement();
00653 if (!e.isNull())
00654 {
00655 if (e.tagName() == "channel")
00656 {
00657 ChanInfo *chinfo = parseChannel(e, baseUrl);
00658 chanlist->push_back(*chinfo);
00659 delete chinfo;
00660 }
00661 else if (e.tagName() == "programme")
00662 {
00663 ProgInfo *pginfo = parseProgram(e, localTimezoneOffset);
00664
00665 if (pginfo->startts == pginfo->endts)
00666 {
00667
00668 if (!pginfo->title.isEmpty())
00669 groupingTitle = pginfo->title + " : ";
00670
00671 if (!pginfo->description.isEmpty())
00672 groupingDesc = pginfo->description + " : ";
00673 }
00674 else
00675 {
00676 if (pginfo->clumpidx.isEmpty())
00677 {
00678 if (!groupingTitle.isEmpty())
00679 {
00680 pginfo->title.prepend(groupingTitle);
00681 groupingTitle.clear();
00682 }
00683
00684 if (!groupingDesc.isEmpty())
00685 {
00686 pginfo->description.prepend(groupingDesc);
00687 groupingDesc.clear();
00688 }
00689
00690 (*proglist)[pginfo->channel].push_back(*pginfo);
00691 }
00692 else
00693 {
00694
00695 if (pginfo->clumpidx.toInt() == 0)
00696 {
00697 aggregatedTitle.clear();
00698 aggregatedDesc.clear();
00699 }
00700
00701 if (!pginfo->title.isEmpty())
00702 {
00703 if (!aggregatedTitle.isEmpty())
00704 aggregatedTitle.append(" | ");
00705 aggregatedTitle.append(pginfo->title);
00706 }
00707
00708 if (!pginfo->description.isEmpty())
00709 {
00710 if (!aggregatedDesc.isEmpty())
00711 aggregatedDesc.append(" | ");
00712 aggregatedDesc.append(pginfo->description);
00713 }
00714 if (pginfo->clumpidx.toInt() ==
00715 pginfo->clumpmax.toInt() - 1)
00716 {
00717 pginfo->title = aggregatedTitle;
00718 pginfo->description = aggregatedDesc;
00719 (*proglist)[pginfo->channel].push_back(*pginfo);
00720 }
00721 }
00722 }
00723 delete pginfo;
00724 }
00725 }
00726 n = n.nextSibling();
00727 }
00728
00729 return true;
00730 }
00731