File indexing completed on 2024-05-12 05:09:26
0001 /*************************************************************************** 0002 Copyright (C) 2012-2022 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 <config.h> // for TELLICO_VERSION 0026 0027 #include "allocinefetcher.h" 0028 #include "../collections/videocollection.h" 0029 #include "../images/imagefactory.h" 0030 #include "../entry.h" 0031 #include "../utils/guiproxy.h" 0032 #include "../utils/string_utils.h" 0033 #include "../utils/mapvalue.h" 0034 #include "../tellico_debug.h" 0035 0036 #include <KIO/Job> 0037 #include <KIO/JobUiDelegate> 0038 #include <KLocalizedString> 0039 #include <KJobWidgets/KJobWidgets> 0040 #include <KConfigGroup> 0041 0042 #include <QSpinBox> 0043 #include <QUrl> 0044 #include <QLabel> 0045 #include <QFile> 0046 #include <QTextStream> 0047 #include <QGridLayout> 0048 #include <QTextCodec> 0049 #include <QCryptographicHash> 0050 #include <QJsonDocument> 0051 #include <QJsonObject> 0052 #include <QUrlQuery> 0053 0054 namespace { 0055 static const char* ALLOCINE_API_KEY = "100ED1DA33EB"; 0056 static const char* ALLOCINE_API_URL = "http://api.allocine.fr/rest/v3/"; 0057 static const char* ALLOCINE_PARTNER_KEY = "1a1ed8c1bed24d60ae3472eed1da33eb"; 0058 } 0059 0060 using namespace Tellico; 0061 using Tellico::Fetch::AbstractAllocineFetcher; 0062 using Tellico::Fetch::AllocineFetcher; 0063 0064 AbstractAllocineFetcher::AbstractAllocineFetcher(QObject* parent_, const QString& baseUrl_) 0065 : Fetcher(parent_) 0066 , m_started(false) 0067 , m_apiKey(QLatin1String(ALLOCINE_API_KEY)) 0068 , m_baseUrl(baseUrl_) 0069 , m_numCast(10) { 0070 Q_ASSERT(!m_baseUrl.isEmpty()); 0071 } 0072 0073 AbstractAllocineFetcher::~AbstractAllocineFetcher() { 0074 } 0075 0076 bool AbstractAllocineFetcher::canSearch(Fetch::FetchKey k) const { 0077 return k == Keyword; 0078 } 0079 0080 bool AbstractAllocineFetcher::canFetch(int type) const { 0081 return type == Data::Collection::Video; 0082 } 0083 0084 void AbstractAllocineFetcher::readConfigHook(const KConfigGroup& config_) { 0085 QString k = config_.readEntry("API Key", ALLOCINE_API_KEY); 0086 if(!k.isEmpty()) { 0087 m_apiKey = k; 0088 } 0089 m_numCast = config_.readEntry("Max Cast", 10); 0090 } 0091 0092 void AbstractAllocineFetcher::search() { 0093 m_started = true; 0094 0095 const QString method(QStringLiteral("search")); 0096 0097 QUrl u(m_baseUrl); 0098 u = u.adjusted(QUrl::StripTrailingSlash); 0099 u.setPath(u.path() + QLatin1Char('/') + method); 0100 0101 // the order of the parameters appears to matter 0102 QList<QPair<QString, QString> > params; 0103 params.append(qMakePair(QStringLiteral("partner"), m_apiKey)); 0104 0105 // I can't figure out how to encode accent marks, but they don't 0106 // seem to be necessary 0107 QString q = removeAccents(request().value()); 0108 // should I just remove all non alphabetical characters? 0109 // see https://bugs.kde.org/show_bug.cgi?id=337432 0110 q.remove(QRegularExpression(QStringLiteral("[,:!?;\\(\\)]"))); 0111 q.replace(QLatin1Char('\''), QLatin1Char('+')); 0112 q.replace(QLatin1Char(' '), QLatin1Char('+')); 0113 0114 switch(request().key()) { 0115 case Keyword: 0116 params.append(qMakePair(QStringLiteral("q"), q)); 0117 break; 0118 0119 default: 0120 myWarning() << source() << "- key not recognized:" << request().key(); 0121 stop(); 0122 return; 0123 } 0124 0125 params.append(qMakePair(QStringLiteral("format"), QStringLiteral("json"))); 0126 params.append(qMakePair(QStringLiteral("filter"), QStringLiteral("movie"))); 0127 0128 const QString sed = QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMdd")); 0129 params.append(qMakePair(QStringLiteral("sed"), sed)); 0130 0131 const QByteArray sig = calculateSignature(method, params); 0132 0133 QUrlQuery query; 0134 query.setQueryItems(params); 0135 query.addQueryItem(QStringLiteral("sig"), QLatin1String(sig)); 0136 u.setQuery(query); 0137 // myDebug() << u; 0138 0139 m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); 0140 configureJob(m_job); 0141 KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); 0142 connect(m_job.data(), &KJob::result, this, &AbstractAllocineFetcher::slotComplete); 0143 } 0144 0145 void AbstractAllocineFetcher::stop() { 0146 if(!m_started) { 0147 return; 0148 } 0149 if(m_job) { 0150 m_job->kill(); 0151 } 0152 m_started = false; 0153 emit signalDone(this); 0154 } 0155 0156 Tellico::Data::EntryPtr AbstractAllocineFetcher::fetchEntryHook(uint uid_) { 0157 Data::EntryPtr entry = m_entries.value(uid_); 0158 if(!entry) { 0159 myWarning() << "no entry in dict"; 0160 return Data::EntryPtr(); 0161 } 0162 0163 QString code = entry->field(QStringLiteral("allocine-code")); 0164 if(code.isEmpty()) { 0165 // could mean we already updated the entry 0166 myDebug() << "no allocine release found"; 0167 return entry; 0168 } 0169 const QString method(QStringLiteral("movie")); 0170 0171 QUrl u(m_baseUrl); 0172 u = u.adjusted(QUrl::StripTrailingSlash); 0173 u.setPath(u.path() + QLatin1Char('/') + method); 0174 0175 // the order of the parameters appears to matter 0176 QList<QPair<QString, QString> > params; 0177 params.append(qMakePair(QStringLiteral("partner"), m_apiKey)); 0178 params.append(qMakePair(QStringLiteral("code"), code)); 0179 params.append(qMakePair(QStringLiteral("profile"), QStringLiteral("large"))); 0180 params.append(qMakePair(QStringLiteral("filter"), QStringLiteral("movie"))); 0181 params.append(qMakePair(QStringLiteral("format"), QStringLiteral("json"))); 0182 0183 const QString sed = QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMdd")); 0184 params.append(qMakePair(QStringLiteral("sed"), sed)); 0185 0186 const QByteArray sig = calculateSignature(method, params); 0187 0188 QUrlQuery query; 0189 query.setQueryItems(params); 0190 query.addQueryItem(QStringLiteral("sig"), QLatin1String(sig)); 0191 u.setQuery(query); 0192 // myDebug() << "url: " << u; 0193 // QByteArray data = FileHandler::readDataFile(u, true); 0194 KIO::StoredTransferJob* dataJob = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); 0195 configureJob(dataJob); 0196 if(!dataJob->exec()) { 0197 myDebug() << "Failed to load" << u; 0198 return entry; 0199 } 0200 const QByteArray data = dataJob->data(); 0201 0202 #if 0 0203 myWarning() << "Remove debug2 from allocinefetcher.cpp"; 0204 QFile f(QString::fromLatin1("/tmp/test2.json")); 0205 if(f.open(QIODevice::WriteOnly)) { 0206 QTextStream t(&f); 0207 t.setCodec("UTF-8"); 0208 t << data; 0209 } 0210 f.close(); 0211 #endif 0212 0213 QJsonParseError error; 0214 QJsonDocument doc = QJsonDocument::fromJson(data, &error); 0215 QVariantMap result = doc.object().toVariantMap().value(QStringLiteral("movie")).toMap(); 0216 if(error.error != QJsonParseError::NoError) { 0217 myDebug() << "Bad JSON results"; 0218 #if 0 0219 myWarning() << "Remove debug3 from allocinefetcher.cpp"; 0220 QFile f2(QString::fromLatin1("/tmp/test3.json")); 0221 if(f2.open(QIODevice::WriteOnly)) { 0222 QTextStream t(&f2); 0223 t.setCodec("UTF-8"); 0224 t << data; 0225 } 0226 f2.close(); 0227 #endif 0228 return entry; 0229 } 0230 populateEntry(entry, result); 0231 0232 // image might still be a URL 0233 const QString image_id = entry->field(QStringLiteral("cover")); 0234 if(image_id.contains(QLatin1Char('/'))) { 0235 const QString id = ImageFactory::addImage(QUrl::fromUserInput(image_id), true /* quiet */); 0236 if(id.isEmpty()) { 0237 message(i18n("The cover image could not be loaded."), MessageHandler::Warning); 0238 } 0239 // empty image ID is ok 0240 entry->setField(QStringLiteral("cover"), id); 0241 } 0242 0243 // don't want to include id 0244 entry->collection()->removeField(QStringLiteral("allocine-code")); 0245 QStringList castRows = FieldFormat::splitTable(entry->field(QStringLiteral("cast"))); 0246 while(castRows.count() > m_numCast) { 0247 castRows.removeLast(); 0248 } 0249 entry->setField(QStringLiteral("cast"), castRows.join(FieldFormat::rowDelimiterString())); 0250 return entry; 0251 } 0252 0253 void AbstractAllocineFetcher::slotComplete(KJob*) { 0254 if(m_job->error()) { 0255 myDebug() << "Error:" << m_job->errorString(); 0256 m_job->uiDelegate()->showErrorMessage(); 0257 stop(); 0258 return; 0259 } 0260 0261 QByteArray data = m_job->data(); 0262 if(data.isEmpty()) { 0263 myDebug() << "no data"; 0264 stop(); 0265 return; 0266 } 0267 // see bug 319662. If fetcher is cancelled, job is killed 0268 // if the pointer is retained, it gets double-deleted 0269 m_job = nullptr; 0270 0271 #if 0 0272 myWarning() << "Remove debug from allocinefetcher.cpp"; 0273 QFile f(QString::fromLatin1("/tmp/test.json")); 0274 if(f.open(QIODevice::WriteOnly)) { 0275 QTextStream t(&f); 0276 t.setCodec("UTF-8"); 0277 t << data; 0278 } 0279 f.close(); 0280 #endif 0281 0282 QJsonDocument doc = QJsonDocument::fromJson(data); 0283 QVariantMap result = doc.object().toVariantMap().value(QStringLiteral("feed")).toMap(); 0284 // myDebug() << "total:" << result.value(QLatin1String("totalResults")); 0285 0286 QVariantList resultList = result.value(QStringLiteral("movie")).toList(); 0287 if(resultList.isEmpty()) { 0288 myDebug() << "no results"; 0289 stop(); 0290 return; 0291 } 0292 0293 foreach(const QVariant& result, resultList) { 0294 // myDebug() << "found result:" << result; 0295 0296 //create a new collection for every result since we end up removing the allocine code field 0297 // when fetchEntryHook is called. See bug 338389 0298 Data::EntryPtr entry(new Data::Entry(createCollection())); 0299 populateEntry(entry, result.toMap()); 0300 0301 FetchResult* r = new FetchResult(this, entry); 0302 m_entries.insert(r->uid, entry); 0303 emit signalResultFound(r); 0304 } 0305 0306 m_hasMoreResults = false; 0307 stop(); 0308 } 0309 0310 Tellico::Data::CollPtr AbstractAllocineFetcher::createCollection() const { 0311 Data::CollPtr coll(new Data::VideoCollection(true)); 0312 // always add the allocine release code for fetchEntryHook 0313 Data::FieldPtr field(new Data::Field(QStringLiteral("allocine-code"), QStringLiteral("Allocine Code"), Data::Field::Number)); 0314 field->setCategory(i18n("General")); 0315 coll->addField(field); 0316 0317 // add new fields 0318 if(optionalFields().contains(QStringLiteral("allocine"))) { 0319 Data::FieldPtr field(new Data::Field(QStringLiteral("allocine"), i18n("Allocine Link"), Data::Field::URL)); 0320 field->setCategory(i18n("General")); 0321 coll->addField(field); 0322 } 0323 if(optionalFields().contains(QStringLiteral("origtitle"))) { 0324 Data::FieldPtr f(new Data::Field(QStringLiteral("origtitle"), i18n("Original Title"))); 0325 f->setFormatType(FieldFormat::FormatTitle); 0326 coll->addField(f); 0327 } 0328 0329 return coll; 0330 } 0331 0332 void AbstractAllocineFetcher::populateEntry(Data::EntryPtr entry, const QVariantMap& resultMap) { 0333 if(entry->collection()->hasField(QStringLiteral("allocine-code"))) { 0334 entry->setField(QStringLiteral("allocine-code"), mapValue(resultMap, "code")); 0335 } 0336 0337 entry->setField(QStringLiteral("title"), mapValue(resultMap, "title")); 0338 if(optionalFields().contains(QStringLiteral("origtitle"))) { 0339 entry->setField(QStringLiteral("origtitle"), mapValue(resultMap, "originalTitle")); 0340 } 0341 if(entry->title().isEmpty()) { 0342 entry->setField(QStringLiteral("title"), mapValue(resultMap, "originalTitle")); 0343 } 0344 entry->setField(QStringLiteral("year"), mapValue(resultMap, "productionYear")); 0345 entry->setField(QStringLiteral("plot"), mapValue(resultMap, "synopsis")); 0346 0347 const int runTime = mapValue(resultMap, "runtime").toInt(); 0348 entry->setField(QStringLiteral("running-time"), QString::number(runTime/60)); 0349 0350 const QVariantList castList = resultMap.value(QStringLiteral("castMember")).toList(); 0351 QStringList actors, directors, producers, composers; 0352 foreach(const QVariant& castVariant, castList) { 0353 const QVariantMap castMap = castVariant.toMap(); 0354 const int code = mapValue(castMap, "activity", "code").toInt(); 0355 switch(code) { 0356 case 8001: 0357 actors << (mapValue(castMap, "person", "name") + FieldFormat::columnDelimiterString() + mapValue(castMap, "role")); 0358 break; 0359 case 8002: 0360 directors << mapValue(castMap, "person", "name"); 0361 break; 0362 case 8029: 0363 producers << mapValue(castMap, "person", "name"); 0364 break; 0365 case 8003: 0366 composers << mapValue(castMap, "person", "name"); 0367 break; 0368 } 0369 } 0370 entry->setField(QStringLiteral("cast"), actors.join(FieldFormat::rowDelimiterString())); 0371 entry->setField(QStringLiteral("director"), directors.join(FieldFormat::delimiterString())); 0372 entry->setField(QStringLiteral("producer"), producers.join(FieldFormat::delimiterString())); 0373 entry->setField(QStringLiteral("composer"), composers.join(FieldFormat::delimiterString())); 0374 0375 const QVariantMap releaseMap = resultMap.value(QStringLiteral("release")).toMap(); 0376 entry->setField(QStringLiteral("studio"), mapValue(releaseMap, "distributor", "name")); 0377 0378 QStringList genres; 0379 foreach(const QVariant& variant, resultMap.value(QLatin1String("genre")).toList()) { 0380 genres << i18n(mapValue(variant.toMap(), "$").toUtf8().constData()); 0381 } 0382 entry->setField(QStringLiteral("genre"), genres.join(FieldFormat::delimiterString())); 0383 0384 QStringList nats; 0385 foreach(const QVariant& variant, resultMap.value(QLatin1String("nationality")).toList()) { 0386 nats << mapValue(variant.toMap(), "$"); 0387 } 0388 entry->setField(QStringLiteral("nationality"), nats.join(FieldFormat::delimiterString())); 0389 0390 QStringList langs; 0391 foreach(const QVariant& variant, resultMap.value(QLatin1String("language")).toList()) { 0392 langs << mapValue(variant.toMap(), "$"); 0393 } 0394 entry->setField(QStringLiteral("language"), langs.join(FieldFormat::delimiterString())); 0395 0396 const QVariantMap colorMap = resultMap.value(QLatin1String("color")).toMap(); 0397 if(colorMap.value(QStringLiteral("code")) == QLatin1String("12001")) { 0398 entry->setField(QStringLiteral("color"), i18n("Color")); 0399 } 0400 0401 entry->setField(QStringLiteral("cover"), mapValue(resultMap, "poster", "href")); 0402 0403 if(optionalFields().contains(QStringLiteral("allocine"))) { 0404 entry->setField(QStringLiteral("allocine"), mapValue(resultMap, "link", "href")); 0405 } 0406 } 0407 0408 Tellico::Fetch::FetchRequest AbstractAllocineFetcher::updateRequest(Data::EntryPtr entry_) { 0409 QString title = entry_->field(QStringLiteral("title")); 0410 if(!title.isEmpty()) { 0411 return FetchRequest(Keyword, title); 0412 } 0413 return FetchRequest(); 0414 } 0415 0416 void AbstractAllocineFetcher::configureJob(KIO::StoredTransferJob* job_) { 0417 // 10/8/17: UserAgent appears necessary to receive data 0418 job_->addMetaData(QLatin1String("SendUserAgent"), QLatin1String("true")); 0419 job_->addMetaData(QStringLiteral("UserAgent"), QStringLiteral("Tellico/%1") 0420 .arg(QStringLiteral(TELLICO_VERSION))); 0421 job_->addMetaData(QLatin1String("Languages"), QLatin1String("fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3")); 0422 } 0423 0424 AbstractAllocineFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const AbstractAllocineFetcher* fetcher_) 0425 : Fetch::ConfigWidget(parent_) { 0426 QGridLayout* l = new QGridLayout(optionsWidget()); 0427 l->setSpacing(4); 0428 l->setColumnStretch(1, 10); 0429 0430 int row = -1; 0431 0432 QLabel* label = new QLabel(i18n("&Maximum cast: "), optionsWidget()); 0433 l->addWidget(label, ++row, 0); 0434 m_numCast = new QSpinBox(optionsWidget()); 0435 m_numCast->setMaximum(99); 0436 m_numCast->setMinimum(0); 0437 m_numCast->setValue(10); 0438 #if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0)) 0439 void (QSpinBox::* textChanged)(const QString&) = &QSpinBox::valueChanged; 0440 #else 0441 void (QSpinBox::* textChanged)(const QString&) = &QSpinBox::textChanged; 0442 #endif 0443 connect(m_numCast, textChanged, this, &ConfigWidget::slotSetModified); 0444 l->addWidget(m_numCast, row, 1); 0445 QString w = i18n("The list of cast members may include many people. Set the maximum number returned from the search."); 0446 label->setWhatsThis(w); 0447 m_numCast->setWhatsThis(w); 0448 label->setBuddy(m_numCast); 0449 0450 l->setRowStretch(++row, 10); 0451 0452 m_numCast->setValue(fetcher_ ? fetcher_->m_numCast : 10); 0453 } 0454 0455 void AbstractAllocineFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { 0456 config_.writeEntry("Max Cast", m_numCast->value()); 0457 } 0458 0459 QByteArray AbstractAllocineFetcher::calculateSignature(const QString& method, const QList<QPair<QString, QString> >& params_) { 0460 typedef QPair<QString, QString> StringPair; 0461 QByteArray queryString; 0462 foreach(const StringPair& pair, params_) { 0463 queryString.append(pair.first.toUtf8().toPercentEncoding("+")); 0464 queryString.append('='); 0465 queryString.append(pair.second.toUtf8().toPercentEncoding("+")); 0466 queryString.append('&'); 0467 } 0468 // remove final '&' 0469 queryString.chop(1); 0470 0471 const QByteArray toSign = method.toUtf8() + queryString + ALLOCINE_PARTNER_KEY; 0472 const QByteArray hash = QCryptographicHash::hash(toSign, QCryptographicHash::Sha1); 0473 return hash.toBase64(); 0474 } 0475 0476 /**********************************************************************************************/ 0477 0478 AllocineFetcher::AllocineFetcher(QObject* parent_) 0479 : AbstractAllocineFetcher(parent_, QLatin1String(ALLOCINE_API_URL)) { 0480 } 0481 0482 AllocineFetcher::~AllocineFetcher() { 0483 } 0484 0485 QString AllocineFetcher::source() const { 0486 return m_name.isEmpty() ? defaultName() : m_name; 0487 } 0488 0489 Tellico::Fetch::ConfigWidget* AllocineFetcher::configWidget(QWidget* parent_) const { 0490 return new AllocineFetcher::ConfigWidget(parent_, this); 0491 } 0492 0493 QString AllocineFetcher::defaultName() { 0494 return QStringLiteral("AlloCiné.fr"); 0495 } 0496 0497 QString AllocineFetcher::defaultIcon() { 0498 return favIcon("http://www.allocine.fr"); 0499 } 0500 0501 Tellico::StringHash AllocineFetcher::allOptionalFields() { 0502 StringHash hash; 0503 hash[QStringLiteral("origtitle")] = i18n("Original Title"); 0504 hash[QStringLiteral("allocine")] = i18n("Allocine Link"); 0505 return hash; 0506 } 0507 0508 AllocineFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const AbstractAllocineFetcher* fetcher_) 0509 : AbstractAllocineFetcher::ConfigWidget(parent_, fetcher_) { 0510 // now add additional fields widget 0511 addFieldsWidget(AllocineFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); 0512 } 0513 0514 QString AllocineFetcher::ConfigWidget::preferredName() const { 0515 return AllocineFetcher::defaultName(); 0516 }