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