File indexing completed on 2024-05-12 05:09:43

0001 /***************************************************************************
0002     Copyright (C) 2009-2014 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 "themoviedbfetcher.h"
0026 #include "../collections/videocollection.h"
0027 #include "../images/imagefactory.h"
0028 #include "../gui/combobox.h"
0029 #include "../core/filehandler.h"
0030 #include "../utils/guiproxy.h"
0031 #include "../utils/mapvalue.h"
0032 #include "../tellico_debug.h"
0033 
0034 #include <KLocalizedString>
0035 #include <KConfigGroup>
0036 #include <KJob>
0037 #include <KJobUiDelegate>
0038 #include <KJobWidgets/KJobWidgets>
0039 #include <KIO/StoredTransferJob>
0040 #include <kwidgetsaddons_version.h>
0041 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5,55,0)
0042 #include <KLanguageName>
0043 #endif
0044 
0045 #include <QLabel>
0046 #include <QLineEdit>
0047 #include <QFile>
0048 #include <QTextStream>
0049 #include <QGridLayout>
0050 #include <QTextCodec>
0051 #include <QJsonDocument>
0052 #include <QJsonObject>
0053 #include <QUrlQuery>
0054 #include <QStandardPaths>
0055 #include <QSpinBox>
0056 
0057 namespace {
0058   static const int THEMOVIEDB_MAX_RETURNS_TOTAL = 20;
0059   static const char* THEMOVIEDB_API_URL = "https://api.themoviedb.org";
0060   static const char* THEMOVIEDB_API_VERSION = "3"; // krazy:exclude=doublequote_chars
0061   static const char* THEMOVIEDB_API_KEY = "919890b4128d33c729dc368209ece555";
0062   static const uint THEMOVIEDB_DEFAULT_CAST_SIZE = 10;
0063   static const uint THEMOVIEDB_MAX_SEASON_COUNT = 10;
0064 }
0065 
0066 using namespace Tellico;
0067 using Tellico::Fetch::TheMovieDBFetcher;
0068 
0069 TheMovieDBFetcher::TheMovieDBFetcher(QObject* parent_)
0070     : Fetcher(parent_)
0071     , m_started(false)
0072     , m_locale(QStringLiteral("en"))
0073     , m_apiKey(QLatin1String(THEMOVIEDB_API_KEY))
0074     , m_numCast(THEMOVIEDB_DEFAULT_CAST_SIZE) {
0075   //  setLimit(THEMOVIEDB_MAX_RETURNS_TOTAL);
0076 }
0077 
0078 TheMovieDBFetcher::~TheMovieDBFetcher() {
0079 }
0080 
0081 QString TheMovieDBFetcher::source() const {
0082   return m_name.isEmpty() ? defaultName() : m_name;
0083 }
0084 
0085 // https://www.themoviedb.org/about/api-terms
0086 QString TheMovieDBFetcher::attribution() const {
0087   return QStringLiteral("This product uses the TMDb API but is not endorsed or certified by TMDb.");
0088 }
0089 
0090 bool TheMovieDBFetcher::canSearch(Fetch::FetchKey k) const {
0091   return k == Title;
0092 }
0093 
0094 bool TheMovieDBFetcher::canFetch(int type) const {
0095   return type == Data::Collection::Video;
0096 }
0097 
0098 void TheMovieDBFetcher::readConfigHook(const KConfigGroup& config_) {
0099   QString k = config_.readEntry("API Key", THEMOVIEDB_API_KEY);
0100   if(!k.isEmpty()) {
0101     m_apiKey = k;
0102   }
0103   k = config_.readEntry("Locale", "en");
0104   if(!k.isEmpty()) {
0105     m_locale = k.toLower();
0106   }
0107   k = config_.readEntry("ImageBase");
0108   if(!k.isEmpty()) {
0109     m_imageBase = k;
0110   }
0111   m_serverConfigDate = config_.readEntry("ServerConfigDate", QDate());
0112   m_numCast = config_.readEntry("Max Cast", THEMOVIEDB_DEFAULT_CAST_SIZE);
0113 }
0114 
0115 void TheMovieDBFetcher::saveConfigHook(KConfigGroup& config_) {
0116   if(!m_serverConfigDate.isNull()) {
0117     config_.writeEntry("ServerConfigDate", m_serverConfigDate);
0118   }
0119   config_.writeEntry("ImageBase", m_imageBase);
0120 }
0121 
0122 void TheMovieDBFetcher::search() {
0123   continueSearch();
0124 }
0125 
0126 void TheMovieDBFetcher::continueSearch() {
0127   m_started = true;
0128 
0129   QUrl u(QString::fromLatin1(THEMOVIEDB_API_URL));
0130   u.setPath(QLatin1Char('/') + QLatin1String(THEMOVIEDB_API_VERSION));
0131   u = u.adjusted(QUrl::StripTrailingSlash);
0132 
0133   QUrlQuery q;
0134   switch(request().key()) {
0135     case Title:
0136       u.setPath(u.path() + QLatin1String("/search/multi"));
0137       q.addQueryItem(QStringLiteral("query"), request().value());
0138       break;
0139 
0140     case Raw:
0141       if(request().data().isEmpty()) {
0142         u.setPath(u.path() + QLatin1String("/search/multi"));
0143       } else {
0144         u.setPath(u.path() + request().data());
0145       }
0146       q.setQuery(request().value());
0147       break;
0148 
0149     default:
0150       myWarning() << source() << "- key not recognized:" << request().key();
0151       stop();
0152       return;
0153   }
0154   q.addQueryItem(QStringLiteral("language"), m_locale);
0155   q.addQueryItem(QStringLiteral("api_key"), m_apiKey);
0156   u.setQuery(q);
0157 //  myDebug() << u;
0158 
0159   if(m_apiKey.isEmpty()) {
0160     myDebug() << source() << "- empty API key";
0161     message(i18n("An access key is required to use this data source.")
0162             + QLatin1Char(' ') +
0163             i18n("Those values must be entered in the data source settings."), MessageHandler::Error);
0164     stop();
0165     return;
0166   }
0167 
0168   m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo);
0169   KJobWidgets::setWindow(m_job, GUI::Proxy::widget());
0170   connect(m_job.data(), &KJob::result, this, &TheMovieDBFetcher::slotComplete);
0171 }
0172 
0173 void TheMovieDBFetcher::stop() {
0174   if(!m_started) {
0175     return;
0176   }
0177   if(m_job) {
0178     m_job->kill();
0179     m_job = nullptr;
0180   }
0181   m_started = false;
0182   emit signalDone(this);
0183 }
0184 
0185 Tellico::Data::EntryPtr TheMovieDBFetcher::fetchEntryHook(uint uid_) {
0186   Data::EntryPtr entry = m_entries.value(uid_);
0187   if(!entry) {
0188     myWarning() << "no entry in dict";
0189     return Data::EntryPtr();
0190   }
0191 
0192   if(m_imageBase.isEmpty() || m_serverConfigDate.daysTo(QDate::currentDate()) > 7) {
0193     readConfiguration();
0194   }
0195 
0196   QString id = entry->field(QStringLiteral("tmdb-id"));
0197   if(!id.isEmpty()) {
0198     const QString mediaType = entry->field(QStringLiteral("tmdb-type"));
0199     // quiet
0200     QUrl u(QString::fromLatin1(THEMOVIEDB_API_URL));
0201     u.setPath(QStringLiteral("/%1/%2/%3")
0202               .arg(QLatin1String(THEMOVIEDB_API_VERSION),
0203                    mediaType.isEmpty() ? QLatin1String("movie") : mediaType,
0204                    id));
0205     QUrlQuery q;
0206     q.addQueryItem(QStringLiteral("api_key"), m_apiKey);
0207     q.addQueryItem(QStringLiteral("language"), m_locale);
0208     QString append;
0209     if(optionalFields().contains(QStringLiteral("episode"))) {
0210       // can only do one season at a time?
0211       append = QLatin1String("alternative_titles,credits");
0212       for(uint snum = 1; snum <= THEMOVIEDB_MAX_SEASON_COUNT; ++snum) {
0213         append += QLatin1String(",season/") + QString::number(snum);
0214       }
0215     } else {
0216       append = QLatin1String("alternative_titles,credits");
0217     }
0218     q.addQueryItem(QStringLiteral("append_to_response"), append);
0219     u.setQuery(q);
0220     QByteArray data = FileHandler::readDataFile(u, true);
0221 #if 0
0222     myWarning() << "Remove debug2 from themoviedbfetcher.cpp" << u.url();
0223     QFile f(QStringLiteral("/tmp/test2.json"));
0224     if(f.open(QIODevice::WriteOnly)) {
0225       QTextStream t(&f);
0226       t.setCodec("UTF-8");
0227       t << data;
0228     }
0229     f.close();
0230 #endif
0231     QJsonDocument doc = QJsonDocument::fromJson(data);
0232     populateEntry(entry, doc.object().toVariantMap(), true);
0233   }
0234 
0235   // image might still be a URL
0236   const QString image_id = entry->field(QStringLiteral("cover"));
0237   if(image_id.contains(QLatin1Char('/'))) {
0238     const QString id = ImageFactory::addImage(QUrl::fromUserInput(image_id), true /* quiet */);
0239     if(id.isEmpty()) {
0240       message(i18n("The cover image could not be loaded."), MessageHandler::Warning);
0241     }
0242     // empty image ID is ok
0243     entry->setField(QStringLiteral("cover"), id);
0244   }
0245 
0246   // don't want to include TMDb ID field
0247   entry->setField(QStringLiteral("tmdb-id"), QString());
0248   entry->setField(QStringLiteral("tmdb-type"), QString());
0249 
0250   return entry;
0251 }
0252 
0253 Tellico::Fetch::FetchRequest TheMovieDBFetcher::updateRequest(Data::EntryPtr entry_) {
0254   QString imdb = entry_->field(QStringLiteral("imdb"));
0255   if(imdb.isEmpty()) {
0256     imdb = entry_->field(QStringLiteral("imdb-id"));
0257   }
0258   if(!imdb.isEmpty()) {
0259     QRegularExpression ttRx(QStringLiteral("tt\\d+"));
0260     auto ttMatch = ttRx.match(imdb);
0261     if(ttMatch.hasMatch()) {
0262       FetchRequest req(Raw, QStringLiteral("external_source=imdb_id"));
0263       req.setData(QLatin1String("/find/") + ttMatch.captured()); // tell the request to use a different endpoint
0264       return req;
0265     }
0266   }
0267 
0268   const QString title = entry_->field(QStringLiteral("title"));
0269   const QString year = entry_->field(QStringLiteral("year"));
0270   if(!title.isEmpty()) {
0271     if(year.isEmpty()) {
0272       return FetchRequest(Title, title);
0273     } else {
0274       return FetchRequest(Raw, QStringLiteral("query=\"%1\"&year=%2").arg(title, year));
0275     }
0276   }
0277   return FetchRequest();
0278 }
0279 
0280 void TheMovieDBFetcher::slotComplete(KJob* job_) {
0281   KIO::StoredTransferJob* job = static_cast<KIO::StoredTransferJob*>(job_);
0282 
0283   if(job->error()) {
0284     job->uiDelegate()->showErrorMessage();
0285     stop();
0286     return;
0287   }
0288 
0289   const QByteArray data = job->data();
0290   if(data.isEmpty()) {
0291     myDebug() << "no data";
0292     stop();
0293     return;
0294   }
0295   // see bug 319662. If fetcher is cancelled, job is killed
0296   // if the pointer is retained, it gets double-deleted
0297   m_job = nullptr;
0298 
0299 #if 0
0300   myWarning() << "Remove debug from themoviedbfetcher.cpp";
0301   QFile f(QStringLiteral("/tmp/test.json"));
0302   if(f.open(QIODevice::WriteOnly)) {
0303     QTextStream t(&f);
0304     t.setCodec("UTF-8");
0305     t << data;
0306   }
0307   f.close();
0308 #endif
0309 
0310   Data::CollPtr coll(new Data::VideoCollection(true));
0311   // always add the tmdb-id for fetchEntryHook
0312   Data::FieldPtr field(new Data::Field(QStringLiteral("tmdb-id"), QStringLiteral("TMDb ID"), Data::Field::Line));
0313   field->setCategory(i18n("General"));
0314   coll->addField(field);
0315   field = new Data::Field(QStringLiteral("tmdb-type"), QStringLiteral("TMDb Type"), Data::Field::Line);
0316   field->setCategory(i18n("General"));
0317   coll->addField(field);
0318 
0319   if(optionalFields().contains(QStringLiteral("tmdb"))) {
0320     Data::FieldPtr field(new Data::Field(QStringLiteral("tmdb"), i18n("TMDb Link"), Data::Field::URL));
0321     field->setCategory(i18n("General"));
0322     coll->addField(field);
0323   }
0324   if(optionalFields().contains(QStringLiteral("imdb"))) {
0325     coll->addField(Data::Field::createDefaultField(Data::Field::ImdbField));
0326   }
0327   if(optionalFields().contains(QStringLiteral("alttitle"))) {
0328     Data::FieldPtr field(new Data::Field(QStringLiteral("alttitle"), i18n("Alternative Titles"), Data::Field::Table));
0329     field->setFormatType(FieldFormat::FormatTitle);
0330     coll->addField(field);
0331   }
0332   if(optionalFields().contains(QStringLiteral("origtitle"))) {
0333     Data::FieldPtr f(new Data::Field(QStringLiteral("origtitle"), i18n("Original Title")));
0334     f->setFormatType(FieldFormat::FormatTitle);
0335     coll->addField(f);
0336   }
0337   if(optionalFields().contains(QStringLiteral("network"))) {
0338     Data::FieldPtr field(new Data::Field(QStringLiteral("network"), i18n("Network"), Data::Field::Line));
0339     field->setCategory(i18n("General"));
0340     coll->addField(field);
0341   }
0342   if(optionalFields().contains(QStringLiteral("episode"))) {
0343     coll->addField(Data::Field::createDefaultField(Data::Field::EpisodeField));
0344   }
0345 
0346   QJsonDocument doc = QJsonDocument::fromJson(data);
0347   QVariantMap result = doc.object().toVariantMap();
0348 
0349   QVariantList resultList = result.value(QStringLiteral("results")).toList();
0350   if(resultList.isEmpty()) {
0351     resultList = result.value(QStringLiteral("movie_results")).toList();
0352   }
0353   if(resultList.isEmpty()) {
0354     resultList = result.value(QStringLiteral("tv_results")).toList();
0355   }
0356 
0357   if(resultList.isEmpty()) {
0358     myDebug() << "no results";
0359     stop();
0360     return;
0361   }
0362 
0363   int count = 0;
0364   foreach(const QVariant& result, resultList) {
0365 //    myDebug() << "found result:" << result;
0366 
0367     Data::EntryPtr entry(new Data::Entry(coll));
0368     populateEntry(entry, result.toMap(), false);
0369 
0370     FetchResult* r = new FetchResult(this, entry);
0371     m_entries.insert(r->uid, entry);
0372     emit signalResultFound(r);
0373     ++count;
0374     if(count >= THEMOVIEDB_MAX_RETURNS_TOTAL) {
0375       break;
0376     }
0377   }
0378 
0379   stop();
0380 }
0381 
0382 void TheMovieDBFetcher::populateEntry(Data::EntryPtr entry_, const QVariantMap& resultMap_, bool fullData_) {
0383   entry_->setField(QStringLiteral("tmdb-id"), mapValue(resultMap_, "id"));
0384   const QString tmdbType = QStringLiteral("tmdb-type");
0385   if(entry_->field(tmdbType).isEmpty()) {
0386     entry_->setField(tmdbType, mapValue(resultMap_, "media_type"));
0387   }
0388   entry_->setField(QStringLiteral("title"), mapValue(resultMap_, "title"));
0389   if(entry_->title().isEmpty()) {
0390     entry_->setField(QStringLiteral("title"), mapValue(resultMap_, "name"));
0391   }
0392   if(resultMap_.contains(QLatin1String("release_date"))) {
0393     entry_->setField(QStringLiteral("year"),  mapValue(resultMap_, "release_date").left(4));
0394   } else {
0395     entry_->setField(QStringLiteral("year"),  mapValue(resultMap_, "first_air_date").left(4));
0396   }
0397 
0398   QStringList directors, producers, writers, composers;
0399   QVariantList crewList = resultMap_.value(QStringLiteral("credits")).toMap()
0400                                     .value(QStringLiteral("crew")).toList();
0401   foreach(const QVariant& crew, crewList) {
0402     const QVariantMap crewMap = crew.toMap();
0403     const QString job = mapValue(crewMap, "job");
0404     if(job == QLatin1String("Director")) {
0405       directors += mapValue(crewMap, "name");
0406     } else if(job == QLatin1String("Producer")) {
0407       producers += mapValue(crewMap, "name");
0408     } else if(job == QLatin1String("Screenplay")) {
0409       writers += mapValue(crewMap, "name");
0410     } else if(job == QLatin1String("Original Music Composer")) {
0411       composers += mapValue(crewMap, "name");
0412     }
0413   }
0414   entry_->setField(QStringLiteral("director"), directors.join(FieldFormat::delimiterString()));
0415   entry_->setField(QStringLiteral("producer"), producers.join(FieldFormat::delimiterString()));
0416   entry_->setField(QStringLiteral("writer"),     writers.join(FieldFormat::delimiterString()));
0417   entry_->setField(QStringLiteral("composer"), composers.join(FieldFormat::delimiterString()));
0418 
0419   // if we only need cursory data, then we're done
0420   if(!fullData_) {
0421     return;
0422   }
0423 
0424   if(entry_->collection()->hasField(QStringLiteral("tmdb"))) {
0425     QString mediaType = entry_->field(tmdbType);
0426     if(mediaType.isEmpty()) mediaType = QLatin1String("movie");
0427     entry_->setField(QStringLiteral("tmdb"), QStringLiteral("https://www.themoviedb.org/%1/%2").arg(mediaType, mapValue(resultMap_, "id")));
0428   }
0429   if(entry_->collection()->hasField(QStringLiteral("imdb"))) {
0430     const QString imdbId = mapValue(resultMap_, "imdb_id");
0431     if(!imdbId.isEmpty()) {
0432       entry_->setField(QStringLiteral("imdb"), QLatin1String("https://www.imdb.com/title/") + imdbId);
0433     }
0434   }
0435   if(entry_->collection()->hasField(QStringLiteral("origtitle"))) {
0436     QString otitle = mapValue(resultMap_, "original_title");
0437     if(otitle.isEmpty()) otitle = mapValue(resultMap_, "original_name");
0438     entry_->setField(QStringLiteral("origtitle"), otitle);
0439   }
0440   if(entry_->collection()->hasField(QStringLiteral("alttitle"))) {
0441     QStringList atitles;
0442     foreach(const QVariant& atitle, resultMap_.value(QLatin1String("alternative_titles")).toMap()
0443                                                .value(QLatin1String("titles")).toList()) {
0444       atitles << mapValue(atitle.toMap(), "title");
0445     }
0446     if(atitles.isEmpty()) {
0447       atitles += mapValue(resultMap_, "alternative_titles", "results", "title");
0448     }
0449     entry_->setField(QStringLiteral("alttitle"), atitles.join(FieldFormat::rowDelimiterString()));
0450   }
0451   if(entry_->collection()->hasField(QStringLiteral("network"))) {
0452     entry_->setField(QStringLiteral("network"), mapValue(resultMap_, "networks", "name"));
0453   }
0454   if(optionalFields().contains(QStringLiteral("episode"))) {
0455     QStringList episodes;
0456     for(uint snum = 1; snum <= THEMOVIEDB_MAX_SEASON_COUNT; ++snum) {
0457       const QString seasonString = QLatin1String("season/") + QString::number(snum);
0458       if(!resultMap_.contains(seasonString)) {
0459         break; // no more seasons
0460       }
0461       const auto episodeList = resultMap_.value(seasonString).toMap()
0462                                          .value(QStringLiteral("episodes")).toList();
0463       foreach(const QVariant& row, episodeList) {
0464         // episode title, season, episode number
0465         const auto map = row.toMap();
0466         episodes << mapValue(map, "name") + FieldFormat::columnDelimiterString() +
0467                     mapValue(map, "season_number") + FieldFormat::columnDelimiterString() +
0468                     mapValue(map, "episode_number");
0469       }
0470     }
0471     entry_->setField(QStringLiteral("episode"), episodes.join(FieldFormat::rowDelimiterString()));
0472   }
0473 
0474   QStringList actors;
0475   QVariantList castList = resultMap_.value(QStringLiteral("credits")).toMap()
0476                                     .value(QStringLiteral("cast")).toList();
0477   foreach(const QVariant& cast, castList) {
0478     const QVariantMap castMap = cast.toMap();
0479     actors << mapValue(castMap, "name") + FieldFormat::columnDelimiterString() + mapValue(castMap, "character");
0480     if(actors.count() >= m_numCast) {
0481       break;
0482     }
0483   }
0484   entry_->setField(QStringLiteral("cast"), actors.join(FieldFormat::rowDelimiterString()));
0485 
0486   QStringList studios;
0487   foreach(const QVariant& studio, resultMap_.value(QLatin1String("production_companies")).toList()) {
0488     studios << mapValue(studio.toMap(), "name");
0489   }
0490   entry_->setField(QStringLiteral("studio"), studios.join(FieldFormat::delimiterString()));
0491 
0492   QStringList countries;
0493   foreach(const QVariant& country, resultMap_.value(QLatin1String("production_countries")).toList()) {
0494     QString name = mapValue(country.toMap(), "name");
0495     if(name == QLatin1String("United States of America")) {
0496       name = QStringLiteral("USA");
0497     }
0498     countries << name;
0499   }
0500   if(countries.isEmpty()) {
0501     foreach(const QVariant& country, resultMap_.value(QLatin1String("origin_country")).toList()) {
0502       QString name = country.toString();
0503       if(name == QLatin1String("United States of America") || name == QLatin1String("US")) {
0504         name = QStringLiteral("USA");
0505       }
0506       if(!name.isEmpty()) countries << name;
0507     }
0508   }
0509   entry_->setField(QStringLiteral("nationality"), countries.join(FieldFormat::delimiterString()));
0510 
0511   QStringList genres;
0512   foreach(const QVariant& genre, resultMap_.value(QLatin1String("genres")).toList()) {
0513     genres << mapValue(genre.toMap(), "name");
0514   }
0515   entry_->setField(QStringLiteral("genre"), genres.join(FieldFormat::delimiterString()));
0516 
0517   // hard-coded poster size for now
0518   const QString cover = m_imageBase + QLatin1String("w342") + mapValue(resultMap_, "poster_path");
0519   entry_->setField(QStringLiteral("cover"), cover);
0520 
0521   entry_->setField(QStringLiteral("running-time"), mapValue(resultMap_, "runtime"));
0522   QString lang = mapValue(resultMap_, "original_language");
0523 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5,55,0)
0524   const QString langName = KLanguageName::nameForCode(lang);
0525   if(!langName.isEmpty()) lang = langName;
0526   if(lang == QLatin1String("US English")) lang = QLatin1String("English");
0527 #else
0528   if(lang == QLatin1String("en")) lang = QStringLiteral("English");
0529 #endif
0530   entry_->setField(QStringLiteral("language"), lang);
0531   entry_->setField(QStringLiteral("plot"), mapValue(resultMap_, "overview"));
0532 }
0533 
0534 void TheMovieDBFetcher::readConfiguration() {
0535   QUrl u(QString::fromLatin1(THEMOVIEDB_API_URL));
0536   u.setPath(QStringLiteral("/%1/configuration").arg(QLatin1String(THEMOVIEDB_API_VERSION)));
0537   QUrlQuery q;
0538   q.addQueryItem(QStringLiteral("api_key"), m_apiKey);
0539   u.setQuery(q);
0540 
0541   QByteArray data = FileHandler::readDataFile(u, true);
0542 #if 0
0543   myWarning() << "Remove debug3 from themoviedbfetcher.cpp";
0544   QFile f(QString::fromLatin1("/tmp/test3.json"));
0545   if(f.open(QIODevice::WriteOnly)) {
0546     QTextStream t(&f);
0547     t.setCodec("UTF-8");
0548     t << data;
0549   }
0550   f.close();
0551 #endif
0552 
0553   QJsonDocument doc = QJsonDocument::fromJson(data);
0554   QVariantMap resultMap = doc.object().toVariantMap();
0555 
0556   m_imageBase = mapValue(resultMap.value(QStringLiteral("images")).toMap(), "base_url");
0557   m_serverConfigDate = QDate::currentDate();
0558 }
0559 
0560 Tellico::Fetch::ConfigWidget* TheMovieDBFetcher::configWidget(QWidget* parent_) const {
0561   return new TheMovieDBFetcher::ConfigWidget(parent_, this);
0562 }
0563 
0564 QString TheMovieDBFetcher::defaultName() {
0565   return QStringLiteral("The Movie DB (TMDb)");
0566 }
0567 
0568 QString TheMovieDBFetcher::defaultIcon() {
0569   return favIcon("https://www.themoviedb.org");
0570 }
0571 
0572 Tellico::StringHash TheMovieDBFetcher::allOptionalFields() {
0573   StringHash hash;
0574   hash[QStringLiteral("tmdb")] = i18n("TMDb Link");
0575   hash[QStringLiteral("imdb")] = i18n("IMDb Link");
0576   hash[QStringLiteral("alttitle")] = i18n("Alternative Titles");
0577   hash[QStringLiteral("origtitle")] = i18n("Original Title");
0578   hash[QStringLiteral("network")] = i18n("Network");
0579   hash[QStringLiteral("episode")] = i18n("Episodes");
0580   return hash;
0581 }
0582 
0583 TheMovieDBFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const TheMovieDBFetcher* fetcher_)
0584     : Fetch::ConfigWidget(parent_) {
0585   QGridLayout* l = new QGridLayout(optionsWidget());
0586   l->setSpacing(4);
0587   l->setColumnStretch(1, 10);
0588 
0589   int row = -1;
0590 
0591   QLabel* al = new QLabel(i18n("Registration is required for accessing this data source. "
0592                                "If you agree to the terms and conditions, <a href='%1'>sign "
0593                                "up for an account</a>, and enter your information below.",
0594                                 QLatin1String("http://api.themoviedb.org")),
0595                           optionsWidget());
0596   al->setOpenExternalLinks(true);
0597   al->setWordWrap(true);
0598   ++row;
0599   l->addWidget(al, row, 0, 1, 2);
0600   // richtext gets weird with size
0601   al->setMinimumWidth(al->sizeHint().width());
0602 
0603   QLabel* label = new QLabel(i18n("Access key: "), optionsWidget());
0604   l->addWidget(label, ++row, 0);
0605 
0606   m_apiKeyEdit = new QLineEdit(optionsWidget());
0607   connect(m_apiKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified);
0608   l->addWidget(m_apiKeyEdit, row, 1);
0609   QString w = i18n("The default Tellico key may be used, but searching may fail due to reaching access limits.");
0610   label->setWhatsThis(w);
0611   m_apiKeyEdit->setWhatsThis(w);
0612   label->setBuddy(m_apiKeyEdit);
0613 
0614   label = new QLabel(i18n("&Maximum cast: "), optionsWidget());
0615   l->addWidget(label, ++row, 0);
0616   m_numCast = new QSpinBox(optionsWidget());
0617   m_numCast->setMaximum(99);
0618   m_numCast->setMinimum(0);
0619   m_numCast->setValue(THEMOVIEDB_DEFAULT_CAST_SIZE);
0620 #if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0))
0621   void (QSpinBox::* textChanged)(const QString&) = &QSpinBox::valueChanged;
0622 #else
0623   void (QSpinBox::* textChanged)(const QString&) = &QSpinBox::textChanged;
0624 #endif
0625   connect(m_numCast, textChanged, this, &ConfigWidget::slotSetModified);
0626   l->addWidget(m_numCast, row, 1);
0627   w = i18n("The list of cast members may include many people. Set the maximum number returned from the search.");
0628   label->setWhatsThis(w);
0629   m_numCast->setWhatsThis(w);
0630   label->setBuddy(m_numCast);
0631 
0632   label = new QLabel(i18n("Language: "), optionsWidget());
0633   l->addWidget(label, ++row, 0);
0634   m_langCombo = new GUI::ComboBox(optionsWidget());
0635   // check https://www.themoviedb.org/contribute occasionally for top languages
0636   QIcon iconCN(QStandardPaths::locate(QStandardPaths::GenericDataLocation,
0637                                       QStringLiteral("kf5/locale/countries/cn/flag.png")));
0638   m_langCombo->addItem(iconCN, i18nc("Language", "Chinese"), QLatin1String("cn"));
0639   QIcon iconUS(QStandardPaths::locate(QStandardPaths::GenericDataLocation,
0640                                       QStringLiteral("kf5/locale/countries/us/flag.png")));
0641   m_langCombo->addItem(iconUS, i18nc("Language", "English"), QLatin1String("en"));
0642   QIcon iconFR(QStandardPaths::locate(QStandardPaths::GenericDataLocation,
0643                                       QStringLiteral("kf5/locale/countries/fr/flag.png")));
0644   m_langCombo->addItem(iconFR, i18nc("Language", "French"), QLatin1String("fr"));
0645   QIcon iconDE(QStandardPaths::locate(QStandardPaths::GenericDataLocation,
0646                                       QStringLiteral("kf5/locale/countries/de/flag.png")));
0647   m_langCombo->addItem(iconDE, i18nc("Language", "German"), QLatin1String("de"));
0648   QIcon iconES(QStandardPaths::locate(QStandardPaths::GenericDataLocation,
0649                                       QStringLiteral("kf5/locale/countries/es/flag.png")));
0650   m_langCombo->addItem(iconES, i18nc("Language", "Spanish"), QLatin1String("es"));
0651   QIcon iconRU(QStandardPaths::locate(QStandardPaths::GenericDataLocation,
0652                                       QStringLiteral("kf5/locale/countries/ru/flag.png")));
0653   m_langCombo->addItem(iconRU, i18nc("Language", "Russian"), QLatin1String("ru"));
0654   m_langCombo->setEditable(true);
0655   m_langCombo->setCurrentData(QLatin1String("en"));
0656   void (GUI::ComboBox::* activatedInt)(int) = &GUI::ComboBox::activated;
0657   connect(m_langCombo, activatedInt, this, &ConfigWidget::slotSetModified);
0658   connect(m_langCombo, activatedInt, this, &ConfigWidget::slotLangChanged);
0659   l->addWidget(m_langCombo, row, 1);
0660   label->setBuddy(m_langCombo);
0661 
0662   l->setRowStretch(++row, 10);
0663 
0664   // now add additional fields widget
0665   addFieldsWidget(TheMovieDBFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList());
0666 
0667   if(fetcher_) {
0668     // only show the key if it is not the default Tellico one...
0669     // that way the user is prompted to apply for their own
0670     if(fetcher_->m_apiKey != QLatin1String(THEMOVIEDB_API_KEY)) {
0671       m_apiKeyEdit->setText(fetcher_->m_apiKey);
0672     }
0673     if(m_langCombo->findData(fetcher_->m_locale) == -1) {
0674       m_langCombo->addItem(fetcher_->m_locale, fetcher_->m_locale);
0675     }
0676     m_langCombo->setCurrentData(fetcher_->m_locale);
0677     m_numCast->setValue(fetcher_->m_numCast);
0678   }
0679 }
0680 
0681 void TheMovieDBFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) {
0682   const QString apiKey = m_apiKeyEdit->text().trimmed();
0683   if(!apiKey.isEmpty()) {
0684     config_.writeEntry("API Key", apiKey);
0685   }
0686   QString lang = m_langCombo->currentData().toString();
0687   if(lang.isEmpty()) {
0688     // user-entered format will not have data set for the item. Just use the text itself
0689     lang = m_langCombo->currentText().trimmed();
0690   }
0691   config_.writeEntry("Locale", lang);
0692   config_.writeEntry("Max Cast", m_numCast->value());
0693 }
0694 
0695 QString TheMovieDBFetcher::ConfigWidget::preferredName() const {
0696   return i18n("TheMovieDB (%1)", m_langCombo->currentText());
0697 }
0698 
0699 void TheMovieDBFetcher::ConfigWidget::slotLangChanged() {
0700   emit signalName(preferredName());
0701 }