File indexing completed on 2024-05-12 05:09:37
0001 /*************************************************************************** 0002 Copyright (C) 2017-2021 Robby Stephenson <robby@periapsis.org> 0003 ***************************************************************************/ 0004 0005 /*************************************************************************** 0006 * * 0007 * This program is free software; you can redistribute it and/or * 0008 * modify it under the terms of the GNU General Public License as * 0009 * published by the Free Software Foundation; either version 2 of * 0010 * the License or (at your option) version 3 or any later version * 0011 * accepted by the membership of KDE e.V. (or its successor approved * 0012 * by the membership of KDE e.V.), which shall act as a proxy * 0013 * defined in Section 14 of version 3 of the license. * 0014 * * 0015 * This program is distributed in the hope that it will be useful, * 0016 * but WITHOUT ANY WARRANTY; without even the implied warranty of * 0017 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 0018 * GNU General Public License for more details. * 0019 * * 0020 * You should have received a copy of the GNU General Public License * 0021 * along with this program. If not, see <http://www.gnu.org/licenses/>. * 0022 * * 0023 ***************************************************************************/ 0024 0025 #include "kinopoiskfetcher.h" 0026 #include "../utils/guiproxy.h" 0027 #include "../collections/videocollection.h" 0028 #include "../entry.h" 0029 #include "../fieldformat.h" 0030 #include "../images/imagefactory.h" 0031 #include "../utils/string_utils.h" 0032 #include "../utils/mapvalue.h" 0033 #include "../tellico_debug.h" 0034 0035 #include <KLocalizedString> 0036 #include <KIO/Job> 0037 #include <KJobUiDelegate> 0038 #include <KJobWidgets/KJobWidgets> 0039 0040 #include <QRegularExpression> 0041 #include <QRegularExpressionMatch> 0042 #include <QLabel> 0043 #include <QFile> 0044 #include <QTextStream> 0045 #include <QGridLayout> 0046 #include <QSpinBox> 0047 #include <QUrlQuery> 0048 #include <QJsonDocument> 0049 #include <QJsonObject> 0050 #include <QJsonArray> 0051 #include <QJsonParseError> 0052 0053 namespace { 0054 static const char* KINOPOISK_SEARCH_URL = "https://www.kinopoisk.ru/index.php"; 0055 static const char* KINOPOISK_IMAGE_SIZE = "300x450"; 0056 static const char* KINOPOISK_API_FILM_URL = "https://kinopoiskapiunofficial.tech/api/v2.2/films/"; 0057 static const char* KINOPOISK_API_STAFF_URL = "https://kinopoiskapiunofficial.tech/api/v1/staff"; 0058 static const char* KINOPOISK_API_KEY = "9ca8395a794fb28b82e01120a6968bbf03651271fd9ce5d5371a096d4f7dc7a3caa8361ba8914425a1c5c0f4f5d88dbd3d0fccaa781ca18cd4b2b587ebdeaac89cfa771622162a12"; 0059 static const int KINOPOISK_DEFAULT_CAST_SIZE = 10; 0060 } 0061 0062 using namespace Tellico; 0063 using Tellico::Fetch::KinoPoiskFetcher; 0064 0065 KinoPoiskFetcher::KinoPoiskFetcher(QObject* parent_) 0066 : Fetcher(parent_), m_started(false), m_redirected(false), m_numCast(KINOPOISK_DEFAULT_CAST_SIZE) { 0067 m_apiKey = Tellico::reverseObfuscate(KINOPOISK_API_KEY); 0068 } 0069 0070 KinoPoiskFetcher::~KinoPoiskFetcher() { 0071 } 0072 0073 QString KinoPoiskFetcher::source() const { 0074 return m_name.isEmpty() ? defaultName() : m_name; 0075 } 0076 0077 bool KinoPoiskFetcher::canFetch(int type) const { 0078 return type == Data::Collection::Video; 0079 } 0080 0081 bool KinoPoiskFetcher::canSearch(Fetch::FetchKey k) const { 0082 return k == Title; 0083 } 0084 0085 void KinoPoiskFetcher::readConfigHook(const KConfigGroup& config_) { 0086 m_numCast = config_.readEntry("Max Cast", KINOPOISK_DEFAULT_CAST_SIZE); 0087 } 0088 0089 void KinoPoiskFetcher::search() { 0090 m_started = true; 0091 m_redirected = false; 0092 m_redirectUrl.clear(); 0093 m_matches.clear(); 0094 0095 QUrl u(QString::fromLatin1(KINOPOISK_SEARCH_URL)); 0096 QUrlQuery q; 0097 0098 switch(request().key()) { 0099 case Title: 0100 // first means return first result only 0101 //q.addQueryItem(QStringLiteral("first"), QStringLiteral("yes")); 0102 q.addQueryItem(QStringLiteral("kp_query"), request().value()); 0103 break; 0104 0105 default: 0106 myWarning() << source() << "- key not recognized:" << request().key(); 0107 stop(); 0108 return; 0109 } 0110 u.setQuery(q); 0111 // myDebug() << "url: " << u.url(); 0112 0113 m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); 0114 KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); 0115 connect(m_job.data(), &KJob::result, this, &KinoPoiskFetcher::slotComplete); 0116 connect(m_job.data(), &KIO::TransferJob::redirection, 0117 this, &KinoPoiskFetcher::slotRedirection); 0118 } 0119 0120 void KinoPoiskFetcher::stop() { 0121 if(!m_started) { 0122 return; 0123 } 0124 0125 if(m_job) { 0126 m_job->kill(); 0127 m_job = nullptr; 0128 } 0129 m_started = false; 0130 emit signalDone(this); 0131 } 0132 0133 void KinoPoiskFetcher::slotRedirection(KIO::Job*, const QUrl& toUrl_) { 0134 if(m_redirectUrl.isEmpty()) { 0135 myDebug() << "Redirected to" << toUrl_; 0136 m_redirectUrl = toUrl_; 0137 } 0138 m_redirected = true; 0139 } 0140 0141 void KinoPoiskFetcher::slotComplete(KJob*) { 0142 if(m_job->error()) { 0143 m_job->uiDelegate()->showErrorMessage(); 0144 stop(); 0145 return; 0146 } 0147 0148 QByteArray data = m_job->data(); 0149 if(data.isEmpty()) { 0150 myDebug() << "no data"; 0151 stop(); 0152 return; 0153 } 0154 0155 const QString output = Tellico::decodeHTML(data); 0156 #if 0 0157 myWarning() << "Remove debug from kinopoiskfetcher.cpp"; 0158 QFile f(QStringLiteral("/tmp/test1.html")); 0159 if(f.open(QIODevice::WriteOnly)) { 0160 QTextStream t(&f); 0161 t.setCodec("UTF-8"); 0162 t << output; 0163 } 0164 f.close(); 0165 #endif 0166 0167 if(m_started && m_redirected) { 0168 // don't pull the data here, just add it to a single response 0169 auto res = new FetchResult(this, request().value(), QString()); 0170 m_matches.insert(res->uid, m_redirectUrl); 0171 emit signalResultFound(res); 0172 } 0173 0174 // look for a paragraph, class=",", with an internal /ink to "/level/1/film..." 0175 QRegularExpression resultRx(QStringLiteral("<p class=\"name\">\\s*" 0176 "<a href=\"/film[^\"]+\".*? data-url=\"([^\"]*)\".*?>(.*?)</a>\\s*" 0177 "<span class=\"year\">(.*?)</span")); 0178 0179 QString href, title, year; 0180 QRegularExpressionMatchIterator i = resultRx.globalMatch(output); 0181 while(m_started && !m_redirected && i.hasNext()) { 0182 QRegularExpressionMatch match = i.next(); 0183 href = match.captured(1); 0184 title = match.captured(2); 0185 year = match.captured(3); 0186 if(!href.isEmpty()) { 0187 QUrl url(QString::fromLatin1(KINOPOISK_SEARCH_URL)); 0188 url = url.resolved(QUrl(href)); 0189 // myDebug() << url << title << year; 0190 auto res = new FetchResult(this, title, year); 0191 m_matches.insert(res->uid, url); 0192 emit signalResultFound(res); 0193 } 0194 } 0195 0196 // since the fetch is done, don't worry about holding the job pointer 0197 m_job = nullptr; 0198 stop(); 0199 } 0200 0201 Tellico::Data::EntryPtr KinoPoiskFetcher::fetchEntryHook(uint uid_) { 0202 // if we already grabbed this one, then just pull it out of the dict 0203 Data::EntryPtr entry = m_entries[uid_]; 0204 if(entry) { 0205 return entry; 0206 } 0207 0208 QUrl url = m_matches[uid_]; 0209 if(url.isEmpty()) { 0210 myWarning() << "no url in map"; 0211 return Data::EntryPtr(); 0212 } 0213 0214 QPointer<KIO::StoredTransferJob> getJob = KIO::storedGet(url, KIO::NoReload, KIO::HideProgressInfo); 0215 getJob->addMetaData(QStringLiteral("referrer"), QString::fromLatin1(KINOPOISK_SEARCH_URL)); 0216 KJobWidgets::setWindow(getJob, GUI::Proxy::widget()); 0217 if(!getJob->exec()) { 0218 myWarning() << "unable to read" << url; 0219 return Data::EntryPtr(); 0220 } 0221 0222 // the HTML response has the character encoding after the first 1024 characters and Qt doesn't seem to detect that 0223 // and potentially falls back to iso-8859-1. Enforce UTF-8 0224 // const QByteArray data = FileHandler::readDataFile(url, true); 0225 const QByteArray data = getJob->data(); 0226 const QString results = Tellico::decodeHTML(Tellico::fromHtmlData(data, "UTF-8")); 0227 if(results.isEmpty()) { 0228 myDebug() << "KinoPoiskFetcher: no text results"; 0229 return Data::EntryPtr(); 0230 } 0231 0232 #if 0 0233 myDebug() << url.url(); 0234 myWarning() << "Remove debug from kinopoiskfetcher.cpp"; 0235 QFile f(QStringLiteral("/tmp/test2.html")); 0236 if(f.open(QIODevice::WriteOnly)) { 0237 QTextStream t(&f); 0238 t.setCodec("UTF-8"); 0239 t << results; 0240 } 0241 f.close(); 0242 #endif 0243 0244 if(results.contains(QStringLiteral("captcha")) || results.endsWith(QLatin1String("</script>"))) { 0245 // myDebug() << "KinoPoiskFetcher: captcha triggered"; 0246 static const QRegularExpression re(QLatin1String("/(\\d+)")); 0247 QRegularExpressionMatch match = re.match(url.url()); 0248 if(match.hasMatch()) { 0249 entry = requestEntry(match.captured(1)); 0250 } 0251 } else { 0252 entry = parseEntry(results); 0253 if(!entry) { 0254 // might want to check LD+JSON format 0255 // myDebug() << "...trying Linked Data"; 0256 entry = parseEntryLinkedData(results); 0257 } 0258 } 0259 if(!entry) { 0260 // myDebug() << "No discernible entry data"; 0261 return Data::EntryPtr(); 0262 } 0263 0264 QString cover = entry->field(QStringLiteral("cover")); 0265 if(!cover.isEmpty()) { 0266 const QString id = ImageFactory::addImage(QUrl::fromUserInput(cover), true /* quiet */, 0267 QUrl(QString::fromLatin1(KINOPOISK_SEARCH_URL)) /* referer */); 0268 if(id.isEmpty()) { 0269 message(i18n("The cover image could not be loaded."), MessageHandler::Warning); 0270 } 0271 // empty image ID is ok 0272 entry->setField(QStringLiteral("cover"), id); 0273 } 0274 0275 if(optionalFields().contains(QStringLiteral("kinopoisk"))) { 0276 Data::FieldPtr field(new Data::Field(QStringLiteral("kinopoisk"), i18n("KinoPoisk Link"), Data::Field::URL)); 0277 field->setCategory(i18n("General")); 0278 entry->collection()->addField(field); 0279 entry->setField(QStringLiteral("kinopoisk"), url.url()); 0280 } 0281 0282 m_entries.insert(uid_, entry); // keep for later 0283 return entry; 0284 } 0285 0286 Tellico::Data::EntryPtr KinoPoiskFetcher::requestEntry(const QString& filmId_) { 0287 QUrl url(QLatin1String(KINOPOISK_API_FILM_URL) + filmId_); 0288 0289 QPointer<KIO::StoredTransferJob> getJob = KIO::storedGet(url, KIO::NoReload, KIO::HideProgressInfo); 0290 getJob->addMetaData(QStringLiteral("content-type"), QStringLiteral("application/json")); 0291 getJob->addMetaData(QStringLiteral("customHTTPHeader"), QStringLiteral("X-API-KEY: ") + m_apiKey); 0292 KJobWidgets::setWindow(getJob, GUI::Proxy::widget()); 0293 if(!getJob->exec()) { 0294 myWarning() << "unable to read" << url; 0295 return Data::EntryPtr(); 0296 } 0297 0298 QByteArray data = getJob->data(); 0299 #if 0 0300 myDebug() << url; 0301 myWarning() << "Remove json debug from kinopoiskfetcher.cpp"; 0302 QFile file(QString::fromLatin1("/tmp/test-kinopoisk.json")); 0303 if(file.open(QIODevice::WriteOnly)) { 0304 QTextStream t(&file); 0305 t.setCodec("UTF-8"); 0306 t << data; 0307 } 0308 file.close(); 0309 #endif 0310 0311 Data::CollPtr coll(new Data::VideoCollection(true)); 0312 Data::EntryPtr entry(new Data::Entry(coll)); 0313 coll->addEntries(entry); 0314 0315 QJsonDocument doc = QJsonDocument::fromJson(data); 0316 const QVariantMap resultMap = doc.object().toVariantMap(); 0317 0318 entry->setField(QStringLiteral("title"), mapValue(resultMap, "nameRu")); 0319 entry->setField(QStringLiteral("year"), mapValue(resultMap, "year")); 0320 entry->setField(QStringLiteral("nationality"), mapValue(resultMap, "countries", "country")); 0321 entry->setField(QStringLiteral("genre"), mapValue(resultMap, "genres", "genre")); 0322 entry->setField(QStringLiteral("running-time"), mapValue(resultMap, "filmLength")); 0323 entry->setField(QStringLiteral("plot"), mapValue(resultMap, "description")); 0324 entry->setField(QStringLiteral("cover"), mapValue(resultMap, "posterUrl")); 0325 0326 const QString cert(QStringLiteral("certification")); 0327 auto certField = coll->fieldByName(cert); 0328 if(certField) { 0329 entry->setField(cert, mpaaRating(mapValue(resultMap, "ratingMpaa"), certField->allowed())); 0330 } 0331 0332 const QString imdb(QStringLiteral("imdb")); 0333 const QString imdbId = mapValue(resultMap, "imdbId"); 0334 if(optionalFields().contains(imdb) && !imdbId.isEmpty()) { 0335 coll->addField(Data::Field::createDefaultField(Data::Field::ImdbField)); 0336 entry->setField(imdb, QStringLiteral("https://www.imdb.com/title/") + imdbId); 0337 } 0338 const QString origTitle(QStringLiteral("origtitle")); 0339 if(optionalFields().contains(origTitle)) { 0340 if(!coll->hasField(origTitle)) { 0341 Data::FieldPtr f(new Data::Field(origTitle, i18n("Original Title"))); 0342 f->setFormatType(FieldFormat::FormatTitle); 0343 coll->addField(f); 0344 } 0345 entry->setField(origTitle, mapValue(resultMap, "nameOriginal")); 0346 } 0347 0348 url = QUrl(QLatin1String(KINOPOISK_API_STAFF_URL)); 0349 QUrlQuery q; 0350 q.addQueryItem(QStringLiteral("filmId"), filmId_); 0351 url.setQuery(q); 0352 0353 getJob = KIO::storedGet(url, KIO::NoReload, KIO::HideProgressInfo); 0354 getJob->addMetaData(QStringLiteral("content-type"), QStringLiteral("application/json")); 0355 getJob->addMetaData(QStringLiteral("customHTTPHeader"), QStringLiteral("X-API-KEY: ") + m_apiKey); 0356 KJobWidgets::setWindow(getJob, GUI::Proxy::widget()); 0357 if(!getJob->exec()) { 0358 myWarning() << "unable to read" << url; 0359 return Data::EntryPtr(); 0360 } 0361 0362 data = getJob->data(); 0363 #if 0 0364 myDebug() << url; 0365 myWarning() << "Remove json2 debug from kinopoiskfetcher.cpp"; 0366 QFile file2(QString::fromLatin1("/tmp/test-kinopoisk-staff.json")); 0367 if(file2.open(QIODevice::WriteOnly)) { 0368 QTextStream t(&file2); 0369 t.setCodec("UTF-8"); 0370 t << data; 0371 } 0372 file2.close(); 0373 #endif 0374 0375 QStringList directors, writers, actors, producers, composers; 0376 0377 const auto staffArray = QJsonDocument::fromJson(data).array(); 0378 const int sz = staffArray.size(); 0379 for(int i = 0; i < sz; ++i) { 0380 const auto obj = staffArray.at(i).toObject().toVariantMap(); 0381 const QString key = mapValue(obj, "professionKey"); 0382 QString name = mapValue(obj, "nameRu"); 0383 if(name.isEmpty()) name = mapValue(obj, "nameEn"); 0384 if(name.isEmpty()) continue; 0385 if(key == QLatin1String("DIRECTOR")) { 0386 directors += name; 0387 } else if(key == QLatin1String("ACTOR")) { 0388 if(actors.size() < m_numCast) { 0389 actors += (name + FieldFormat::columnDelimiterString() + mapValue(obj, "description")); 0390 } 0391 } else if(key == QLatin1String("WRITER")) { 0392 writers += name; 0393 } else if(key == QLatin1String("PRODUCER")) { 0394 producers += name; 0395 } else if(key == QLatin1String("COMPOSER")) { 0396 composers += name; 0397 } else { 0398 // myDebug() << "...skipping" << key; 0399 } 0400 } 0401 0402 entry->setField(QStringLiteral("director"), directors.join(Tellico::FieldFormat::delimiterString())); 0403 entry->setField(QStringLiteral("writer"), writers.join(Tellico::FieldFormat::delimiterString())); 0404 entry->setField(QStringLiteral("producer"), producers.join(Tellico::FieldFormat::delimiterString())); 0405 entry->setField(QStringLiteral("composer"), composers.join(Tellico::FieldFormat::delimiterString())); 0406 entry->setField(QStringLiteral("cast"), actors.join(Tellico::FieldFormat::rowDelimiterString())); 0407 0408 return entry; 0409 } 0410 0411 Tellico::Data::EntryPtr KinoPoiskFetcher::parseEntry(const QString& str_) { 0412 static const QRegularExpression jsonRx(QStringLiteral("<script.*?type=\"application/json\".*?>(.+?)</script>")); 0413 QRegularExpressionMatch jsonMatch = jsonRx.match(str_); 0414 if(!jsonMatch.hasMatch()) { 0415 myDebug() << "No JSON data"; 0416 return Data::EntryPtr(); 0417 } 0418 0419 QJsonParseError parseError; 0420 QJsonDocument doc = QJsonDocument::fromJson(jsonMatch.captured(1).toUtf8(), &parseError); 0421 if(doc.isNull()) { 0422 myDebug() << "Bad json data:" << parseError.errorString(); 0423 return Data::EntryPtr(); 0424 } 0425 0426 const QString queryId = doc.object().value(QStringLiteral("query")).toObject() 0427 .value(QStringLiteral("id")).toString(); 0428 // if there's no query ID, then this is not a film object to parse 0429 if(queryId.isEmpty()) { 0430 // myDebug() << "No query ID..."; 0431 return Data::EntryPtr(); 0432 } 0433 0434 // otherwise, we're good to go 0435 Data::CollPtr coll(new Data::VideoCollection(true)); 0436 Data::EntryPtr entry(new Data::Entry(coll)); 0437 coll->addEntries(entry); 0438 0439 QJsonObject dataObject = doc.object().value(QStringLiteral("props")).toObject() 0440 .value(QStringLiteral("apolloState")).toObject() 0441 .value(QStringLiteral("data")).toObject(); 0442 QJsonObject filmObject = dataObject.value(QStringLiteral("Film:") + queryId).toObject(); 0443 if(filmObject.isEmpty()) { 0444 filmObject = dataObject.value(QStringLiteral("TvSeries:") + queryId).toObject(); 0445 } 0446 if(filmObject.isEmpty()) { 0447 return Data::EntryPtr(); 0448 } 0449 // iterate over the filmObject members to find the json keys in the dataObject 0450 QJsonObject::const_iterator i = filmObject.constBegin(); 0451 for( ; i != filmObject.constEnd(); ++i) { 0452 const QString fieldName = fieldNameFromKey(i.key()); 0453 if(fieldName.isEmpty()) { 0454 continue; 0455 } 0456 Data::FieldPtr field = entry->collection()->fieldByName(fieldName); 0457 Q_ASSERT(field); 0458 QString fieldValue = fieldValueFromObject(dataObject, fieldName, i.value(), 0459 field ? field->allowed() : QStringList()); 0460 if(!fieldValue.isEmpty()) { 0461 entry->setField(fieldName, fieldValue); 0462 } 0463 0464 // also add original title 0465 if(fieldName == QLatin1String("title")) { 0466 const QString origTitle(QStringLiteral("origtitle")); 0467 if(optionalFields().contains(origTitle)) { 0468 if(!entry->collection()->hasField(origTitle)) { 0469 Data::FieldPtr f(new Data::Field(origTitle, i18n("Original Title"))); 0470 f->setFormatType(FieldFormat::FormatTitle); 0471 entry->collection()->addField(f); 0472 } 0473 fieldValue = fieldValueFromObject(dataObject, origTitle, i.value(), QStringList()); 0474 if(!fieldValue.isEmpty()) { 0475 entry->setField(origTitle, fieldValue); 0476 } 0477 } 0478 } 0479 } 0480 0481 return entry; 0482 } 0483 0484 Tellico::Data::EntryPtr KinoPoiskFetcher::parseEntryLinkedData(const QString& str_) { 0485 QRegularExpression jsonRx(QStringLiteral("<script.*?type=\"application/ld\\+json\".*?>(.+?)</script>")); 0486 QRegularExpressionMatch jsonMatch = jsonRx.match(str_); 0487 if(!jsonMatch.hasMatch()) { 0488 myDebug() << "No LD+JSON data"; 0489 return Data::EntryPtr(); 0490 } 0491 0492 QJsonParseError parseError; 0493 QJsonDocument doc = QJsonDocument::fromJson(jsonMatch.captured(1).toUtf8(), &parseError); 0494 if(doc.isNull()) { 0495 myDebug() << "Bad json data:" << parseError.errorString(); 0496 return Data::EntryPtr(); 0497 } 0498 0499 // otherwise, we're good to go 0500 Data::CollPtr coll(new Data::VideoCollection(true)); 0501 Data::EntryPtr entry(new Data::Entry(coll)); 0502 coll->addEntries(entry); 0503 0504 QVariantMap objectMap = doc.object().toVariantMap(); 0505 entry->setField(QStringLiteral("title"), mapValue(objectMap, "name")); 0506 entry->setField(QStringLiteral("year"), mapValue(objectMap, "datePublished").left(4)); 0507 entry->setField(QStringLiteral("nationality"), mapValue(objectMap, "countryOfOrigin")); 0508 entry->setField(QStringLiteral("cast"), mapValue(objectMap, "actor", "name")); 0509 entry->setField(QStringLiteral("director"), mapValue(objectMap, "director", "name")); 0510 entry->setField(QStringLiteral("producer"), mapValue(objectMap, "producer", "name")); 0511 entry->setField(QStringLiteral("genre"), mapValue(objectMap, "genre")); 0512 entry->setField(QStringLiteral("plot"), mapValue(objectMap, "description")); 0513 QString cover = mapValue(objectMap, "image"); 0514 if(cover.startsWith(QLatin1Char('/'))) { 0515 cover.prepend(QLatin1String("https:")); 0516 } 0517 entry->setField(QStringLiteral("cover"), cover); 0518 0519 return entry; 0520 } 0521 0522 // static 0523 QString KinoPoiskFetcher::fieldNameFromKey(const QString& key_) { 0524 static QHash<QString, QString> fieldHash; 0525 if(fieldHash.isEmpty()) { 0526 fieldHash.insert(QStringLiteral("title"), QStringLiteral("title")); 0527 fieldHash.insert(QStringLiteral("productionYear"), QStringLiteral("year")); 0528 fieldHash.insert(QStringLiteral("duration"), QStringLiteral("running-time")); 0529 fieldHash.insert(QStringLiteral("countries"), QStringLiteral("nationality")); 0530 fieldHash.insert(QStringLiteral("genres"), QStringLiteral("genre")); 0531 fieldHash.insert(QStringLiteral("restriction"), QStringLiteral("certification")); 0532 fieldHash.insert(QStringLiteral("synopsis"), QStringLiteral("plot")); 0533 fieldHash.insert(QStringLiteral("poster"), QStringLiteral("cover")); 0534 } 0535 if(fieldHash.contains(key_)) { 0536 return fieldHash.value(key_); 0537 } 0538 0539 // otherwise some wonky key names 0540 if(key_.contains(QLatin1String("DIRECTOR"), Qt::CaseInsensitive)) { 0541 return QStringLiteral("director"); 0542 } 0543 if(key_.contains(QLatin1String("WRITER"), Qt::CaseInsensitive)) { 0544 return QStringLiteral("writer"); 0545 } 0546 if(key_.contains(QLatin1String("PRODUCER"), Qt::CaseInsensitive)) { 0547 return QStringLiteral("producer"); 0548 } 0549 if(key_.contains(QLatin1String("COMPOSER"), Qt::CaseInsensitive)) { 0550 return QStringLiteral("composer"); 0551 } 0552 if(key_.contains(QLatin1String("ACTOR"), Qt::CaseInsensitive)) { 0553 return QStringLiteral("cast"); 0554 } 0555 return QString(); 0556 } 0557 0558 QString KinoPoiskFetcher::fieldValueFromObject(const QJsonObject& obj_, const QString& field_, 0559 const QJsonValue& value_, const QStringList& allowed_) { 0560 // if it's an array, loop over and recurse 0561 if(value_.isArray()) { 0562 QJsonArray arr = value_.toArray(); 0563 QStringList fieldValues; 0564 for(QJsonArray::const_iterator i = arr.constBegin(); i != arr.constEnd(); ++i) { 0565 const QString value = fieldValueFromObject(obj_, field_, *i, allowed_); 0566 if(!value.isEmpty()) { 0567 fieldValues << value; 0568 } 0569 } 0570 return fieldValues.isEmpty() ? QString() : fieldValues.join(field_ == QLatin1String("cast") ? 0571 Tellico::FieldFormat::rowDelimiterString() : 0572 Tellico::FieldFormat::delimiterString()); 0573 } 0574 0575 if(field_ == QLatin1String("year") || 0576 field_ == QLatin1String("running-time")) { 0577 const int n = value_.toInt(); 0578 return n > 0 ? QString::number(n) : QString(); 0579 } 0580 if(field_ == QLatin1String("plot")) { 0581 return value_.toString(); 0582 } 0583 0584 QJsonObject valueObj = value_.toObject(); 0585 // if there's a reference to another object, need to pull it from the higher level data object 0586 if(valueObj.contains(QStringLiteral("__ref"))) { 0587 valueObj = obj_.value(valueObj.value(QStringLiteral("__ref")).toString()).toObject(); 0588 } 0589 0590 // if it has a 'person' field, gotta grab the person name 0591 if(valueObj.contains(QLatin1String("person"))) { 0592 return fieldValueFromObject(obj_, field_, valueObj.value(QLatin1String("person")), allowed_); 0593 } 0594 0595 if(field_ == QLatin1String("title")) { 0596 const QString title = valueObj.value(QStringLiteral("russian")).toString(); 0597 // return original if russian is not available 0598 return title.isEmpty() ? valueObj.value(QStringLiteral("original")).toString() : title; 0599 } else if(field_ == QLatin1String("origtitle")) { 0600 return valueObj.value(QStringLiteral("original")).toString(); 0601 } else if(field_ == QLatin1String("cover")) { 0602 QString url = valueObj.value(QStringLiteral("avatarsUrl")).toString(); 0603 if(url.startsWith(QLatin1Char('/'))) { 0604 url.prepend(QLatin1String("https:")); 0605 } 0606 // also add size 0607 url.append(QLatin1Char('/') + QLatin1String(KINOPOISK_IMAGE_SIZE)); 0608 return url; 0609 } else if(field_ == QLatin1String("certification")) { 0610 return mpaaRating(valueObj.value(QLatin1String("mpaa")).toString(), allowed_); 0611 // with an 'originalName' or 'name' field return that 0612 // and check this before comparing against field names for people, like 'director' 0613 } else if(valueObj.contains(QLatin1String("originalName")) || valueObj.contains(QLatin1String("name"))) { 0614 const QString name = valueObj.value(QStringLiteral("name")).toString(); 0615 // prefer name to originalName 0616 return name.isEmpty() ? valueObj.value(QStringLiteral("originalName")).toString() : name; 0617 } else if(valueObj.contains(QLatin1String("items"))) { 0618 // some additional nesting apparently 0619 // key in film object points to director object, whose 'items' is an array where each 'is' points to 0620 // a director object which has a person.id pointing to a person object with a 'name' and 'original' value 0621 // valueObj is the director so we want the items array 0622 QJsonValue itemsValue = valueObj.value(QLatin1String("items")); 0623 if(!itemsValue.isArray()) { 0624 myDebug() << "items value is not an array"; 0625 return QString(); 0626 } 0627 return fieldValueFromObject(obj_, field_, itemsValue, allowed_); 0628 } 0629 0630 return QString(); 0631 } 0632 0633 QString KinoPoiskFetcher::mpaaRating(const QString& value_, const QStringList& allowed_) { 0634 // default collection has 5 MPAA values 0635 if(allowed_.size() != 5) return value_; 0636 if(value_ == QLatin1String("g")) { 0637 return allowed_.at(0); 0638 } else if(value_ == QLatin1String("pg")) { 0639 return allowed_.at(1); 0640 } else if(value_ == QLatin1String("pg13")) { 0641 return allowed_.at(2); 0642 } else if(value_ == QLatin1String("r")) { 0643 return allowed_.at(3); 0644 } else { 0645 return allowed_.at(4); 0646 } 0647 } 0648 0649 Tellico::Fetch::FetchRequest KinoPoiskFetcher::updateRequest(Data::EntryPtr entry_) { 0650 QString t = entry_->field(QStringLiteral("title")); 0651 if(!t.isEmpty()) { 0652 return FetchRequest(Fetch::Title, t); 0653 } 0654 return FetchRequest(); 0655 } 0656 0657 Tellico::Fetch::ConfigWidget* KinoPoiskFetcher::configWidget(QWidget* parent_) const { 0658 return new KinoPoiskFetcher::ConfigWidget(parent_); 0659 } 0660 0661 QString KinoPoiskFetcher::defaultName() { 0662 return QStringLiteral("КиноПоиск (KinoPoisk.ru)"); 0663 } 0664 0665 QString KinoPoiskFetcher::defaultIcon() { 0666 return favIcon("http://www.kinopoisk.ru"); 0667 } 0668 0669 Tellico::StringHash KinoPoiskFetcher::allOptionalFields() { 0670 StringHash hash; 0671 hash[QStringLiteral("kinopoisk")] = i18n("KinoPoisk Link"); 0672 hash[QStringLiteral("imdb")] = i18n("IMDb Link"); 0673 hash[QStringLiteral("origtitle")] = i18n("Original Title"); 0674 return hash; 0675 } 0676 0677 KinoPoiskFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const KinoPoiskFetcher* fetcher_) 0678 : Fetch::ConfigWidget(parent_) { 0679 QGridLayout* l = new QGridLayout(optionsWidget()); 0680 l->setSpacing(4); 0681 l->setColumnStretch(1, 10); 0682 0683 int row = -1; 0684 0685 QLabel* label = new QLabel(i18n("&Maximum cast: "), optionsWidget()); 0686 l->addWidget(label, ++row, 0); 0687 m_numCast = new QSpinBox(optionsWidget()); 0688 m_numCast->setMaximum(99); 0689 m_numCast->setMinimum(0); 0690 m_numCast->setValue(KINOPOISK_DEFAULT_CAST_SIZE); 0691 #if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0)) 0692 void (QSpinBox::* textChanged)(const QString&) = &QSpinBox::valueChanged; 0693 #else 0694 void (QSpinBox::* textChanged)(const QString&) = &QSpinBox::textChanged; 0695 #endif 0696 connect(m_numCast, textChanged, this, &ConfigWidget::slotSetModified); 0697 l->addWidget(m_numCast, row, 1); 0698 QString w = i18n("The list of cast members may include many people. Set the maximum number returned from the search."); 0699 label->setWhatsThis(w); 0700 m_numCast->setWhatsThis(w); 0701 label->setBuddy(m_numCast); 0702 0703 l->setRowStretch(++row, 10); 0704 0705 addFieldsWidget(KinoPoiskFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); 0706 if(fetcher_) { 0707 m_numCast->setValue(fetcher_->m_numCast); 0708 } 0709 } 0710 0711 QString KinoPoiskFetcher::ConfigWidget::preferredName() const { 0712 return KinoPoiskFetcher::defaultName(); 0713 } 0714 0715 void KinoPoiskFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { 0716 config_.writeEntry("Max Cast", m_numCast->value()); 0717 }