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"