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 }