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 }