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

0001 /***************************************************************************
0002     Copyright (C) 2021 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 "thetvdbfetcher.h"
0026 #include "../collections/videocollection.h"
0027 #include "../images/imagefactory.h"
0028 #include "../utils/guiproxy.h"
0029 #include "../utils/string_utils.h"
0030 #include "../utils/mapvalue.h"
0031 #include "../core/tellico_strings.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 <QFile>
0047 #include <QTextStream>
0048 #include <QVBoxLayout>
0049 #include <QTextCodec>
0050 #include <QJsonDocument>
0051 #include <QJsonObject>
0052 #include <QJsonArray>
0053 #include <QUrlQuery>
0054 #include <QLineEdit>
0055 
0056 namespace {
0057   static const int THETVDB_MAX_RETURNS_TOTAL = 20;
0058   static const char* THETVDB_API_URL = "https://api4.thetvdb.com/v4";
0059   static const char* THETVDB_API_KEY = "c0a67445dded5036291dc8fb9ca5d6b33350c1f5610784e0604dc8fcb0d35a3c9bf94a673f5988bcea8cebdf6f423a036c5deedfd6b2b994a8ca9bf9dcbf83e147761023e081ab9f";
0060   static const int THETVDB_TOKEN_EXPIRES = 24*60*60; // expires in 24 hours
0061   static const char* THETVDB_ART_PREFIX = "https://thetvdb.com/banners/";
0062 }
0063 
0064 using namespace Tellico;
0065 using Tellico::Fetch::TheTVDBFetcher;
0066 
0067 TheTVDBFetcher::TheTVDBFetcher(QObject* parent_)
0068     : Fetcher(parent_)
0069     , m_started(false) {
0070   m_apiKey = Tellico::reverseObfuscate(THETVDB_API_KEY);
0071 }
0072 
0073 TheTVDBFetcher::~TheTVDBFetcher() {
0074 }
0075 
0076 QString TheTVDBFetcher::source() const {
0077   return m_name.isEmpty() ? defaultName() : m_name;
0078 }
0079 
0080 QString TheTVDBFetcher::attribution() const {
0081   return i18n(providedBy, QLatin1String("https://thetvdb.com"), defaultName());
0082 }
0083 
0084 bool TheTVDBFetcher::canSearch(Fetch::FetchKey k) const {
0085   return k == Title;
0086 }
0087 
0088 bool TheTVDBFetcher::canFetch(int type) const {
0089   return type == Data::Collection::Video;
0090 }
0091 
0092 void TheTVDBFetcher::readConfigHook(const KConfigGroup& config_) {
0093   QString k = config_.readEntry("API Key");
0094   if(!k.isEmpty()) {
0095     // the API key used to be saved in the config
0096     // now in API v4, the API Key is unique to the application and the API PIN is user-specific
0097     // the name of the config option was kept the same
0098     m_apiPin = k;
0099   }
0100   k = config_.readEntry("Access Token");
0101   if(!k.isEmpty()) {
0102     m_accessToken = k;
0103   }
0104   if(!m_accessToken.isEmpty()) {
0105     m_accessTokenExpires = config_.readEntry("Access Token Expires", QDateTime());
0106   }
0107 }
0108 
0109 void TheTVDBFetcher::saveConfigHook(KConfigGroup& config_) {
0110   config_.writeEntry("Access Token", m_accessToken);
0111   config_.writeEntry("Access Token Expires", m_accessTokenExpires);
0112 }
0113 
0114 void TheTVDBFetcher::search() {
0115   continueSearch();
0116 }
0117 
0118 void TheTVDBFetcher::continueSearch() {
0119   m_started = true;
0120 
0121   QUrl u(QString::fromLatin1(THETVDB_API_URL));
0122   switch(request().key()) {
0123     case Title:
0124       u = u.adjusted(QUrl::StripTrailingSlash);
0125       u.setPath(u.path() + QLatin1String("/search"));
0126       {
0127         QUrlQuery q;
0128         q.addQueryItem(QStringLiteral("type"), QStringLiteral("series"));
0129         q.addQueryItem(QStringLiteral("q"), request().value());
0130         u.setQuery(q);
0131       }
0132       break;
0133 
0134     case Raw:
0135       u = u.adjusted(QUrl::StripTrailingSlash);
0136       u.setPath(u.path() + QLatin1String("/search"));
0137       {
0138         QUrlQuery q;
0139         q.addQueryItem(QStringLiteral("type"), QStringLiteral("series"));
0140         if(request().data() == QLatin1String("imdb")) {
0141           q.addQueryItem(QStringLiteral("imdbId"), request().value());
0142         } else if(request().data() == QLatin1String("slug")) {
0143           q.addQueryItem(QStringLiteral("slug"), request().value());
0144         } else {
0145           myDebug() << source() << "raw data not recognized";
0146           stop();
0147           return;
0148         }
0149         u.setQuery(q);
0150       }
0151       break;
0152 
0153     default:
0154       myWarning() << source() << "- key not recognized:" << request().key();
0155       stop();
0156       return;
0157   }
0158   myLog() << "Reading" << u.toDisplayString();
0159   if(m_apiPin.isEmpty()) {
0160     myDebug() << source() << "- empty API PIN";
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 = getJob(u);
0169   connect(m_job.data(), &KJob::result, this, &TheTVDBFetcher::slotComplete);
0170 }
0171 
0172 void TheTVDBFetcher::stop() {
0173   if(!m_started) {
0174     return;
0175   }
0176   if(m_job) {
0177     m_job->kill();
0178     m_job = nullptr;
0179   }
0180   m_started = false;
0181   emit signalDone(this);
0182 }
0183 
0184 Tellico::Fetch::FetchRequest TheTVDBFetcher::updateRequest(Data::EntryPtr entry_) {
0185   QString imdb = entry_->field(QStringLiteral("imdb"));
0186   if(imdb.isEmpty()) {
0187     imdb = entry_->field(QStringLiteral("imdb-id"));
0188   }
0189   if(!imdb.isEmpty()) {
0190     QRegularExpression ttRx(QStringLiteral("tt\\d+"));
0191     auto ttMatch = ttRx.match(imdb);
0192     if(ttMatch.hasMatch()) {
0193       FetchRequest req(Raw, ttMatch.captured());
0194       req.setData(QLatin1String("imdb")); // tell the request to use imdb criteria
0195       return req;
0196     }
0197   }
0198 
0199   const QString thetvdb = entry_->field(QStringLiteral("thetvdb"));
0200   if(!thetvdb.isEmpty()) {
0201     QRegularExpression slugRx(QStringLiteral("series/(\\w+)"));
0202     auto slugMatch = slugRx.match(thetvdb);
0203     if(slugMatch.hasMatch()) {
0204       FetchRequest req(Raw, slugMatch.captured(1));
0205       req.setData(QLatin1String("slug")); // tell the request to use this as the slug
0206       return req;
0207     }
0208   }
0209 
0210   const QString title = entry_->field(QStringLiteral("title"));
0211   if(!title.isEmpty()) {
0212     return FetchRequest(Title, title);
0213   }
0214   return FetchRequest();
0215 }
0216 
0217 void TheTVDBFetcher::slotComplete(KJob* job_) {
0218   KIO::StoredTransferJob* job = static_cast<KIO::StoredTransferJob*>(job_);
0219 
0220   if(job->error()) {
0221     job->uiDelegate()->showErrorMessage();
0222     stop();
0223     return;
0224   }
0225 
0226   const QByteArray data = job->data();
0227   if(data.isEmpty()) {
0228     myDebug() << "TheTVDB: no data";
0229     stop();
0230     return;
0231   }
0232   // see bug 319662. If fetcher is cancelled, job is killed
0233   // if the pointer is retained, it gets double-deleted
0234   m_job = nullptr;
0235 
0236 #if 0
0237   myWarning() << "Remove debug from thetvdbfetcher.cpp";
0238   QFile f(QStringLiteral("/tmp/test-thetvdb.json"));
0239   if(f.open(QIODevice::WriteOnly)) {
0240     QTextStream t(&f);
0241     t.setCodec("UTF-8");
0242     t << data;
0243   }
0244   f.close();
0245 #endif
0246 
0247   Data::CollPtr coll(new Data::VideoCollection(true));
0248   // always add the thetvdb-id for fetchEntryHook
0249   Data::FieldPtr field(new Data::Field(QStringLiteral("thetvdb-id"), QString(), Data::Field::Line));
0250   field->setCategory(i18n("General"));
0251   coll->addField(field);
0252 
0253   if(optionalFields().contains(QStringLiteral("network"))) {
0254     Data::FieldPtr field(new Data::Field(QStringLiteral("network"), i18n("Network"), Data::Field::Line));
0255     field->setCategory(i18n("General"));
0256     coll->addField(field);
0257   }
0258   if(optionalFields().contains(QStringLiteral("thetvdb"))) {
0259     Data::FieldPtr field(new Data::Field(QStringLiteral("thetvdb"), i18n("TheTVDB Link"), Data::Field::URL));
0260     field->setCategory(i18n("General"));
0261     coll->addField(field);
0262   }
0263   if(optionalFields().contains(QStringLiteral("imdb"))) {
0264     coll->addField(Data::Field::createDefaultField(Data::Field::ImdbField));
0265   }
0266   if(optionalFields().contains(QStringLiteral("episode"))) {
0267     coll->addField(Data::Field::createDefaultField(Data::Field::EpisodeField));
0268   }
0269 
0270   QJsonDocument doc = QJsonDocument::fromJson(data);
0271   const QJsonArray results = doc.object().value(QLatin1String("data")).toArray();
0272 
0273   if(results.isEmpty()) {
0274     myLog() << "No results";
0275     stop();
0276     return;
0277   }
0278 
0279   int count = 0;
0280   foreach(const QJsonValue& result, results) {
0281     Data::EntryPtr entry(new Data::Entry(coll));
0282     populateEntry(entry, result.toObject().toVariantMap(), false);
0283 
0284     FetchResult* r = new FetchResult(this, entry);
0285     m_entries.insert(r->uid, entry);
0286     emit signalResultFound(r);
0287     ++count;
0288     if(count >= THETVDB_MAX_RETURNS_TOTAL) {
0289       break;
0290     }
0291   }
0292 
0293   stop();
0294 }
0295 
0296 Tellico::Data::EntryPtr TheTVDBFetcher::fetchEntryHook(uint uid_) {
0297   Data::EntryPtr entry = m_entries.value(uid_);
0298   if(!entry) {
0299     myWarning() << "no entry in dict";
0300     return Data::EntryPtr();
0301   }
0302 
0303   const QString id = entry->field(QStringLiteral("thetvdb-id"));
0304   if(!id.isEmpty()) {
0305     QUrl url(QString::fromLatin1(THETVDB_API_URL));
0306     url.setPath(url.path() + QStringLiteral("/series/%1/extended").arg(id));
0307     QUrlQuery q;
0308     q.addQueryItem(QStringLiteral("meta"), QStringLiteral("episodes"));
0309     url.setQuery(q);
0310     auto job = getJob(url);
0311     if(!job->exec()) {
0312       myDebug() << job->errorString() << url;
0313       return Data::EntryPtr();
0314     }
0315     QByteArray data = job->data();
0316     if(data.isEmpty()) {
0317       myDebug() << "no data for" << url;
0318       return Data::EntryPtr();
0319     }
0320 #if 0
0321     myWarning() << "Remove debug2 from thetvdbfetcher.cpp";
0322     QFile f(QStringLiteral("/tmp/test2-thetvdb.json"));
0323     if(f.open(QIODevice::WriteOnly)) {
0324       QTextStream t(&f);
0325       t.setCodec("UTF-8");
0326       t << data;
0327     }
0328     f.close();
0329 #endif
0330     QJsonDocument doc = QJsonDocument::fromJson(data);
0331     const QJsonObject dataObject = doc.object().value(QLatin1String("data")).toObject();
0332     populateEntry(entry, dataObject.toVariantMap(), true);
0333     populatePeople(entry, dataObject.value(QLatin1String("characters")).toArray());
0334 
0335     // now episode info
0336     if(optionalFields().contains(QStringLiteral("episode"))) {
0337       populateEpisodes(entry, dataObject.value(QLatin1String("episodes")).toArray());
0338     }
0339   }
0340 
0341   // image might still be a URL
0342   const QString image_id = entry->field(QStringLiteral("cover"));
0343   if(image_id.contains(QLatin1Char('/'))) {
0344     const QString id = ImageFactory::addImage(QUrl::fromUserInput(image_id), true /* quiet */);
0345     if(id.isEmpty()) {
0346       message(i18n("The cover image could not be loaded."), MessageHandler::Warning);
0347     }
0348     // empty image ID is ok
0349     entry->setField(QStringLiteral("cover"), id);
0350   }
0351 
0352   // don't want to include ID field - absence indicates entry is fully populated
0353   entry->setField(QStringLiteral("thetvdb-id"), QString());
0354 
0355   return entry;
0356 }
0357 
0358 void TheTVDBFetcher::populateEntry(Data::EntryPtr entry_, const QVariantMap& resultMap_, bool fullData_) {
0359   entry_->setField(QStringLiteral("thetvdb-id"), mapValue(resultMap_, "tvdb_id"));
0360   entry_->setField(QStringLiteral("title"), mapValue(resultMap_, "name"));
0361   const QString yearString = QStringLiteral("year");
0362   if(entry_->field(yearString).isEmpty()) {
0363     QString year = mapValue(resultMap_, "year");
0364     if(year.isEmpty()) {
0365       year = mapValue(resultMap_, "firstAired");
0366     }
0367     entry_->setField(yearString, year.left(4));
0368   }
0369 
0370   const QString network(QStringLiteral("network"));
0371   if(entry_->collection()->hasField(network) && entry_->field(network).isEmpty()) {
0372     entry_->setField(network, mapValue(resultMap_, "network"));
0373   }
0374   const QString plot(QStringLiteral("plot"));
0375   if(entry_->field(plot).isEmpty()) {
0376     entry_->setField(plot, mapValue(resultMap_, "overview"));
0377   }
0378 
0379   // if we only need cursory data, then we're done
0380   if(!fullData_) {
0381     return;
0382   }
0383 
0384   const QString thetvdb(QStringLiteral("thetvdb"));
0385   if(entry_->collection()->hasField(thetvdb)) {
0386     entry_->setField(thetvdb, QLatin1String("https://thetvdb.com/series/") + mapValue(resultMap_, "slug"));
0387   }
0388 
0389   const QString imdb(QStringLiteral("imdb"));
0390   if(entry_->collection()->hasField(imdb)) {
0391     auto remoteList = resultMap_.value(QLatin1String("remoteIds")).toList();
0392     foreach(const auto& remoteId, remoteList) {
0393       const QVariantMap remoteMap = remoteId.toMap();
0394       if(remoteMap.value(QLatin1String("sourceName")) == QLatin1String("IMDB")) {
0395         entry_->setField(imdb, QLatin1String("https://www.imdb.com/title/") + mapValue(remoteMap, "id"));
0396       }
0397     }
0398   }
0399 
0400   QStringList genres;
0401   foreach(const QVariant& genre, resultMap_.value(QLatin1String("genres")).toList()) {
0402     genres << mapValue(genre.toMap(), "name");
0403   }
0404   entry_->setField(QStringLiteral("genre"), genres.join(FieldFormat::delimiterString()));
0405 
0406   const QString cert = QStringLiteral("certification");
0407   const QString rating = mapValue(resultMap_, "rating");
0408   QStringList allowed = entry_->collection()->fieldByName(cert)->allowed();
0409   if(!rating.isEmpty() && !allowed.contains(rating)) {
0410     allowed << rating;
0411     entry_->collection()->fieldByName(cert)->setAllowed(allowed);
0412     entry_->setField(cert, rating);
0413   }
0414 
0415   QString cover = mapValue(resultMap_, "image");
0416   if(cover.isEmpty()) cover = mapValue(resultMap_, "poster");
0417   if(!cover.startsWith(QLatin1String("http"))) cover.prepend(QLatin1String(THETVDB_ART_PREFIX));
0418   if(!cover.isEmpty()) entry_->setField(QStringLiteral("cover"), cover);
0419 
0420   QString lang = mapValue(resultMap_, "originalLanguage");
0421 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5,55,0)
0422   const QString langName = KLanguageName::nameForCode(lang);
0423   if(!langName.isEmpty()) lang = langName;
0424 #endif
0425   if(lang == QLatin1String("US English") ||
0426      lang == QLatin1String("en") ||
0427      lang == QLatin1String("eng")) {
0428     lang = QLatin1String("English");
0429   }
0430   if(!lang.isEmpty()) entry_->setField(QStringLiteral("language"), lang);
0431 }
0432 
0433 void TheTVDBFetcher::populatePeople(Data::EntryPtr entry_, const QJsonArray& peopleArray_) {
0434   QStringList actors, directors, writers;
0435   foreach(const QJsonValue& person, peopleArray_) {
0436     const QVariantMap personMap = person.toObject().toVariantMap();
0437     const QString personType = mapValue(personMap, "peopleType");
0438     if(personType == QLatin1String("Actor")) {
0439       actors << mapValue(personMap, "personName") + FieldFormat::columnDelimiterString() + mapValue(personMap, "name");
0440     } else if(personType == QLatin1String("Director")) {
0441       directors << mapValue(personMap, "personName");
0442     } else if(personType == QLatin1String("Writer")) {
0443       writers << mapValue(personMap, "personName");
0444     }
0445   }
0446   directors.removeDuplicates();
0447   writers.removeDuplicates();
0448   entry_->setField(QStringLiteral("cast"), actors.join(FieldFormat::rowDelimiterString()));
0449   entry_->setField(QStringLiteral("director"), directors.join(FieldFormat::delimiterString()));
0450   entry_->setField(QStringLiteral("writer"), writers.join(FieldFormat::delimiterString()));
0451 }
0452 
0453 void TheTVDBFetcher::populateEpisodes(Data::EntryPtr entry_, const QJsonArray& episodeArray_) {
0454   QStringList episodes;
0455   foreach(const QJsonValue& episode, episodeArray_) {
0456     const QVariantMap map = episode.toObject().toVariantMap();
0457     QString seasonString = mapValue(map, "seasonNumber");
0458     // skip season 0, they're extras or specials
0459     if(seasonString == QLatin1String("0")) continue;
0460     if(seasonString.isEmpty()) seasonString = QLatin1String("1");
0461     episodes << mapValue(map, "name") + FieldFormat::columnDelimiterString() +
0462                 seasonString + FieldFormat::columnDelimiterString() +
0463                 mapValue(map, "number");
0464   }
0465   entry_->setField(QStringLiteral("episode"), episodes.join(FieldFormat::rowDelimiterString()));
0466 }
0467 
0468 void TheTVDBFetcher::checkAccessToken() {
0469   const QDateTime now = QDateTime::currentDateTimeUtc();
0470   if(m_accessToken.isEmpty() || m_accessTokenExpires < now) {
0471     requestToken();
0472   } else if(now.secsTo(m_accessTokenExpires) < 12*60*60) {
0473     // refresh the token if it expires within 12 hours
0474     refreshToken();
0475   }
0476 }
0477 
0478 void TheTVDBFetcher::requestToken() {
0479   QUrl u(QString::fromLatin1(THETVDB_API_URL));
0480   u.setPath(u.path() + QLatin1String("/login"));
0481   QJsonObject obj;
0482   obj.insert(QLatin1String("apikey"), m_apiKey);
0483   obj.insert(QLatin1String("pin"), m_apiPin);
0484   const QByteArray loginPayload = QJsonDocument(obj).toJson();
0485 
0486   myLog() << "Requesting access token for API PIN:" << m_apiPin;
0487   QPointer<KIO::StoredTransferJob> job = KIO::storedHttpPost(loginPayload, u, KIO::HideProgressInfo);
0488   job->addMetaData(QStringLiteral("content-type"), QStringLiteral("Content-Type: application/json"));
0489   job->addMetaData(QStringLiteral("accept"), QStringLiteral("application/json"));
0490   KJobWidgets::setWindow(job, GUI::Proxy::widget());
0491   if(!job->exec()) {
0492     myDebug() << "TheTVDB: access token request failed";
0493     myDebug() << job->errorString() << u;
0494     return;
0495   }
0496 
0497   QJsonDocument doc = QJsonDocument::fromJson(job->data());
0498   if(doc.isNull()) {
0499     myDebug() << "TheTVDB: Invalid JSON in login response";
0500     return;
0501   }
0502   QJsonObject response = doc.object();
0503   if(response.contains(QLatin1String("Error"))) {
0504     myLog() << "Error:" << response.value(QLatin1String("Error")).toString();
0505   } else if(response.value(QLatin1String("status")) == QLatin1String("failure")) {
0506     myLog() << "Failure:" << response.value(QLatin1String("message")).toString();
0507   }
0508   m_accessToken = response.value(QLatin1String("data")).toObject()
0509                           .value(QLatin1String("token")).toString();
0510   if(m_accessToken.isEmpty()) {
0511     m_accessToken = response.value(QLatin1String("token")).toString();
0512   }
0513   if(!m_accessToken.isEmpty()) {
0514     m_accessTokenExpires = QDateTime::currentDateTimeUtc().addSecs(THETVDB_TOKEN_EXPIRES);
0515   }
0516 }
0517 
0518 void TheTVDBFetcher::refreshToken() {
0519   Q_ASSERT(!m_accessToken.isEmpty());
0520   QUrl refreshUrl(QString::fromLatin1(THETVDB_API_URL));
0521   refreshUrl.setPath(refreshUrl.path() + QLatin1String("/refresh_token"));
0522   auto job = getJob(refreshUrl, false /* check token */);
0523   if(!job->exec()) {
0524     myDebug() << "TheTVDB: access token refresh failed";
0525     myDebug() << job->errorString() << refreshUrl;
0526     return;
0527   }
0528 
0529   QJsonDocument doc = QJsonDocument::fromJson(job->data());
0530   if(doc.isNull()) {
0531     myDebug() << "TheTVDB: Invalid JSON in refresh_token response";
0532     return;
0533   }
0534   QJsonObject response = doc.object();
0535   if(response.contains(QLatin1String("Error"))) {
0536     myDebug() << "TheTVDB:" << response.value(QLatin1String("Error")).toString();
0537   }
0538   m_accessToken = response.value(QLatin1String("data")).toObject()
0539                           .value(QLatin1String("token")).toString();
0540   if(m_accessToken.isEmpty()) {
0541     m_accessToken = response.value(QLatin1String("token")).toString();
0542   }
0543   if(!m_accessToken.isEmpty()) {
0544     m_accessTokenExpires = QDateTime::currentDateTimeUtc().addSecs(THETVDB_TOKEN_EXPIRES);
0545   }
0546 }
0547 
0548 QPointer<KIO::StoredTransferJob> TheTVDBFetcher::getJob(const QUrl& url_, bool checkToken_) {
0549   if(checkToken_) checkAccessToken();
0550   QPointer<KIO::StoredTransferJob> job = KIO::storedGet(url_, KIO::NoReload, KIO::HideProgressInfo);
0551   job->addMetaData(QStringLiteral("accept"), QStringLiteral("application/json"));
0552   job->addMetaData(QStringLiteral("customHTTPHeader"), QStringLiteral("Authorization: Bearer ") + m_accessToken);
0553   KJobWidgets::setWindow(job, GUI::Proxy::widget());
0554   return job;
0555 }
0556 
0557 Tellico::Fetch::ConfigWidget* TheTVDBFetcher::configWidget(QWidget* parent_) const {
0558   return new TheTVDBFetcher::ConfigWidget(parent_, this);
0559 }
0560 
0561 QString TheTVDBFetcher::defaultName() {
0562   return QStringLiteral("The TVDB");
0563 }
0564 
0565 QString TheTVDBFetcher::defaultIcon() {
0566   return favIcon("https://thetvdb.com/images/icon.png");
0567 }
0568 
0569 Tellico::StringHash TheTVDBFetcher::allOptionalFields() {
0570   StringHash hash;
0571   hash[QStringLiteral("thetvdb")] = i18n("TheTVDB Link");
0572   hash[QStringLiteral("imdb")] = i18n("IMDb Link");
0573   hash[QStringLiteral("episode")] = i18n("Episodes");
0574   hash[QStringLiteral("network")] = i18n("Network");
0575   return hash;
0576 }
0577 
0578 TheTVDBFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const TheTVDBFetcher* fetcher_)
0579     : Fetch::ConfigWidget(parent_) {
0580   QGridLayout* l = new QGridLayout(optionsWidget());
0581   l->setSpacing(4);
0582   l->setColumnStretch(1, 10);
0583 
0584   int row = -1;
0585 
0586   QLabel* al = new QLabel(i18n("Registration is required for accessing this data source. "
0587                                "If you agree to the terms and conditions, <a href='%1'>sign "
0588                                "up for an account</a>, and enter your information below.",
0589                                 QLatin1String("https://thetvdb.com/api-information")),
0590                           optionsWidget());
0591   al->setOpenExternalLinks(true);
0592   al->setWordWrap(true);
0593   ++row;
0594   l->addWidget(al, row, 0, 1, 2);
0595   // richtext gets weird with size
0596   al->setMinimumWidth(al->sizeHint().width());
0597 
0598   QLabel* label = new QLabel(i18n("Subscriber PIN: "), optionsWidget());
0599   l->addWidget(label, ++row, 0);
0600 
0601   m_apiPinEdit = new QLineEdit(optionsWidget());
0602   connect(m_apiPinEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified);
0603   l->addWidget(m_apiPinEdit, row, 1);
0604   QString w = i18n("The default Tellico key may be used, but searching may fail due to reaching access limits.");
0605   label->setWhatsThis(w);
0606   m_apiPinEdit->setWhatsThis(w);
0607   label->setBuddy(m_apiPinEdit);
0608 
0609   l->setRowStretch(++row, 10);
0610 
0611   // now add additional fields widget
0612   addFieldsWidget(TheTVDBFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList());
0613 
0614   if(fetcher_) {
0615     m_apiPinEdit->setText(fetcher_->m_apiPin);
0616   }
0617 }
0618 
0619 QString TheTVDBFetcher::ConfigWidget::preferredName() const {
0620   return TheTVDBFetcher::defaultName();
0621 }
0622 
0623 void TheTVDBFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) {
0624   // This is the API v4 subscribe PIN
0625   const QString apiPin = m_apiPinEdit->text().trimmed();
0626   if(!apiPin.isEmpty()) {
0627     config_.writeEntry("API Key", apiPin);
0628   }
0629 }