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 }