File indexing completed on 2024-05-12 16:45:58
0001 /*************************************************************************** 0002 Copyright (C) 2017 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 "omdbfetcher.h" 0026 #include "../collections/videocollection.h" 0027 #include "../images/imagefactory.h" 0028 #include "../utils/guiproxy.h" 0029 #include "../core/filehandler.h" 0030 #include "../utils/string_utils.h" 0031 #include "../tellico_debug.h" 0032 0033 #include <KLocalizedString> 0034 #include <KConfigGroup> 0035 #include <KJob> 0036 #include <KJobUiDelegate> 0037 #include <KJobWidgets/KJobWidgets> 0038 #include <KIO/StoredTransferJob> 0039 0040 #include <QUrl> 0041 #include <QLabel> 0042 #include <QLineEdit> 0043 #include <QFile> 0044 #include <QTextStream> 0045 #include <QGridLayout> 0046 #include <QTextCodec> 0047 #include <QJsonDocument> 0048 #include <QJsonObject> 0049 #include <QUrlQuery> 0050 0051 namespace { 0052 static const int OMDB_MAX_RETURNS_TOTAL = 20; 0053 static const char* OMDB_API_URL = "https://www.omdbapi.com"; 0054 } 0055 0056 using namespace Tellico; 0057 using Tellico::Fetch::OMDBFetcher; 0058 0059 OMDBFetcher::OMDBFetcher(QObject* parent_) 0060 : Fetcher(parent_) 0061 , m_started(false) { 0062 // setLimit(OMDB_MAX_RETURNS_TOTAL); 0063 } 0064 0065 OMDBFetcher::~OMDBFetcher() { 0066 } 0067 0068 QString OMDBFetcher::source() const { 0069 return m_name.isEmpty() ? defaultName() : m_name; 0070 } 0071 0072 bool OMDBFetcher::canSearch(Fetch::FetchKey k) const { 0073 return k == Title; 0074 } 0075 0076 bool OMDBFetcher::canFetch(int type) const { 0077 return type == Data::Collection::Video; 0078 } 0079 0080 void OMDBFetcher::readConfigHook(const KConfigGroup& config_) { 0081 QString k = config_.readEntry("API Key", QString()); 0082 if(!k.isEmpty()) { 0083 m_apiKey = k; 0084 } 0085 } 0086 0087 void OMDBFetcher::saveConfigHook(KConfigGroup&) { 0088 } 0089 0090 void OMDBFetcher::search() { 0091 continueSearch(); 0092 } 0093 0094 void OMDBFetcher::continueSearch() { 0095 m_started = true; 0096 0097 if(m_apiKey.isEmpty()) { 0098 myDebug() << "No API key"; 0099 message(i18n("An access key is required to use this data source.") 0100 + QLatin1Char(' ') + 0101 i18n("Those values must be entered in the data source settings."), MessageHandler::Error); 0102 stop(); 0103 return; 0104 } 0105 0106 QUrl u(QString::fromLatin1(OMDB_API_URL)); 0107 QUrlQuery q; 0108 switch(request().key()) { 0109 case Title: 0110 q.addQueryItem(QStringLiteral("type"), QStringLiteral("movie")); 0111 q.addQueryItem(QStringLiteral("r"), QStringLiteral("json")); 0112 q.addQueryItem(QStringLiteral("s"), request().value()); 0113 break; 0114 0115 case Raw: 0116 q.setQuery(request().value()); 0117 break; 0118 0119 default: 0120 myWarning() << "key not recognized:" << request().key(); 0121 stop(); 0122 return; 0123 } 0124 q.addQueryItem(QStringLiteral("apikey"), m_apiKey); 0125 u.setQuery(q); 0126 // myDebug() << u; 0127 0128 m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); 0129 KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); 0130 connect(m_job.data(), &KJob::result, this, &OMDBFetcher::slotComplete); 0131 } 0132 0133 void OMDBFetcher::stop() { 0134 if(!m_started) { 0135 return; 0136 } 0137 if(m_job) { 0138 m_job->kill(); 0139 m_job = nullptr; 0140 } 0141 m_started = false; 0142 emit signalDone(this); 0143 } 0144 0145 Tellico::Data::EntryPtr OMDBFetcher::fetchEntryHook(uint uid_) { 0146 Data::EntryPtr entry = m_entries.value(uid_); 0147 if(!entry) { 0148 myWarning() << "no entry in dict"; 0149 return Data::EntryPtr(); 0150 } 0151 0152 QString id = entry->field(QStringLiteral("imdb-id")); 0153 if(!id.isEmpty()) { 0154 // quiet 0155 QUrl u(QString::fromLatin1(OMDB_API_URL)); 0156 QUrlQuery q; 0157 q.addQueryItem(QStringLiteral("type"), QStringLiteral("movie")); 0158 q.addQueryItem(QStringLiteral("r"), QStringLiteral("json")); 0159 q.addQueryItem(QStringLiteral("i"), id); 0160 q.addQueryItem(QStringLiteral("apikey"), m_apiKey); 0161 u.setQuery(q); 0162 QByteArray data = FileHandler::readDataFile(u, true); 0163 #if 0 0164 myWarning() << "Remove debug2 from omdbfetcher.cpp"; 0165 QFile f(QString::fromLatin1("/tmp/test2.json")); 0166 if(f.open(QIODevice::WriteOnly)) { 0167 QTextStream t(&f); 0168 t.setCodec("UTF-8"); 0169 t << data; 0170 } 0171 f.close(); 0172 #endif 0173 QJsonDocument doc = QJsonDocument::fromJson(data); 0174 populateEntry(entry, doc.object().toVariantMap(), true); 0175 } 0176 0177 // image might still be a URL 0178 const QString image_id = entry->field(QStringLiteral("cover")); 0179 if(image_id.contains(QLatin1Char('/'))) { 0180 const QString id = ImageFactory::addImage(QUrl::fromUserInput(image_id), true /* quiet */); 0181 if(id.isEmpty()) { 0182 message(i18n("The cover image could not be loaded."), MessageHandler::Warning); 0183 } 0184 // empty image ID is ok 0185 entry->setField(QStringLiteral("cover"), id); 0186 } 0187 0188 // don't want to include IMDb ID field 0189 entry->setField(QStringLiteral("imdb-id"), QString()); 0190 0191 return entry; 0192 } 0193 0194 Tellico::Fetch::FetchRequest OMDBFetcher::updateRequest(Data::EntryPtr entry_) { 0195 QString imdb = entry_->field(QStringLiteral("imdb")); 0196 if(imdb.isEmpty()) { 0197 imdb = entry_->field(QStringLiteral("imdb-id")); 0198 } 0199 if(!imdb.isEmpty()) { 0200 QRegularExpression ttRx(QStringLiteral("tt\\d+")); 0201 auto ttMatch = ttRx.match(imdb); 0202 if(ttMatch.hasMatch()) { 0203 return FetchRequest(Raw, QStringLiteral("type=movie&r=json&i=") + ttMatch.captured()); 0204 } 0205 } 0206 0207 const QString title = entry_->field(QStringLiteral("title")); 0208 if(!title.isEmpty()) { 0209 const QString year = entry_->field(QStringLiteral("year")); 0210 if(year.isEmpty()) { 0211 return FetchRequest(Title, title); 0212 } else { 0213 return FetchRequest(Raw, QStringLiteral("type=movie&r=json&s=\"%1\"&y=%2").arg(title, year)); 0214 } 0215 } 0216 return FetchRequest(); 0217 } 0218 0219 void OMDBFetcher::slotComplete(KJob* job_) { 0220 KIO::StoredTransferJob* job = static_cast<KIO::StoredTransferJob*>(job_); 0221 0222 if(job->error()) { 0223 job->uiDelegate()->showErrorMessage(); 0224 stop(); 0225 return; 0226 } 0227 0228 QByteArray data = job->data(); 0229 if(data.isEmpty()) { 0230 myDebug() << "no data"; 0231 stop(); 0232 return; 0233 } 0234 // see bug 319662. If fetcher is cancelled, job is killed 0235 // if the pointer is retained, it gets double-deleted 0236 m_job = nullptr; 0237 0238 #if 0 0239 myWarning() << "Remove debug from omdbfetcher.cpp"; 0240 QFile f(QString::fromLatin1("/tmp/test.json")); 0241 if(f.open(QIODevice::WriteOnly)) { 0242 QTextStream t(&f); 0243 t.setCodec("UTF-8"); 0244 t << data; 0245 } 0246 f.close(); 0247 #endif 0248 0249 Data::CollPtr coll(new Data::VideoCollection(true)); 0250 // always add the imdb-id for fetchEntryHook 0251 Data::FieldPtr field(new Data::Field(QStringLiteral("imdb-id"), QStringLiteral("IMDb ID"), Data::Field::Line)); 0252 field->setCategory(i18n("General")); 0253 coll->addField(field); 0254 0255 #if 0 0256 if(optionalFields().contains(QStringLiteral("alttitle"))) { 0257 Data::FieldPtr field(new Data::Field(QLatin1String("alttitle"), i18n("Alternative Titles"), Data::Field::Table)); 0258 field->setFormatType(FieldFormat::FormatTitle); 0259 coll->addField(field); 0260 } 0261 if(optionalFields().contains(QStringLiteral("origtitle"))) { 0262 Data::FieldPtr f(new Data::Field(QLatin1String("origtitle"), i18n("Original Title"))); 0263 f->setFormatType(FieldFormat::FormatTitle); 0264 coll->addField(f); 0265 } 0266 #endif 0267 0268 QJsonDocument doc = QJsonDocument::fromJson(data); 0269 QVariantMap result = doc.object().toVariantMap(); 0270 0271 const bool response = result.value(QStringLiteral("Response")).toBool(); 0272 if(!response) { 0273 // a lack of results is considered an error 0274 // don't show a user alert for that 0275 myDebug() << "Error:" << result.value(QStringLiteral("Error")).toString(); 0276 // message(result.value(QStringLiteral("Error")).toString(), MessageHandler::Error); 0277 stop(); 0278 return; 0279 } 0280 0281 const QString search = QStringLiteral("Search"); 0282 QVariantList resultList = result.value(search).toList(); 0283 if(resultList.isEmpty()) { 0284 // might be a single result 0285 if(result.contains(QLatin1String("Title"))) { 0286 resultList << result; 0287 } else { 0288 myDebug() << "no results"; 0289 stop(); 0290 return; 0291 } 0292 } 0293 0294 // if the search was a Raw update, then fully populate it 0295 // and wipe the imdb-id value 0296 const bool fullResult = request().key() == Fetch::Raw && 0297 !result.contains(search); 0298 0299 int count = 0; 0300 foreach(const QVariant& result, resultList) { 0301 // myDebug() << "found result:" << result; 0302 0303 Data::EntryPtr entry(new Data::Entry(coll)); 0304 populateEntry(entry, result.toMap(), fullResult); 0305 if(fullResult) { 0306 // indicate it's already fully populated 0307 entry->setField(QStringLiteral("imdb-id"), QString()); 0308 } 0309 0310 FetchResult* r = new FetchResult(this, entry); 0311 m_entries.insert(r->uid, entry); 0312 emit signalResultFound(r); 0313 ++count; 0314 if(count >= OMDB_MAX_RETURNS_TOTAL) { 0315 break; 0316 } 0317 } 0318 0319 stop(); 0320 } 0321 0322 void OMDBFetcher::populateEntry(Data::EntryPtr entry_, const QVariantMap& resultMap_, bool fullData_) { 0323 entry_->setField(QStringLiteral("imdb-id"), mapValue(resultMap_, "imdbID")); 0324 entry_->setField(QStringLiteral("title"), mapValue(resultMap_, "Title")); 0325 entry_->setField(QStringLiteral("year"), mapValue(resultMap_, "Year")); 0326 0327 if(!fullData_) { 0328 return; 0329 } 0330 0331 const QString cert = mapValue(resultMap_, "Rated"); 0332 Data::FieldPtr certField = entry_->collection()->fieldByName(QStringLiteral("certification")); 0333 if(certField) { 0334 foreach(const QString& value, certField->allowed()) { 0335 if(value.startsWith(cert)) { 0336 entry_->setField(QStringLiteral("certification"), value); 0337 break; 0338 } 0339 } 0340 } 0341 entry_->setField(QStringLiteral("running-time"), 0342 mapValue(resultMap_, "Runtime").remove(QRegularExpression(QLatin1String("[^\\d]")))); 0343 0344 const QStringList genres = mapValue(resultMap_, "Genre").split(QStringLiteral(", ")); 0345 entry_->setField(QStringLiteral("genre"), genres.join(FieldFormat::delimiterString())); 0346 0347 const QStringList directors = mapValue(resultMap_, "Director").split(QStringLiteral(", ")); 0348 entry_->setField(QStringLiteral("director"), directors.join(FieldFormat::delimiterString())); 0349 0350 QStringList writers = mapValue(resultMap_, "Writer").split(QStringLiteral(", ")); 0351 // some wrtiers have parentheticals, remove those 0352 entry_->setField(QStringLiteral("writer"), 0353 writers.replaceInStrings(QRegularExpression(QLatin1String("\\s*\\(.+\\)\\s*")), QString()) 0354 .join(FieldFormat::delimiterString())); 0355 0356 const QStringList producers = mapValue(resultMap_, "Producer").split(QStringLiteral(", ")); 0357 entry_->setField(QStringLiteral("producer"), producers.join(FieldFormat::delimiterString())); 0358 0359 const QStringList actors = mapValue(resultMap_, "Actors").split(QStringLiteral(", ")); 0360 entry_->setField(QStringLiteral("cast"), actors.join(FieldFormat::rowDelimiterString())); 0361 0362 const QStringList countries = mapValue(resultMap_, "Country").split(QStringLiteral(", ")); 0363 entry_->setField(QStringLiteral("nationality"), countries.join(FieldFormat::delimiterString())); 0364 0365 const QStringList langs = mapValue(resultMap_, "Language").split(QStringLiteral(", ")); 0366 entry_->setField(QStringLiteral("language"), langs.join(FieldFormat::delimiterString())); 0367 0368 entry_->setField(QStringLiteral("cover"), mapValue(resultMap_, "Poster")); 0369 entry_->setField(QStringLiteral("plot"), mapValue(resultMap_, "Plot")); 0370 0371 const QString imdb(QStringLiteral("imdb")); 0372 if(optionalFields().contains(imdb)) { 0373 if(!entry_->collection()->hasField(imdb)) { 0374 entry_->collection()->addField(Data::Field::createDefaultField(Data::Field::ImdbField)); 0375 } 0376 entry_->setField(imdb, QLatin1String("http://www.imdb.com/title/") 0377 + entry_->field(QStringLiteral("imdb-id")) 0378 + QLatin1Char('/')); 0379 } 0380 0381 } 0382 0383 Tellico::Fetch::ConfigWidget* OMDBFetcher::configWidget(QWidget* parent_) const { 0384 return new OMDBFetcher::ConfigWidget(parent_, this); 0385 } 0386 0387 QString OMDBFetcher::defaultName() { 0388 return QStringLiteral("The Open Movie Database"); 0389 } 0390 0391 QString OMDBFetcher::defaultIcon() { 0392 return favIcon("http://www.omdbapi.com"); 0393 } 0394 0395 Tellico::StringHash OMDBFetcher::allOptionalFields() { 0396 StringHash hash; 0397 hash[QStringLiteral("imdb")] = i18n("IMDb Link"); 0398 return hash; 0399 } 0400 0401 OMDBFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const OMDBFetcher* fetcher_) 0402 : Fetch::ConfigWidget(parent_) { 0403 QGridLayout* l = new QGridLayout(optionsWidget()); 0404 l->setSpacing(4); 0405 l->setColumnStretch(1, 10); 0406 0407 int row = -1; 0408 QLabel* al = new QLabel(i18n("Registration is required for accessing this data source. " 0409 "If you agree to the terms and conditions, <a href='%1'>sign " 0410 "up for an account</a>, and enter your information below.", 0411 QLatin1String("http://www.omdbapi.com")), 0412 optionsWidget()); 0413 al->setOpenExternalLinks(true); 0414 al->setWordWrap(true); 0415 ++row; 0416 l->addWidget(al, row, 0, 1, 2); 0417 // richtext gets weird with size 0418 al->setMinimumWidth(al->sizeHint().width()); 0419 0420 QLabel* label = new QLabel(i18n("Access key: "), optionsWidget()); 0421 l->addWidget(label, ++row, 0); 0422 0423 m_apiKeyEdit = new QLineEdit(optionsWidget()); 0424 connect(m_apiKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); 0425 l->addWidget(m_apiKeyEdit, row, 1); 0426 label->setBuddy(m_apiKeyEdit); 0427 0428 l->setRowStretch(++row, 10); 0429 0430 if(fetcher_) { 0431 m_apiKeyEdit->setText(fetcher_->m_apiKey); 0432 } 0433 0434 // now add additional fields widget 0435 addFieldsWidget(OMDBFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); 0436 } 0437 0438 void OMDBFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { 0439 const QString apiKey = m_apiKeyEdit->text().trimmed(); 0440 if(!apiKey.isEmpty()) { 0441 config_.writeEntry("API Key", apiKey); 0442 } 0443 } 0444 0445 QString OMDBFetcher::ConfigWidget::preferredName() const { 0446 return OMDBFetcher::defaultName(); 0447 }