File indexing completed on 2024-05-12 04:52:21
0001 /* 0002 * xmltv.cpp 0003 * 0004 * Copyright (C) 2019 Mauro Carvalho Chehab <mchehab+samsung@kernel.org> 0005 * 0006 * Matches the xmltv dtd found on version 0.6.1 0007 * 0008 * This program is free software; you can redistribute it and/or modify 0009 * it under the terms of the GNU General Public License as published by 0010 * the Free Software Foundation; either version 2 of the License, or 0011 * (at your option) any later version. 0012 * 0013 * This program is distributed in the hope that it will be useful, 0014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 0015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0016 * GNU General Public License for more details. 0017 */ 0018 0019 #include "log.h" 0020 0021 #include <KLocalizedString> 0022 #include <QFile> 0023 #include <QLocale> 0024 #include <QRegularExpression> 0025 #include <QStandardPaths> 0026 #include <QXmlStreamReader> 0027 0028 #include "dvbchannel.h" 0029 #include "dvbepg.h" 0030 #include "dvbmanager.h" 0031 #include "iso-codes.h" 0032 #include "xmltv.h" 0033 0034 #include <QEventLoop> 0035 0036 XmlTv::XmlTv(DvbManager *manager_) : manager(manager_), r(NULL) 0037 { 0038 channelModel = manager->getChannelModel(); 0039 epgModel = manager->getEpgModel(); 0040 0041 connect(&watcher, &QFileSystemWatcher::fileChanged, 0042 this, &XmlTv::load); 0043 }; 0044 0045 void XmlTv::addFile(QString file) 0046 { 0047 if (file.isEmpty()) 0048 return; 0049 0050 load(file); 0051 }; 0052 0053 void XmlTv::clear() 0054 { 0055 if (watcher.files().empty()) 0056 return; 0057 channelMap.clear(); 0058 watcher.removePaths(watcher.files()); 0059 }; 0060 0061 0062 // This function is very close to the one at dvbepg.cpp 0063 DvbEpgLangEntry *XmlTv::getLangEntry(DvbEpgEntry &epgEntry, 0064 QString &code, 0065 bool add_code = true) 0066 { 0067 DvbEpgLangEntry *langEntry; 0068 0069 if (!epgEntry.langEntry.contains(code)) { 0070 if (!add_code) 0071 return NULL; 0072 0073 DvbEpgLangEntry e; 0074 epgEntry.langEntry.insert(code, e); 0075 if (!manager->languageCodes.contains(code)) { 0076 manager->languageCodes[code] = true; 0077 emit epgModel->languageAdded(code); 0078 } 0079 } 0080 langEntry = &epgEntry.langEntry[code]; 0081 0082 return langEntry; 0083 } 0084 0085 bool XmlTv::parseChannel(void) 0086 { 0087 const QString emptyString(""); 0088 QStringRef empty(&emptyString); 0089 0090 const QXmlStreamAttributes attrs = r->attributes(); 0091 QStringRef channelName = attrs.value("id"); 0092 QList<QString>list; 0093 0094 QString current = r->name().toString(); 0095 while (!r->atEnd()) { 0096 const QXmlStreamReader::TokenType t = r->readNext(); 0097 0098 if (t == QXmlStreamReader::EndElement) { 0099 if (r->name() == current) 0100 break; 0101 } 0102 0103 if (t != QXmlStreamReader::StartElement) 0104 continue; 0105 0106 QStringRef name = r->name(); 0107 if (name == "display-name") { 0108 QString display = r->readElementText(); 0109 list.append(display); 0110 } else if (name != "icon" && name != "url") { 0111 static QString lastNotFound(""); 0112 if (name.toString() != lastNotFound) { 0113 qCWarning(logDvb, 0114 "Ignoring unknown channel tag '%s'", 0115 qPrintable(name.toString())); 0116 lastNotFound = name.toString(); 0117 } 0118 } 0119 } 0120 0121 channelMap.insert(channelName.toString(), list); 0122 return true; 0123 } 0124 0125 void XmlTv::parseKeyValues(QHash<QString, QString> &keyValues) 0126 { 0127 QXmlStreamAttributes attrs; 0128 QHash<QString, QString>::ConstIterator it; 0129 0130 QString current = r->name().toString(); 0131 while (!r->atEnd()) { 0132 const QXmlStreamReader::TokenType t = r->readNext(); 0133 0134 if (t == QXmlStreamReader::EndElement) { 0135 if (r->name() == current) 0136 return; 0137 } 0138 0139 if (t != QXmlStreamReader::StartElement) 0140 continue; 0141 0142 attrs = r->attributes(); 0143 QString key = r->name().toString(); 0144 QString value; 0145 0146 it = keyValues.constFind(key); 0147 if (it != keyValues.constEnd()) 0148 value = *it + ", "; 0149 0150 value += r->readElementText(); 0151 0152 keyValues.insert(key, value); 0153 } 0154 } 0155 0156 void XmlTv::ignoreTag(void) 0157 { 0158 QXmlStreamAttributes attrs; 0159 0160 QString current = r->name().toString(); 0161 while (!r->atEnd()) { 0162 const QXmlStreamReader::TokenType t = r->readNext(); 0163 if (t == QXmlStreamReader::EndElement) { 0164 if (r->name() == current) 0165 return; 0166 } 0167 } 0168 } 0169 0170 QString XmlTv::getValue(QHash<QString, QString> &keyValues, QString key) 0171 { 0172 QHash<QString, QString>::ConstIterator it; 0173 0174 it = keyValues.constFind(key); 0175 if (it == keyValues.constEnd()) 0176 return QString(""); 0177 0178 return *it; 0179 } 0180 0181 QString XmlTv::parseCredits(void) 0182 { 0183 QHash<QString, QString>::ConstIterator it; 0184 QHash<QString, QString> keyValues; 0185 QXmlStreamAttributes attrs; 0186 QString name, values; 0187 0188 // Store everything into a hash 0189 QString current = r->name().toString(); 0190 while (!r->atEnd()) { 0191 const QXmlStreamReader::TokenType t = r->readNext(); 0192 0193 if (t == QXmlStreamReader::EndElement) { 0194 if (r->name() == current) 0195 break; 0196 } 0197 0198 if (t != QXmlStreamReader::StartElement) 0199 continue; 0200 0201 attrs = r->attributes(); 0202 QString key = r->name().toString(); 0203 QString value; 0204 0205 it = keyValues.constFind(key); 0206 if (it != keyValues.constEnd()) 0207 value = *it + ", "; 0208 0209 value += r->readElementText(); 0210 0211 if (key == "actor") { 0212 value = i18nc("%1 is a person; %2 is their character", "%1 as %2", value, attrs.value("role").toString()); 0213 } 0214 0215 keyValues.insert(key, value); 0216 } 0217 0218 // Parse the hash values 0219 foreach(const QString &key, keyValues.keys()) { 0220 // Be explicit here, in order to allow translations 0221 if (key == "director") 0222 name = i18n("Director(s)"); 0223 else if (key == "actor") 0224 name = i18n("Actor(s)"); 0225 else if (key == "writer") 0226 name = i18n("Writer(s)"); 0227 else if (key == "adapter") 0228 name = i18n("Adapter(s)"); 0229 else if (key == "producer") 0230 name = i18n("Producer(s)"); 0231 else if (key == "composer") 0232 name = i18n("Composer(s)"); 0233 else if (key == "editor") 0234 name = i18n("Editor(s)"); 0235 else if (key == "presenter") 0236 name = i18n("Presenter(s)"); 0237 else if (key == "commentator") 0238 name = i18n("Commentator(s)"); 0239 else if (key == "guest") 0240 name = i18n("Guest(s)"); 0241 else 0242 name = key + "(s)"; 0243 0244 values += i18nc("%1 is a role (actor, director, etc); %2 is one or more people", "%1: %2\n", name, keyValues.value(key)); 0245 } 0246 0247 return values; 0248 } 0249 0250 bool XmlTv::parseProgram(void) 0251 { 0252 const QString emptyString(""); 0253 QStringRef empty(&emptyString); 0254 0255 QXmlStreamAttributes attrs = r->attributes(); 0256 QStringRef channelName = attrs.value("channel"); 0257 QHash<QString, QList<QString>>::ConstIterator it; 0258 0259 it = channelMap.constFind(channelName.toString()); 0260 if (it == channelMap.constEnd()) { 0261 qCWarning(logDvb, 0262 "Error parsing program: channel %s not found", 0263 qPrintable(channelName.toString())); 0264 return false; 0265 } 0266 0267 QList<QString>list = it.value(); 0268 QList<QString>::iterator name; 0269 bool has_channel = false; 0270 0271 for (name = list.begin(); name != list.end(); ++name) { 0272 if (channelModel->hasChannelByName(*name)) { 0273 has_channel = true; 0274 break; 0275 } 0276 } 0277 0278 if (!has_channel) { 0279 #if 0 // This can be too noisy to keep enabled 0280 static QString lastNotFound(""); 0281 if (channelName.toString() != lastNotFound) { 0282 qCWarning(logDvb, 0283 "Error: channel %s not found at transponders", 0284 qPrintable(channelName.toString())); 0285 lastNotFound = channelName.toString(); 0286 } 0287 #endif 0288 ignoreTag(); 0289 0290 return true; // Not a parsing error 0291 } 0292 0293 DvbSharedChannel channel = channelModel->findChannelByName(*name); 0294 DvbEpgEntry epgEntry; 0295 DvbEpgLangEntry *langEntry; 0296 QString start = attrs.value("start").toString(); 0297 QString stop = attrs.value("stop").toString(); 0298 0299 /* Place "-", ":" and spaces to date formats for Qt::ISODate parser */ 0300 start.replace(QRegularExpression("^(\\d...)(\\d)"), "\\1-\\2"); 0301 start.replace(QRegularExpression("^(\\d...-\\d.)(\\d)"), "\\1-\\2"); 0302 start.replace(QRegularExpression("^(\\d...-\\d.-\\d.)(\\d)"), "\\1 \\2"); 0303 start.replace(QRegularExpression("^(\\d...-\\d.-\\d. \\d.)(\\d)"), "\\1:\\2"); 0304 start.replace(QRegularExpression("^(\\d...-\\d.-\\d. \\d.:\\d.)(\\d)"), "\\1:\\2"); 0305 0306 stop.replace(QRegularExpression("^(\\d...)(\\d)"), "\\1-\\2"); 0307 stop.replace(QRegularExpression("^(\\d...-\\d.)(\\d)"), "\\1-\\2"); 0308 stop.replace(QRegularExpression("^(\\d...-\\d.-\\d.)(\\d)"), "\\1 \\2"); 0309 stop.replace(QRegularExpression("^(\\d...-\\d.-\\d. \\d.)(\\d)"), "\\1:\\2"); 0310 stop.replace(QRegularExpression("^(\\d...-\\d.-\\d. \\d.:\\d.)(\\d)"), "\\1:\\2"); 0311 0312 /* Convert formats to QDateTime */ 0313 epgEntry.begin = QDateTime::fromString(start, Qt::ISODate); 0314 QDateTime end = QDateTime::fromString(stop, Qt::ISODate); 0315 epgEntry.duration = QTime(0, 0, 0).addSecs(epgEntry.begin.secsTo(end)); 0316 0317 epgEntry.begin.setTimeSpec(Qt::UTC); 0318 epgEntry.channel = channel; 0319 0320 QString starRating, credits, date, language, origLanguage, country; 0321 QString episode; 0322 QHash<QString, QString>category, keyword; 0323 0324 QString current = r->name().toString(); 0325 while (!r->atEnd()) { 0326 const QXmlStreamReader::TokenType t = r->readNext(); 0327 0328 if (t == QXmlStreamReader::EndElement) { 0329 if (r->name() == current) 0330 break; 0331 } 0332 0333 if (t != QXmlStreamReader::StartElement) 0334 continue; 0335 0336 QString lang; 0337 QStringRef element = r->name(); 0338 if (element == "title") { 0339 attrs = r->attributes(); 0340 lang = IsoCodes::code2Convert(attrs.value("lang").toString()); 0341 langEntry = getLangEntry(epgEntry, lang); 0342 if (!langEntry->title.isEmpty()) 0343 langEntry->title += ' '; 0344 langEntry->title += r->readElementText(); 0345 } else if (element == "sub-title") { 0346 attrs = r->attributes(); 0347 lang = IsoCodes::code2Convert(attrs.value("lang").toString()); 0348 langEntry = getLangEntry(epgEntry, lang); 0349 if (!langEntry->subheading.isEmpty()) 0350 langEntry->subheading += ' '; 0351 langEntry->subheading += r->readElementText(); 0352 } else if (element == "desc") { 0353 attrs = r->attributes(); 0354 lang = IsoCodes::code2Convert(attrs.value("lang").toString()); 0355 langEntry = getLangEntry(epgEntry, lang); 0356 if (!langEntry->details.isEmpty()) 0357 langEntry->details += ' '; 0358 langEntry->details += r->readElementText(); 0359 } else if (element == "rating") { 0360 QHash<QString, QString> keyValues; 0361 0362 attrs = r->attributes(); 0363 QString system = attrs.value("system").toString(); 0364 0365 parseKeyValues(keyValues); 0366 QString value = getValue(keyValues, "value"); 0367 0368 if (value.isEmpty()) 0369 continue; 0370 0371 if (!epgEntry.parental.isEmpty()) 0372 epgEntry.parental += ", "; 0373 0374 if (!system.isEmpty()) 0375 epgEntry.parental += system + ' '; 0376 0377 epgEntry.parental += i18n("rating: %1", value); 0378 } else if (element == "star-rating") { 0379 QHash<QString, QString> keyValues; 0380 0381 attrs = r->attributes(); 0382 QString system = attrs.value("system").toString(); 0383 0384 parseKeyValues(keyValues); 0385 QString value = getValue(keyValues, "value"); 0386 0387 if (value.isEmpty()) 0388 continue; 0389 0390 if (!system.isEmpty()) 0391 starRating += system + ' '; 0392 0393 starRating += value; 0394 } else if (element == "category") { 0395 attrs = r->attributes(); 0396 lang = IsoCodes::code2Convert(attrs.value("lang").toString()); 0397 0398 QString cat = getValue(category, lang); 0399 if (!cat.isEmpty()) 0400 cat += ", "; 0401 cat += r->readElementText(); 0402 category[lang] = cat; 0403 } else if (element == "keyword") { 0404 attrs = r->attributes(); 0405 lang = IsoCodes::code2Convert(attrs.value("lang").toString()); 0406 0407 QString kw = getValue(keyword, lang); 0408 if (!kw.isEmpty()) 0409 kw += ", "; 0410 kw += r->readElementText(); 0411 keyword[lang] = kw; 0412 } else if (element == "credits") { 0413 credits = parseCredits(); 0414 } else if ((element == "date")) { 0415 QString rawdate = r->readElementText(); 0416 date = rawdate.mid(0, 4); 0417 QString month = rawdate.mid(4, 2); 0418 QString day = rawdate.mid(6, 2); 0419 if (!month.isEmpty()) 0420 date += '-' + month; 0421 if (!day.isEmpty()) { 0422 date += '-' + day; 0423 QDate d = QDate::fromString(date, Qt::ISODate); 0424 date = d.toString(Qt::DefaultLocaleShortDate); 0425 } 0426 } else if (element == "language") { 0427 language = r->readElementText(); 0428 if (language.size() == 2) 0429 IsoCodes::getLanguage(IsoCodes::code2Convert(language), &language); 0430 } else if (element == "orig-language") { 0431 origLanguage = r->readElementText(); 0432 if (origLanguage.size() == 2) 0433 IsoCodes::getLanguage(IsoCodes::code2Convert(origLanguage), &origLanguage); 0434 } else if (element == "country") { 0435 country = r->readElementText(); 0436 if (origLanguage.size() == 2) 0437 IsoCodes::getCountry(IsoCodes::code2Convert(country), &country); 0438 } else if (element == "episode-num") { 0439 attrs = r->attributes(); 0440 QString system = attrs.value("system").toString(); 0441 0442 if (system != "xmltv_ns") 0443 continue; 0444 0445 episode = r->readElementText(); 0446 episode.remove(' '); 0447 episode.remove(QRegularExpression("/.*")); 0448 QStringList list = episode.split('.'); 0449 if (!list.size()) 0450 continue; 0451 episode = i18n("Season %1", QString::number(list[0].toInt() + 1)); 0452 if (list.size() < 2) 0453 continue; 0454 episode += i18n(" Episode %1", QString::number(list[1].toInt() + 1)); 0455 } else if ((element == "aspect") || 0456 (element == "audio") || 0457 (element == "icon") || 0458 (element == "length") || 0459 (element == "last-chance") || 0460 (element == "new") || 0461 (element == "premiere") || 0462 (element == "previously-shown") || 0463 (element == "quality") || 0464 (element == "review") || 0465 (element == "stereo") || 0466 (element == "subtitles") || 0467 (element == "url") || 0468 (element == "video")) { 0469 ignoreTag(); 0470 } else { 0471 static QString lastNotFound(""); 0472 if (element.toString() != lastNotFound) { 0473 qCWarning(logDvb, 0474 "Ignoring unknown programme tag '%s'", 0475 qPrintable(element.toString())); 0476 lastNotFound = element.toString(); 0477 } 0478 } 0479 } 0480 0481 /* Those extra fields are not language-specific data */ 0482 if (!starRating.isEmpty()) { 0483 if (!epgEntry.content.isEmpty()) 0484 epgEntry.content += '\n'; 0485 epgEntry.content += i18n("Star rating: %1", starRating); 0486 } 0487 0488 if (!date.isEmpty()) { 0489 if (!epgEntry.content.isEmpty()) 0490 epgEntry.content += '\n'; 0491 epgEntry.content += i18n("Date: %1", date); 0492 } 0493 0494 foreach(const QString &key, category.keys()) { 0495 QString value = i18n("Category: %1", category[key]); 0496 QString lang = key; 0497 if (key != "QAA") 0498 langEntry = getLangEntry(epgEntry, lang, false); 0499 if (!langEntry) { 0500 if (!epgEntry.content.isEmpty()) 0501 epgEntry.content += '\n'; 0502 epgEntry.content += value; 0503 } else { 0504 langEntry->details.replace(QRegularExpression("\\n*$"), "<p/>"); 0505 langEntry->details += value; 0506 } 0507 } 0508 foreach(const QString &key, keyword.keys()) { 0509 QString value = i18n("Keyword: %1", keyword[key]); 0510 QString lang = key; 0511 if (key != "QAA") 0512 langEntry = getLangEntry(epgEntry, lang, false); 0513 if (!langEntry) { 0514 if (!epgEntry.content.isEmpty()) 0515 epgEntry.content += '\n'; 0516 epgEntry.content += value; 0517 } else { 0518 langEntry->details.replace(QRegularExpression("\\n*$"), "<p/>"); 0519 langEntry->details += value; 0520 } 0521 } 0522 0523 if (!episode.isEmpty()) { 0524 if (!epgEntry.content.isEmpty()) 0525 epgEntry.content += '\n'; 0526 epgEntry.content += i18n("language: %1", episode); 0527 } 0528 0529 if (!language.isEmpty()) { 0530 if (!epgEntry.content.isEmpty()) 0531 epgEntry.content += '\n'; 0532 epgEntry.content += i18n("language: %1", language); 0533 } 0534 0535 if (!origLanguage.isEmpty()) { 0536 if (!epgEntry.content.isEmpty()) 0537 epgEntry.content += '\n'; 0538 epgEntry.content += i18n("Original language: %1", origLanguage); 0539 } 0540 0541 if (!country.isEmpty()) { 0542 if (!epgEntry.content.isEmpty()) 0543 epgEntry.content += '\n'; 0544 epgEntry.content += i18n("Country: %1", country); 0545 } 0546 0547 if (!credits.isEmpty()) { 0548 if (!epgEntry.content.isEmpty()) 0549 epgEntry.content += '\n'; 0550 epgEntry.content += credits; 0551 } 0552 0553 epgEntry.content.replace(QRegularExpression("\\n+$"), ""); 0554 epgEntry.content.replace(QRegularExpression("\\n"), "<p/>"); 0555 0556 epgModel->addEntry(epgEntry); 0557 0558 /* 0559 * It is not uncommon to have the same xmltv channel 0560 * associated with multiple DVB channels. It happens, 0561 * for example, when there is a SD, HD, 4K video 0562 * streams associated with the same programs. 0563 * So, add entries also for the other channels. 0564 */ 0565 for (; name != list.end(); name++) { 0566 if (channelModel->hasChannelByName(*name)) { 0567 channel = channelModel->findChannelByName(*name); 0568 epgEntry.channel = channel; 0569 epgModel->addEntry(epgEntry); 0570 } 0571 } 0572 return true; 0573 } 0574 0575 bool XmlTv::load(QString file) 0576 { 0577 bool parseError = false; 0578 0579 watcher.removePath(file); 0580 if (file.isEmpty()) { 0581 qCInfo(logDvb, "File to load not specified"); 0582 return false; 0583 } 0584 0585 QFile f(file); 0586 if (!f.open(QIODevice::ReadOnly)) { 0587 qCWarning(logDvb, 0588 "Error opening %s: %s. Will stop monitoring it", 0589 qPrintable(file), 0590 qPrintable(f.errorString())); 0591 return false; 0592 } 0593 0594 qCInfo(logDvb, "Reading XMLTV file from %s", qPrintable(file)); 0595 0596 r = new QXmlStreamReader(&f); 0597 while (!r->atEnd()) { 0598 if (r->readNext() != QXmlStreamReader::StartElement) 0599 continue; 0600 0601 QStringRef name = r->name(); 0602 0603 if (name == "channel") { 0604 if (!parseChannel()) 0605 parseError = true; 0606 } else if (name == "programme") { 0607 if (!parseProgram()) 0608 parseError = true; 0609 } else if (name != "tv") { 0610 static QString lastNotFound(""); 0611 if (name.toString() != lastNotFound) { 0612 qCWarning(logDvb, 0613 "Ignoring unknown main tag '%s'", 0614 qPrintable(r->qualifiedName().toString())); 0615 lastNotFound = name.toString(); 0616 } 0617 } 0618 } 0619 0620 if (r->error()) { 0621 qCWarning(logDvb, "XMLTV: error: %s", 0622 qPrintable(r->errorString())); 0623 } 0624 0625 if (parseError) { 0626 qCWarning(logDvb, "XMLTV: parsing error"); 0627 } 0628 0629 f.close(); 0630 watcher.addPath(file); 0631 return parseError; 0632 } 0633 0634 #include "moc_xmltv.cpp"