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