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 }