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