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 }