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

0001 /***************************************************************************
0002     Copyright (C) 2004-2009 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 "imdbfetcher.h"
0026 #include "../collections/videocollection.h"
0027 #include "../entry.h"
0028 #include "../field.h"
0029 #include "../fieldformat.h"
0030 #include "../images/imagefactory.h"
0031 #include "../utils/mapvalue.h"
0032 #include "../utils/guiproxy.h"
0033 #include "../gui/combobox.h"
0034 #include "../gui/lineedit.h"
0035 #include "../tellico_debug.h"
0036 
0037 #include <KLocalizedString>
0038 #include <KConfigGroup>
0039 #include <KIO/Job>
0040 #include <KJobUiDelegate>
0041 #include <KAcceleratorManager>
0042 #include <KJobWidgets/KJobWidgets>
0043 
0044 #include <QSpinBox>
0045 #include <QFile>
0046 #include <QMap>
0047 #include <QLabel>
0048 #include <QRadioButton>
0049 #include <QGroupBox>
0050 #include <QButtonGroup>
0051 #include <QGridLayout>
0052 #include <QJsonDocument>
0053 #include <QJsonParseError>
0054 #include <QJsonObject>
0055 #include <QRegularExpression>
0056 
0057 namespace {
0058   static const uint IMDB_DEFAULT_CAST_SIZE = 10;
0059 }
0060 
0061 using namespace Tellico;
0062 using Tellico::Fetch::IMDBFetcher;
0063 
0064 IMDBFetcher::IMDBFetcher(QObject* parent_) : Fetcher(parent_),
0065     m_job(nullptr), m_started(false), m_imageSize(MediumImage),
0066     m_numCast(IMDB_DEFAULT_CAST_SIZE), m_useSystemLocale(true) {
0067 }
0068 
0069 IMDBFetcher::~IMDBFetcher() = default;
0070 
0071 QString IMDBFetcher::source() const {
0072   return m_name.isEmpty() ? defaultName() : m_name;
0073 }
0074 
0075 bool IMDBFetcher::canFetch(int type) const {
0076   return type == Data::Collection::Video;
0077 }
0078 
0079 // imdb can search title only
0080 bool IMDBFetcher::canSearch(Fetch::FetchKey k) const {
0081   // Raw searches are intended to be the imdb url
0082   return k == Title || k == Raw;
0083 }
0084 
0085 void IMDBFetcher::readConfigHook(const KConfigGroup& config_) {
0086   m_numCast = config_.readEntry("Max Cast", IMDB_DEFAULT_CAST_SIZE);
0087   const int imageSize = config_.readEntry("Image Size", -1);
0088   if(imageSize > -1) {
0089     m_imageSize = static_cast<ImageSize>(imageSize);
0090   }
0091   m_useSystemLocale = config_.readEntry("System Locale", true);
0092   m_customLocale = config_.readEntry("Custom Locale");
0093 }
0094 
0095 // multiple values not supported
0096 void IMDBFetcher::search() {
0097   m_started = true;
0098   m_matches.clear();
0099 
0100   QString operationName, query;
0101   QJsonObject vars;
0102   switch(request().key()) {
0103     case Title:
0104       operationName = QLatin1String("Search");
0105       query = searchQuery();
0106       vars.insert(QLatin1String("searchTerms"), request().value());
0107       break;
0108 
0109     case Raw:
0110       {
0111         // expect a url that ends with the tt id
0112         QRegularExpression ttEndRx(QStringLiteral("/(tt\\d+)/?$"));
0113         auto match = ttEndRx.match(request().value());
0114         if(match.hasMatch()) {
0115           operationName = QLatin1String("TitleFull");
0116           query = titleQuery();
0117           vars.insert(QLatin1String("id"), match.captured(1));
0118         } else {
0119           // fallback to a general search
0120           myDebug() << "bad url";
0121           operationName = QLatin1String("Search");
0122           query = searchQuery();
0123           vars.insert(QLatin1String("searchTerms"), request().value());
0124         }
0125       }
0126       break;
0127 
0128     default:
0129       myWarning() << source() << "- key not recognized:" << request().key();
0130       stop();
0131       return;
0132   }
0133 
0134   QJsonObject payload;
0135   payload.insert(QLatin1String("operationName"), operationName);
0136   payload.insert(QLatin1String("query"), query);
0137   payload.insert(QLatin1String("variables"), vars);
0138 
0139   m_job = KIO::storedHttpPost(QJsonDocument(payload).toJson(),
0140                               QUrl(QLatin1String("https://api.graphql.imdb.com")),
0141                               KIO::HideProgressInfo);
0142   configureJob(m_job);
0143   connect(m_job.data(), &KJob::result,
0144           this, &IMDBFetcher::slotComplete);
0145 }
0146 
0147 void IMDBFetcher::stop() {
0148   if(!m_started) {
0149     return;
0150   }
0151   if(m_job) {
0152     m_job->kill();
0153     m_job = nullptr;
0154   }
0155 
0156   m_started = false;
0157 
0158   emit signalDone(this);
0159 }
0160 
0161 void IMDBFetcher::slotComplete(KJob*) {
0162   if(m_job->error()) {
0163     myDebug() << m_job->errorString();
0164     m_job->uiDelegate()->showErrorMessage();
0165     stop();
0166     return;
0167   }
0168 
0169   const auto data = m_job->data();
0170   if(data.isEmpty()) {
0171     myDebug() << "IMDB - no data";
0172     stop();
0173     return;
0174   }
0175   m_job = nullptr;
0176 
0177 #if 0
0178   myWarning() << "Remove JSON debug from imdbfetcher.cpp";
0179   QFile f(QString::fromLatin1("/tmp/imdb-graphql-search.json"));
0180   if(f.open(QIODevice::WriteOnly)) {
0181     QTextStream t(&f);
0182     t.setCodec("UTF-8");
0183     t << QString::fromUtf8(data.constData(), data.size());
0184   }
0185   f.close();
0186 #endif
0187 
0188   // for Raw searches, the result should be a single title
0189   if(request().key() == Raw) {
0190     auto entry = parseResult(data);
0191     if(entry) {
0192       FetchResult* r = new FetchResult(this, entry);
0193       m_entries.insert(r->uid, entry);
0194       emit signalResultFound(r);
0195     }
0196     stop();
0197     return;
0198   }
0199 
0200   QJsonParseError jsonError;
0201   QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
0202   if(doc.isNull()) {
0203     myDebug() << "null JSON document:" << jsonError.errorString();
0204     message(jsonError.errorString(), MessageHandler::Error);
0205   }
0206 
0207   const auto objectMap = doc.object()
0208                             .value(QLatin1String("data")).toObject()
0209                             .value(QLatin1String("mainSearch")).toObject().toVariantMap();
0210   auto list = objectMap.value(QLatin1String("edges")).toList();
0211   for(const auto& edge: qAsConst(list)) {
0212     const auto map = edge.toMap().value(QLatin1String("node"))
0213                          .toMap().value(QLatin1String("entity")).toMap();
0214     const auto id = mapValue(map, "id");
0215     const auto title = mapValue(map, "titleText", "text");
0216     const auto year = mapValue(map, "releaseYear", "year");
0217 
0218     FetchResult* r = new FetchResult(this, title, year);
0219     m_matches.insert(r->uid, id);
0220     m_titleTypes.insert(r->uid, mapValue(map, "titleType", "text"));
0221     emit signalResultFound(r);
0222   }
0223 
0224   stop();
0225 }
0226 
0227 Tellico::Data::EntryPtr IMDBFetcher::fetchEntryHook(uint uid_) {
0228   // if we already grabbed this one, then just pull it out of the dict
0229   Data::EntryPtr entry = m_entries[uid_];
0230   if(entry) {
0231     return entry;
0232   }
0233   if(!m_matches.contains(uid_)) {
0234     myDebug() << "no id match for" << uid_;
0235     return entry;
0236   }
0237 
0238   entry = readGraphQL(m_matches.value(uid_), m_titleTypes.value(uid_));
0239   if(entry) {
0240     m_entries.insert(uid_, entry); // keep for later
0241   }
0242 
0243   return entry;
0244 }
0245 
0246 Tellico::Fetch::FetchRequest IMDBFetcher::updateRequest(Data::EntryPtr entry_) {
0247   QUrl link = QUrl::fromUserInput(entry_->field(QStringLiteral("imdb")));
0248 
0249   if(!link.isEmpty() && link.isValid()) {
0250     return FetchRequest(Fetch::Raw, link.url());
0251   }
0252 
0253   // optimistically try searching for title and rely on Collection::sameEntry() to figure things out
0254   const QString t = entry_->field(QStringLiteral("title"));
0255   if(!t.isEmpty()) {
0256     return FetchRequest(Fetch::Title, t);
0257   }
0258   return FetchRequest();
0259 }
0260 
0261 void IMDBFetcher::configureJob(QPointer<KIO::StoredTransferJob> job_) {
0262   KJobWidgets::setWindow(job_, GUI::Proxy::widget());
0263   job_->addMetaData(QStringLiteral("content-type"), QStringLiteral("Content-Type: application/json"));
0264   job_->addMetaData(QStringLiteral("accept"), QStringLiteral("application/json"));
0265   job_->addMetaData(QStringLiteral("origin"), QLatin1String("https://www.imdb.com"));
0266   QStringList headers;
0267   headers += QStringLiteral("x-imdb-client-name: imdb-web-next-localized");
0268 
0269   QString localeName;
0270   if(m_useSystemLocale || m_customLocale.isEmpty()) {
0271     // use default locale instead of system in case it was changed
0272     localeName = QLocale().name();
0273     myLog() << "Using system locale:" << localeName;
0274   } else {
0275     localeName = m_customLocale;
0276     myLog() << "Using custom locale:" << localeName;
0277   }
0278   localeName.replace(QLatin1Char('_'), QLatin1Char('-'));
0279 
0280   job_->addMetaData(QStringLiteral("Languages"), localeName);
0281   headers += QStringLiteral("x-imdb-user-country: %1").arg(localeName.section(QLatin1Char('-'), 1, 1));
0282 
0283   job_->addMetaData(QStringLiteral("customHTTPHeader"), headers.join(QLatin1String("\r\n")));
0284 }
0285 
0286 Tellico::Data::EntryPtr IMDBFetcher::readGraphQL(const QString& imdbId_, const QString& titleType_) {
0287   const auto query = titleType_ == QLatin1String("TV Series") ? episodeQuery() : titleQuery();
0288   QJsonObject vars;
0289   vars.insert(QLatin1String("id"), imdbId_);
0290 
0291   QJsonObject payload;
0292   payload.insert(QLatin1String("operationName"), QLatin1String("TitleFull"));
0293   payload.insert(QLatin1String("query"), query);
0294   payload.insert(QLatin1String("variables"), vars);
0295 
0296   QPointer<KIO::StoredTransferJob> job = KIO::storedHttpPost(QJsonDocument(payload).toJson(),
0297                                                              QUrl(QLatin1String("https://api.graphql.imdb.com")),
0298                                                              KIO::HideProgressInfo);
0299   configureJob(job);
0300 
0301   if(!job->exec()) {
0302     myDebug() << "IMDB: graphql failure";
0303     myDebug() << job->errorString();
0304     return Data::EntryPtr();
0305   }
0306 
0307   const auto data = job->data();
0308 #if 0
0309   myWarning() << "Remove JSON debug from imdbfetcher.cpp";
0310   QFile f(QString::fromLatin1("/tmp/imdb-graphql-title.json"));
0311   if(f.open(QIODevice::WriteOnly)) {
0312     QTextStream t(&f);
0313     t.setCodec("UTF-8");
0314     t << QString::fromUtf8(data.constData(), data.size());
0315   }
0316   f.close();
0317 #endif
0318   return parseResult(data);
0319 }
0320 
0321 Tellico::Data::EntryPtr IMDBFetcher::parseResult(const QByteArray& data_) {
0322   QJsonParseError jsonError;
0323   QJsonDocument doc = QJsonDocument::fromJson(data_, &jsonError);
0324   if(doc.isNull()) {
0325     myDebug() << "null JSON document:" << jsonError.errorString();
0326     message(jsonError.errorString(), MessageHandler::Error);
0327     return Data::EntryPtr();
0328   }
0329   Data::CollPtr coll(new Data::VideoCollection(true));
0330   Data::EntryPtr entry(new Data::Entry(coll));
0331   const auto objectMap = doc.object()
0332                             .value(QLatin1String("data")).toObject()
0333                             .value(QLatin1String("title")).toObject().toVariantMap();
0334   entry->setField(QStringLiteral("title"), mapValue(objectMap, "titleText", "text"));
0335   entry->setField(QStringLiteral("year"), mapValue(objectMap, "releaseYear", "year"));
0336   entry->setField(QStringLiteral("language"), mapValue(objectMap, "spokenLanguages", "spokenLanguages"));
0337   entry->setField(QStringLiteral("plot"), mapValue(objectMap, "plot", "plotText", "plainText"));
0338   entry->setField(QStringLiteral("genre"), mapValue(objectMap, "genres", "genres", "text"));
0339   entry->setField(QStringLiteral("nationality"), mapValue(objectMap, "countriesOfOrigin", "countries", "text"));
0340   entry->setField(QStringLiteral("audio-track"), mapValue(objectMap, "technicalSpecifications", "soundMixes", "items", "text"));
0341   entry->setField(QStringLiteral("aspect-ratio"), mapValue(objectMap, "technicalSpecifications", "aspectRatios", "items", "aspectRatio"));
0342   entry->setField(QStringLiteral("color"), mapValue(objectMap, "technicalSpecifications", "colorations", "items", "text"));
0343   const int runTime = mapValue(objectMap, "runtime", "seconds").toInt();
0344   if(runTime > 0) {
0345     entry->setField(QStringLiteral("running-time"), QString::number(runTime/60));
0346   }
0347   entry->setField(QStringLiteral("language"), mapValue(objectMap, "spokenLanguages", "spokenLanguages", "text"));
0348   entry->setField(QStringLiteral("plot"), mapValue(objectMap, "plot", "plotText", "plainText"));
0349 
0350   if(m_imageSize != NoImage) {
0351     QUrl imageUrl(mapValue(objectMap, "primaryImage", "url"));
0352     // LargeImage just means use default available size
0353     if(m_imageSize != LargeImage) {
0354       // limit to 256 for small and 640 for medium
0355       const int maxDim = m_imageSize == SmallImage ? 256 : 640;
0356       const auto imageWidth = mapValue(objectMap, "primaryImage", "width").toFloat();
0357       const auto imageHeight = mapValue(objectMap, "primaryImage", "height").toFloat();
0358       const auto ratio = imageWidth/imageHeight;
0359       int newWidth, newHeight;
0360       if(ratio < 1) {
0361         newWidth = ratio*maxDim;
0362         newHeight = maxDim;
0363       } else {
0364         newWidth = maxDim;
0365         newHeight = ratio*maxDim;
0366       }
0367       auto param = QStringLiteral("QL75_SX%1_CR0,0,%1,%2_.jpg").arg(newWidth).arg(newHeight);
0368       imageUrl.setPath(imageUrl.path().replace(QLatin1String(".jpg"), param));
0369     }
0370     entry->setField(QStringLiteral("cover"), ImageFactory::addImage(imageUrl, true));
0371   }
0372 
0373   QStringList studios;
0374   auto list = objectMap.value(QLatin1String("companyCredits")).toMap().value(QLatin1String("edges")).toList();
0375   for(const auto& edge: qAsConst(list)) {
0376     studios += mapValue(edge.toMap(), "node", "company", "companyText", "text");
0377   }
0378   entry->setField(QStringLiteral("studio"), studios.join(FieldFormat::delimiterString()));
0379 
0380   const QString certification(QStringLiteral("certification"));
0381   QString cert = mapValue(objectMap, "certificate", "rating");
0382   if(!cert.isEmpty()) {
0383     // set default certification, assuming US for now
0384     if(cert == QLatin1String("Not Rated")) {
0385       cert = QLatin1Char('U');
0386     }
0387     const QString certCountry = mapValue(objectMap, "certificate", "country", "text");
0388     if(certCountry == QLatin1String("United States")) {
0389       cert += QStringLiteral(" (USA)");
0390     } else if(!certCountry.isEmpty()) {
0391       cert += QStringLiteral(" (%1)").arg(certCountry);
0392     }
0393     const QStringList& certsAllowed = coll->fieldByName(certification)->allowed();
0394     if(certsAllowed.contains(cert)) {
0395       entry->setField(certification, cert);
0396     } else {
0397       myLog() << "Skipping certification as not allowed:" << cert;
0398     }
0399   }
0400 
0401   QStringList directors;
0402   list = objectMap.value(QLatin1String("principalDirectors")).toList();
0403   for(const auto& director: qAsConst(list)) {
0404     directors += mapValue(director.toMap(), "credits", "name", "nameText", "text");
0405   }
0406   // favor principalDirectors over all the directors, but episodes may be directors only
0407   if(list.isEmpty()) {
0408     list = objectMap.value(QLatin1String("directors")).toMap().value(QLatin1String("edges")).toList();
0409     for(const auto& edge: qAsConst(list)) {
0410       directors += mapValue(edge.toMap(), "node", "name", "nameText", "text");
0411     }
0412   }
0413   entry->setField(QStringLiteral("director"), directors.join(FieldFormat::delimiterString()));
0414 
0415   QStringList producers;
0416   list = objectMap.value(QLatin1String("producers")).toMap().value(QLatin1String("edges")).toList();
0417   for(const auto& edge: qAsConst(list)) {
0418     producers += mapValue(edge.toMap(), "node", "name", "nameText", "text");
0419   }
0420   entry->setField(QStringLiteral("producer"), producers.join(FieldFormat::delimiterString()));
0421 
0422   QStringList composers;
0423   list = objectMap.value(QLatin1String("composers")).toMap().value(QLatin1String("edges")).toList();
0424   for(const auto& edge: qAsConst(list)) {
0425     composers += mapValue(edge.toMap(), "node", "name", "nameText", "text");
0426   }
0427   entry->setField(QStringLiteral("composer"), composers.join(FieldFormat::delimiterString()));
0428 
0429   QStringList writers;
0430   list = objectMap.value(QLatin1String("writers")).toMap().value(QLatin1String("edges")).toList();
0431   for(const auto& edge: qAsConst(list)) {
0432     writers += mapValue(edge.toMap(), "node", "name", "nameText", "text");
0433   }
0434   entry->setField(QStringLiteral("writer"), writers.join(FieldFormat::delimiterString()));
0435 
0436   QStringList cast;
0437   list = objectMap.value(QLatin1String("cast")).toMap().value(QLatin1String("edges")).toList();
0438   for(const auto& edge: qAsConst(list)) {
0439     const auto map = edge.toMap().value(QLatin1String("node")).toMap();
0440     cast += mapValue(map, "name", "nameText", "text")
0441           + FieldFormat::columnDelimiterString()
0442           + mapValue(map, "characters", "name");
0443     if(cast.count() >= m_numCast) {
0444       break;
0445     }
0446   }
0447   entry->setField(QStringLiteral("cast"), cast.join(FieldFormat::rowDelimiterString()));
0448 
0449   const QString imdb(QStringLiteral("imdb"));
0450   if(!coll->hasField(imdb) && optionalFields().contains(imdb)) {
0451     coll->addField(Data::Field::createDefaultField(Data::Field::ImdbField));
0452   }
0453   if(coll->hasField(imdb) && coll->fieldByName(imdb)->type() == Data::Field::URL) {
0454     entry->setField(imdb, mapValue(objectMap, "canonicalUrl"));
0455   }
0456 
0457   const QString imdbRating(QStringLiteral("imdb-rating"));
0458   if(optionalFields().contains(imdbRating)) {
0459     if(!coll->hasField(imdbRating)) {
0460       Data::FieldPtr f(new Data::Field(imdbRating, i18n("IMDb Rating"), Data::Field::Rating));
0461       f->setCategory(i18n("General"));
0462       f->setProperty(QStringLiteral("maximum"), QStringLiteral("10"));
0463       coll->addField(f);
0464     }
0465     const auto value = objectMap.value(QLatin1String("ratingsSummary")).toMap()
0466                                 .value(QLatin1String("aggregateRating")).toFloat();
0467     entry->setField(imdbRating, QString::number(value));
0468   }
0469 
0470   const QString origtitle(QStringLiteral("origtitle"));
0471   if(optionalFields().contains(origtitle)) {
0472     Data::FieldPtr f(new Data::Field(origtitle, i18n("Original Title")));
0473     f->setFormatType(FieldFormat::FormatTitle);
0474     coll->addField(f);
0475     entry->setField(origtitle, mapValue(objectMap, "originalTitleText", "text"));
0476   }
0477 
0478   const QString alttitle(QStringLiteral("alttitle"));
0479   if(optionalFields().contains(alttitle)) {
0480     Data::FieldPtr f(new Data::Field(alttitle, i18n("Alternative Titles"), Data::Field::Table));
0481     f->setFormatType(FieldFormat::FormatTitle);
0482     coll->addField(f);
0483     QStringList akas;
0484     list = objectMap.value(QLatin1String("akas")).toMap().value(QLatin1String("edges")).toList();
0485     for(const auto& edge: qAsConst(list)) {
0486       akas += mapValue(edge.toMap(), "node", "text");
0487     }
0488     akas.removeDuplicates();
0489     entry->setField(alttitle, akas.join(FieldFormat::rowDelimiterString()));
0490   }
0491 
0492   const QString episode(QStringLiteral("episode"));
0493   if(mapValue(objectMap, "titleType", "text") == QLatin1String("TV Series") &&
0494      optionalFields().contains(episode)) {
0495     coll->addField(Data::Field::createDefaultField(Data::Field::EpisodeField));
0496     QStringList episodes;
0497     list = objectMap.value(QLatin1String("episodes")).toMap()
0498                     .value(QLatin1String("episodes")).toMap()
0499                     .value(QLatin1String("edges")).toList();
0500     for(const auto& edge: qAsConst(list)) {
0501       const auto nodeMap = edge.toMap().value(QLatin1String("node")).toMap();
0502       QString row = mapValue(nodeMap, "titleText", "text");
0503       const auto seriesMap = nodeMap.value(QLatin1String("series")).toMap();
0504       // future episodes have a "Episode #" start
0505       if(!row.startsWith(QLatin1String("Episode #")) &&
0506          seriesMap.contains(QLatin1String("displayableEpisodeNumber"))) {
0507         row += FieldFormat::columnDelimiterString() + mapValue(seriesMap, "displayableEpisodeNumber", "displayableSeason", "text")
0508              + FieldFormat::columnDelimiterString() + mapValue(seriesMap, "displayableEpisodeNumber", "episodeNumber", "text");
0509       }
0510       episodes += row;
0511     }
0512     entry->setField(episode, episodes.join(FieldFormat::rowDelimiterString()));
0513   }
0514 
0515   return entry;
0516 }
0517 
0518 QString IMDBFetcher::defaultName() {
0519   return i18n("Internet Movie Database");
0520 }
0521 
0522 QString IMDBFetcher::defaultIcon() {
0523   return favIcon(QUrl(QLatin1String("https://www.imdb.com")),
0524                  QUrl(QLatin1String("https://m.media-amazon.com/images/G/01/imdb/images-ANDW73HA/favicon_desktop_32x32._CB1582158068_.png")));
0525 }
0526 
0527 //static
0528 Tellico::StringHash IMDBFetcher::allOptionalFields() {
0529   StringHash hash;
0530   hash[QStringLiteral("imdb")]             = i18n("IMDb Link");
0531   hash[QStringLiteral("imdb-rating")]      = i18n("IMDb Rating");
0532   hash[QStringLiteral("alttitle")]         = i18n("Alternative Titles");
0533   hash[QStringLiteral("allcertification")] = i18n("Certifications");
0534   hash[QStringLiteral("origtitle")]        = i18n("Original Title");
0535   hash[QStringLiteral("episode")]          = i18n("Episodes");
0536   return hash;
0537 }
0538 
0539 Tellico::Fetch::ConfigWidget* IMDBFetcher::configWidget(QWidget* parent_) const {
0540   return new IMDBFetcher::ConfigWidget(parent_, this);
0541 }
0542 
0543 IMDBFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const IMDBFetcher* fetcher_/*=0*/)
0544     : Fetch::ConfigWidget(parent_) {
0545   QGridLayout* l = new QGridLayout(optionsWidget());
0546   l->setSpacing(4);
0547   l->setColumnStretch(1, 10);
0548 
0549   int row = -1;
0550 
0551   QLabel* label = new QLabel(i18n("&Maximum cast: "), optionsWidget());
0552   l->addWidget(label, ++row, 0);
0553   m_numCast = new QSpinBox(optionsWidget());
0554   m_numCast->setMaximum(99);
0555   m_numCast->setMinimum(0);
0556   m_numCast->setValue(IMDB_DEFAULT_CAST_SIZE);
0557 #if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0))
0558   void (QSpinBox::* textChanged)(const QString&) = &QSpinBox::valueChanged;
0559 #else
0560   void (QSpinBox::* textChanged)(const QString&) = &QSpinBox::textChanged;
0561 #endif
0562   connect(m_numCast, textChanged, this, &ConfigWidget::slotSetModified);
0563   l->addWidget(m_numCast, row, 1);
0564   QString w = i18n("The list of cast members may include many people. Set the maximum number returned from the search.");
0565   label->setWhatsThis(w);
0566   m_numCast->setWhatsThis(w);
0567   label->setBuddy(m_numCast);
0568 
0569   label = new QLabel(i18n("&Image size: "), optionsWidget());
0570   l->addWidget(label, ++row, 0);
0571   m_imageCombo = new GUI::ComboBox(optionsWidget());
0572   m_imageCombo->addItem(i18n("Small Image"), SmallImage);
0573   m_imageCombo->addItem(i18n("Medium Image"), MediumImage);
0574   m_imageCombo->addItem(i18n("Large Image"), LargeImage);
0575   m_imageCombo->addItem(i18n("No Image"), NoImage);
0576   void (GUI::ComboBox::* activatedInt)(int) = &GUI::ComboBox::activated;
0577   connect(m_imageCombo, activatedInt, this, &ConfigWidget::slotSetModified);
0578   l->addWidget(m_imageCombo, row, 1);
0579   w = i18n("The cover image may be downloaded as well. However, too many large images in the "
0580            "collection may degrade performance.");
0581   label->setWhatsThis(w);
0582   m_imageCombo->setWhatsThis(w);
0583   label->setBuddy(m_imageCombo);
0584 
0585   auto localeGroupBox = new QGroupBox(i18n("Locale"), optionsWidget());
0586   l->addWidget(localeGroupBox, ++row, 0, 1, -1);
0587 
0588   m_systemLocaleRadioButton = new QRadioButton(i18n("Use system locale"), localeGroupBox);
0589   m_customLocaleRadioButton = new QRadioButton(i18n("Use custom locale"), localeGroupBox);
0590   m_customLocaleEdit = new GUI::LineEdit(localeGroupBox);
0591   m_customLocaleEdit->setEnabled(false);
0592 
0593   auto localeGroupLayout = new QGridLayout(localeGroupBox);
0594   localeGroupLayout->addWidget(m_systemLocaleRadioButton, 0, 0);
0595   localeGroupLayout->addWidget(m_customLocaleRadioButton, 1, 0);
0596   localeGroupLayout->addWidget(m_customLocaleEdit, 1, 1);
0597   localeGroupBox->setLayout(localeGroupLayout);
0598 
0599   auto localeGroup = new QButtonGroup(localeGroupBox);
0600   localeGroup->addButton(m_systemLocaleRadioButton, 0 /* id */);
0601   localeGroup->addButton(m_customLocaleRadioButton, 1 /* id */);
0602 #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
0603   void (QButtonGroup::* buttonClicked)(int) = &QButtonGroup::buttonClicked;
0604   connect(localeGroup, buttonClicked, this, &ConfigWidget::slotSetModified);
0605   connect(localeGroup, buttonClicked, this, &ConfigWidget::slotLocaleChanged);
0606 #else
0607   connect(localeGroup, &QButtonGroup::idClicked, this, &ConfigWidget::slotSetModified);
0608   connect(localeGroup, &QButtonGroup::idClicked, this, &ConfigWidget::slotLocaleChanged);
0609 #endif
0610   connect(m_customLocaleEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified);
0611 
0612   l->setRowStretch(++row, 10);
0613 
0614   // now add additional fields widget
0615   addFieldsWidget(IMDBFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList());
0616   KAcceleratorManager::manage(optionsWidget());
0617 
0618   if(fetcher_) {
0619     m_numCast->setValue(fetcher_->m_numCast);
0620     m_imageCombo->setCurrentData(fetcher_->m_imageSize);
0621     if(fetcher_->m_useSystemLocale) {
0622       m_systemLocaleRadioButton->setChecked(true);
0623       m_customLocaleEdit->setText(QLocale().name());
0624     } else {
0625       m_customLocaleRadioButton->setChecked(true);
0626       m_customLocaleEdit->setEnabled(true);
0627       m_customLocaleEdit->setText(fetcher_->m_customLocale);
0628     }
0629   } else { //defaults
0630     m_imageCombo->setCurrentData(MediumImage);
0631   }
0632 }
0633 
0634 void IMDBFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) {
0635   config_.deleteEntry("Host"); // clear old host entry
0636   config_.writeEntry("Max Cast", m_numCast->value());
0637   config_.deleteEntry("Fetch Images"); // no longer used
0638   const int n = m_imageCombo->currentData().toInt();
0639   config_.writeEntry("Image Size", n);
0640   config_.deleteEntry("Lang"); // no longer used
0641   config_.writeEntry("System Locale", m_systemLocaleRadioButton->isChecked());
0642   config_.writeEntry("Custom Locale", m_customLocaleRadioButton->isChecked() ?
0643                                         m_customLocaleEdit->text().trimmed() :
0644                                         QString());
0645 }
0646 
0647 QString IMDBFetcher::ConfigWidget::preferredName() const {
0648   return i18n("Internet Movie Database");
0649 }
0650 
0651 void IMDBFetcher::ConfigWidget::slotLocaleChanged(int id_) {
0652   // id 0 is system locale, 1 is custom locale
0653   m_customLocaleEdit->setEnabled(id_ == 1);
0654 }