File indexing completed on 2024-05-12 16:46:01

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