File indexing completed on 2024-05-12 05:09:30
0001 /*************************************************************************** 0002 Copyright (C) 2008-2022 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 <config.h> // for TELLICO_VERSION 0026 0027 #include "discogsfetcher.h" 0028 #include "../collections/musiccollection.h" 0029 #include "../images/imagefactory.h" 0030 #include "../utils/guiproxy.h" 0031 #include "../utils/mapvalue.h" 0032 #include "../core/filehandler.h" 0033 #include "../tellico_debug.h" 0034 0035 #include <KLocalizedString> 0036 #include <KConfigGroup> 0037 #include <KIO/Job> 0038 #include <KJobUiDelegate> 0039 #include <KJobWidgets/KJobWidgets> 0040 0041 #include <QLineEdit> 0042 #include <QLabel> 0043 #include <QFile> 0044 #include <QTextStream> 0045 #include <QBoxLayout> 0046 #include <QJsonDocument> 0047 #include <QJsonObject> 0048 #include <QUrlQuery> 0049 #include <QThread> 0050 0051 namespace { 0052 static const int DISCOGS_MAX_RETURNS_TOTAL = 20; 0053 static const char* DISCOGS_API_URL = "https://api.discogs.com"; 0054 } 0055 0056 using namespace Tellico; 0057 using Tellico::Fetch::DiscogsFetcher; 0058 0059 DiscogsFetcher::DiscogsFetcher(QObject* parent_) 0060 : Fetcher(parent_) 0061 , m_limit(DISCOGS_MAX_RETURNS_TOTAL) 0062 , m_started(false) 0063 , m_page(1) { 0064 } 0065 0066 DiscogsFetcher::~DiscogsFetcher() { 0067 } 0068 0069 QString DiscogsFetcher::source() const { 0070 return m_name.isEmpty() ? defaultName() : m_name; 0071 } 0072 0073 bool DiscogsFetcher::canSearch(Fetch::FetchKey k) const { 0074 return k == Title || k == Person || k == Keyword || k == UPC; 0075 } 0076 0077 bool DiscogsFetcher::canFetch(int type) const { 0078 return type == Data::Collection::Album; 0079 } 0080 0081 void DiscogsFetcher::readConfigHook(const KConfigGroup& config_) { 0082 QString k = config_.readEntry("API Key"); 0083 if(!k.isEmpty()) { 0084 m_apiKey = k; 0085 } 0086 } 0087 0088 void DiscogsFetcher::setLimit(int limit_) { 0089 m_limit = qBound(1, limit_, DISCOGS_MAX_RETURNS_TOTAL); 0090 } 0091 0092 void DiscogsFetcher::search() { 0093 m_page = 1; 0094 continueSearch(); 0095 } 0096 0097 void DiscogsFetcher::continueSearch() { 0098 m_started = true; 0099 0100 QUrl u(QString::fromLatin1(DISCOGS_API_URL)); 0101 u.setPath(QStringLiteral("/database/search")); 0102 0103 QUrlQuery q; 0104 switch(request().key()) { 0105 case Title: 0106 q.addQueryItem(QStringLiteral("release_title"), request().value()); 0107 q.addQueryItem(QStringLiteral("type"), QStringLiteral("release")); 0108 break; 0109 0110 case Person: 0111 q.addQueryItem(QStringLiteral("artist"), request().value()); 0112 q.addQueryItem(QStringLiteral("type"), QStringLiteral("release")); 0113 break; 0114 0115 case Keyword: 0116 q.addQueryItem(QStringLiteral("q"), request().value()); 0117 q.addQueryItem(QStringLiteral("type"), QStringLiteral("release")); 0118 break; 0119 0120 case UPC: 0121 q.addQueryItem(QStringLiteral("barcode"), request().value()); 0122 break; 0123 0124 case Raw: 0125 q.setQuery(request().value()); 0126 break; 0127 0128 default: 0129 myWarning() << source() << "- key not recognized:" << request().key(); 0130 stop(); 0131 return; 0132 } 0133 0134 if(m_apiKey.isEmpty()) { 0135 myDebug() << source() << "- empty API key"; 0136 message(i18n("An access key is required to use this data source.") 0137 + QLatin1Char(' ') + 0138 i18n("Those values must be entered in the data source settings."), MessageHandler::Error); 0139 stop(); 0140 return; 0141 } 0142 0143 q.addQueryItem(QStringLiteral("page"), QString::number(m_page)); 0144 q.addQueryItem(QStringLiteral("per_page"), QString::number(m_limit)); 0145 q.addQueryItem(QStringLiteral("token"), m_apiKey); 0146 u.setQuery(q); 0147 0148 // myDebug() << "url: " << u.url(); 0149 0150 m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); 0151 m_job->addMetaData(QLatin1String("SendUserAgent"), QLatin1String("true")); 0152 m_job->addMetaData(QStringLiteral("UserAgent"), 0153 QStringLiteral("Tellico/%1").arg(QStringLiteral(TELLICO_VERSION))); 0154 KJobWidgets::setWindow(m_job, GUI::Proxy::widget()); 0155 connect(m_job.data(), &KJob::result, this, &DiscogsFetcher::slotComplete); 0156 } 0157 0158 void DiscogsFetcher::stop() { 0159 if(!m_started) { 0160 return; 0161 } 0162 if(m_job) { 0163 m_job->kill(); 0164 m_job = nullptr; 0165 } 0166 m_started = false; 0167 emit signalDone(this); 0168 } 0169 0170 Tellico::Data::EntryPtr DiscogsFetcher::fetchEntryHook(uint uid_) { 0171 Data::EntryPtr entry = m_entries.value(uid_); 0172 if(!entry) { 0173 myWarning() << "no entry in dict"; 0174 return Data::EntryPtr(); 0175 } 0176 0177 QString id = entry->field(QStringLiteral("discogs-id")); 0178 if(!id.isEmpty()) { 0179 // quiet 0180 QUrl u(QString::fromLatin1(DISCOGS_API_URL)); 0181 u.setPath(QStringLiteral("/releases/%1").arg(id)); 0182 QByteArray data = FileHandler::readDataFile(u, true); 0183 0184 #if 0 0185 myWarning() << "Remove data debug from discogsfetcher.cpp"; 0186 QFile f(QString::fromLatin1("/tmp/test-discogs-data.json")); 0187 if(f.open(QIODevice::WriteOnly)) { 0188 QTextStream t(&f); 0189 t.setCodec("UTF-8"); 0190 t << data; 0191 } 0192 f.close(); 0193 #endif 0194 0195 QJsonParseError error; 0196 QJsonDocument doc = QJsonDocument::fromJson(data, &error); 0197 const QVariantMap resultMap = doc.object().toVariantMap(); 0198 if(resultMap.contains(QStringLiteral("message")) && mapValue(resultMap, "id").isEmpty()) { 0199 const auto& msg = mapValue(resultMap, "message"); 0200 message(msg, MessageHandler::Error); 0201 myLog() << "DiscogsFetcher -" << msg; 0202 if(msg.startsWith(QLatin1String("You are making requests too quickly"))) { 0203 QThread::msleep(2000); 0204 } 0205 } else if(error.error == QJsonParseError::NoError) { 0206 populateEntry(entry, resultMap, true); 0207 } else { 0208 myDebug() << "Bad JSON results"; 0209 } 0210 } 0211 0212 const QString image_id = entry->field(QStringLiteral("cover")); 0213 // if it's still a url, we need to load it 0214 if(image_id.contains(QLatin1Char('/'))) { 0215 const QString id = ImageFactory::addImage(QUrl::fromUserInput(image_id), true /* quiet */); 0216 if(id.isEmpty()) { 0217 myDebug() << "empty id for" << image_id; 0218 message(i18n("The cover image could not be loaded."), MessageHandler::Warning); 0219 } 0220 // empty image ID is ok 0221 entry->setField(QStringLiteral("cover"), id); 0222 } 0223 0224 // don't want to include ID field 0225 entry->setField(QStringLiteral("discogs-id"), QString()); 0226 0227 return entry; 0228 } 0229 0230 Tellico::Fetch::FetchRequest DiscogsFetcher::updateRequest(Data::EntryPtr entry_) { 0231 const QString barcode = entry_->field(QStringLiteral("barcode")); 0232 if(!barcode.isEmpty()) { 0233 return FetchRequest(UPC, barcode); 0234 } 0235 0236 const QString title = entry_->field(QStringLiteral("title")); 0237 const QString artist = entry_->field(QStringLiteral("artist")); 0238 const QString year = entry_->field(QStringLiteral("year")); 0239 // if any two of those are non-empty, combine them for a keyword search 0240 const int sum = (title.isEmpty() ? 0:1) + (artist.isEmpty() ? 0:1) + (year.isEmpty() ? 0:1); 0241 if(sum > 1) { 0242 QUrlQuery q; 0243 if(!title.isEmpty()) { 0244 q.addQueryItem(QStringLiteral("title"), title); 0245 } 0246 if(!artist.isEmpty()) { 0247 q.addQueryItem(QStringLiteral("artist"), artist); 0248 } 0249 if(!year.isEmpty()) { 0250 q.addQueryItem(QStringLiteral("year"), year); 0251 } 0252 return FetchRequest(Raw, q.toString()); 0253 } 0254 0255 if(!title.isEmpty()) { 0256 return FetchRequest(Title, title); 0257 } 0258 0259 if(!artist.isEmpty()) { 0260 return FetchRequest(Person, artist); 0261 } 0262 return FetchRequest(); 0263 } 0264 0265 void DiscogsFetcher::slotComplete(KJob*) { 0266 if(m_job->error()) { 0267 m_job->uiDelegate()->showErrorMessage(); 0268 stop(); 0269 return; 0270 } 0271 0272 QByteArray data = m_job->data(); 0273 if(data.isEmpty()) { 0274 myDebug() << "no data"; 0275 stop(); 0276 return; 0277 } 0278 0279 #if 0 // checking remaining discogs rate limit allocation 0280 const QStringList allHeaders = m_job->queryMetaData(QStringLiteral("HTTP-Headers")).split(QLatin1Char('\n')); 0281 foreach(const QString& header, allHeaders) { 0282 if(header.startsWith(QStringLiteral("x-discogs-ratelimit-remaining"))) { 0283 const int index = header.indexOf(QLatin1Char(':')); 0284 if(index > 0) { 0285 myDebug() << "DiscogsFetcher: rate limit remaining:" << header.mid(index + 1); 0286 } 0287 break; 0288 } 0289 } 0290 #endif 0291 // see bug 319662. If fetcher is cancelled, job is killed 0292 // if the pointer is retained, it gets double-deleted 0293 m_job = nullptr; 0294 0295 #if 0 0296 myWarning() << "Remove debug from discogsfetcher.cpp"; 0297 QFile f(QString::fromLatin1("/tmp/test-discogs.json")); 0298 if(f.open(QIODevice::WriteOnly)) { 0299 QTextStream t(&f); 0300 t.setCodec("UTF-8"); 0301 t << data; 0302 } 0303 f.close(); 0304 #endif 0305 0306 Data::CollPtr coll(new Data::MusicCollection(true)); 0307 // always add ID for fetchEntryHook 0308 Data::FieldPtr field(new Data::Field(QStringLiteral("discogs-id"), QStringLiteral("Discogs ID"), Data::Field::Line)); 0309 field->setCategory(i18n("General")); 0310 coll->addField(field); 0311 0312 if(optionalFields().contains(QStringLiteral("discogs"))) { 0313 Data::FieldPtr field(new Data::Field(QStringLiteral("discogs"), i18n("Discogs Link"), Data::Field::URL)); 0314 field->setCategory(i18n("General")); 0315 coll->addField(field); 0316 } 0317 if(optionalFields().contains(QStringLiteral("nationality"))) { 0318 Data::FieldPtr field(new Data::Field(QStringLiteral("nationality"), i18n("Nationality"))); 0319 field->setCategory(i18n("General")); 0320 field->setFlags(Data::Field::AllowCompletion | Data::Field::AllowMultiple | Data::Field::AllowGrouped); 0321 field->setFormatType(FieldFormat::FormatPlain); 0322 coll->addField(field); 0323 } 0324 if(optionalFields().contains(QStringLiteral("producer"))) { 0325 Data::FieldPtr field(new Data::Field(QStringLiteral("producer"), i18n("Producer"))); 0326 field->setCategory(i18n("General")); 0327 field->setFlags(Data::Field::AllowCompletion | Data::Field::AllowMultiple | Data::Field::AllowGrouped); 0328 field->setFormatType(FieldFormat::FormatName); 0329 coll->addField(field); 0330 } 0331 if(optionalFields().contains(QStringLiteral("barcode"))) { 0332 Data::FieldPtr field(new Data::Field(QStringLiteral("barcode"), i18n("Barcode"))); 0333 field->setCategory(i18n("General")); 0334 coll->addField(field); 0335 } 0336 if(optionalFields().contains(QStringLiteral("catno"))) { 0337 Data::FieldPtr field(new Data::Field(QStringLiteral("catno"), i18n("Catalog Number"))); 0338 field->setCategory(i18n("General")); 0339 coll->addField(field); 0340 } 0341 0342 QJsonDocument doc = QJsonDocument::fromJson(data); 0343 // const QVariantMap resultMap = doc.object().toVariantMap().value(QStringLiteral("feed")).toMap(); 0344 const QVariantMap resultMap = doc.object().toVariantMap(); 0345 0346 if(mapValue(resultMap, "message").startsWith(QLatin1String("Invalid consumer token"))) { 0347 message(i18n("The Discogs.com server reports a token error."), 0348 MessageHandler::Error); 0349 stop(); 0350 return; 0351 } 0352 0353 const int totalPages = mapValue(resultMap, "pagination", "pages").toInt(); 0354 m_hasMoreResults = m_page < totalPages; 0355 ++m_page; 0356 0357 int count = 0; 0358 foreach(const QVariant& result, resultMap.value(QLatin1String("results")).toList()) { 0359 if(count >= m_limit) { 0360 break; 0361 } 0362 0363 // myDebug() << "found result:" << result; 0364 0365 Data::EntryPtr entry(new Data::Entry(coll)); 0366 populateEntry(entry, result.toMap(), false); 0367 0368 FetchResult* r = new FetchResult(this, entry); 0369 m_entries.insert(r->uid, entry); 0370 emit signalResultFound(r); 0371 ++count; 0372 } 0373 0374 stop(); 0375 } 0376 0377 void DiscogsFetcher::populateEntry(Data::EntryPtr entry_, const QVariantMap& resultMap_, bool fullData_) { 0378 entry_->setField(QStringLiteral("discogs-id"), mapValue(resultMap_, "id")); 0379 entry_->setField(QStringLiteral("title"), mapValue(resultMap_, "title")); 0380 const QString year = mapValue(resultMap_, "year"); 0381 if(year != QLatin1String("0")) { 0382 entry_->setField(QStringLiteral("year"), year); 0383 } 0384 entry_->setField(QStringLiteral("genre"), mapValue(resultMap_, "genres")); 0385 0386 QStringList artists; 0387 foreach(const QVariant& artist, resultMap_.value(QLatin1String("artists")).toList()) { 0388 artists << mapValue(artist.toMap(), "name"); 0389 } 0390 artists.removeDuplicates(); // sometimes the same value is repeated 0391 entry_->setField(QStringLiteral("artist"), artists.join(FieldFormat::delimiterString())); 0392 0393 QStringList labels, catnos; 0394 foreach(const QVariant& label, resultMap_.value(QLatin1String("labels")).toList()) { 0395 const QVariantMap labelMap = label.toMap(); 0396 labels << mapValue(labelMap, "name"); 0397 catnos << mapValue(labelMap, "catno"); 0398 } 0399 entry_->setField(QStringLiteral("label"), labels.join(FieldFormat::delimiterString())); 0400 if(entry_->collection()->hasField(QStringLiteral("catno"))) { 0401 entry_->setField(QStringLiteral("catno"), catnos.join(FieldFormat::delimiterString())); 0402 } 0403 0404 /* cover value is not always in the full data, so go ahead and set it now */ 0405 QString coverUrl = mapValue(resultMap_, "cover_image"); 0406 if(coverUrl.isEmpty()) { 0407 coverUrl = mapValue(resultMap_, "thumb"); 0408 } 0409 if(!coverUrl.isEmpty()) { 0410 entry_->setField(QStringLiteral("cover"), coverUrl); 0411 } 0412 0413 // if we only need cursory data, then we're done 0414 if(!fullData_) { 0415 return; 0416 } 0417 0418 // check the formats, it could have multiple 0419 // if there is a CD, prefer that in the track list 0420 bool hasCD = false; 0421 foreach(const QVariant& format, resultMap_.value(QLatin1String("formats")).toList()) { 0422 const QString formatName = mapValue(format.toMap(), "name"); 0423 if(formatName == QLatin1String("CD")) { 0424 entry_->setField(QStringLiteral("medium"), i18n("Compact Disc")); 0425 hasCD = true; 0426 } else if(formatName == QLatin1String("Vinyl")) { 0427 entry_->setField(QStringLiteral("medium"), i18n("Vinyl")); 0428 } else if(formatName == QLatin1String("Cassette")) { 0429 entry_->setField(QStringLiteral("medium"), i18n("Cassette")); 0430 } else if(!hasCD && formatName == QLatin1String("DVD")) { 0431 // sometimes a CD and DVD both are included. If we're using the CD, ignore the DVD 0432 entry_->setField(QStringLiteral("medium"), i18n("DVD")); 0433 } 0434 } 0435 0436 static const QRegularExpression discSplit(QStringLiteral("[-. ]")); 0437 static const QRegularExpression nonDigits(QStringLiteral("[\\D]")); 0438 QList<QStringList> discs; // list of tracks per disc 0439 foreach(const QVariant& track, resultMap_.value(QLatin1String("tracklist")).toList()) { 0440 const QVariantMap trackMap = track.toMap(); 0441 if(mapValue(trackMap, "type_") != QLatin1String("track")) { 0442 continue; 0443 } 0444 0445 // Releases might include a CD and a DVD, for example 0446 // prefer only the tracks on the CD. Allow positions of just numbers 0447 if(hasCD && !(mapValue(trackMap, "position").at(0).isNumber() || 0448 mapValue(trackMap, "position").startsWith(QLatin1String("CD")))) { 0449 continue; 0450 } 0451 0452 QStringList trackInfo; 0453 trackInfo << mapValue(trackMap, "title"); 0454 if(trackMap.contains(QStringLiteral("artists"))) { 0455 QStringList artists; 0456 foreach(const QVariant& artist, trackMap.value(QLatin1String("artists")).toList()) { 0457 artists << mapValue(artist.toMap(), "name"); 0458 } 0459 trackInfo << artists.join(FieldFormat::delimiterString()); 0460 } else { 0461 trackInfo << entry_->field(QStringLiteral("artist")); 0462 } 0463 trackInfo << mapValue(trackMap, "duration"); 0464 0465 // determine whether the track is in a multi-disc album 0466 int disc = 1; 0467 const QString trackNum = mapValue(trackMap, "position"); 0468 if(trackNum.contains(discSplit)) { 0469 disc = trackNum.section(discSplit, 0, 0).remove(nonDigits).toInt(); 0470 if(disc == 0) disc = 1; 0471 } 0472 while(discs.size() < disc) discs << QStringList(); 0473 discs[disc-1] << trackInfo.join(FieldFormat::columnDelimiterString()); 0474 } 0475 bool changeTrackTitle = true; 0476 for(int disc = 0; disc < discs.count(); ++disc) { 0477 QString trackField = QStringLiteral("track"); 0478 if(disc > 0) { 0479 trackField.append(QString::number(disc+1)); 0480 Data::FieldPtr f2(new Data::Field(trackField, 0481 i18n("Tracks (Disc %1)", disc+1), 0482 Data::Field::Table)); 0483 f2->setFormatType(FieldFormat::FormatTitle); 0484 f2->setProperty(QStringLiteral("columns"), QStringLiteral("3")); 0485 f2->setProperty(QStringLiteral("column1"), i18n("Title")); 0486 f2->setProperty(QStringLiteral("column2"), i18n("Artist")); 0487 f2->setProperty(QStringLiteral("column3"), i18n("Length")); 0488 entry_->collection()->addField(f2); 0489 // also change the title of the first track field 0490 if(changeTrackTitle) { 0491 Data::FieldPtr f1 = entry_->collection()->fieldByName(QStringLiteral("track")); 0492 f1->setTitle(i18n("Tracks (Disc %1)", 1)); 0493 entry_->collection()->modifyField(f1); 0494 changeTrackTitle = false; 0495 } 0496 } 0497 entry_->setField(trackField, discs.at(disc).join(FieldFormat::rowDelimiterString())); 0498 } 0499 0500 if(entry_->collection()->hasField(QStringLiteral("discogs"))) { 0501 entry_->setField(QStringLiteral("discogs"), mapValue(resultMap_, "uri")); 0502 } 0503 0504 if(entry_->collection()->hasField(QStringLiteral("nationality"))) { 0505 entry_->setField(QStringLiteral("nationality"), mapValue(resultMap_, "country")); 0506 } 0507 0508 if(entry_->collection()->hasField(QStringLiteral("barcode"))) { 0509 foreach(const QVariant& identifier, resultMap_.value(QLatin1String("identifiers")).toList()) { 0510 const QVariantMap idMap = identifier.toMap(); 0511 if(mapValue(idMap, "type") == QLatin1String("Barcode")) { 0512 entry_->setField(QStringLiteral("barcode"), mapValue(idMap, "value")); 0513 break; 0514 } 0515 } 0516 } 0517 0518 if(entry_->collection()->hasField(QStringLiteral("producer"))) { 0519 QStringList producers; 0520 foreach(const QVariant& extraArtist, resultMap_.value(QLatin1String("extraartists")).toList()) { 0521 const QVariantMap extraArtistMap = extraArtist.toMap(); 0522 if(mapValue(extraArtistMap, "role").contains(QStringLiteral("Producer"))) { 0523 producers << mapValue(extraArtistMap, "name"); 0524 } 0525 } 0526 entry_->setField(QStringLiteral("producer"), producers.join(FieldFormat::delimiterString())); 0527 } 0528 0529 entry_->setField(QStringLiteral("comments"), mapValue(resultMap_, "notes")); 0530 } 0531 0532 Tellico::Fetch::ConfigWidget* DiscogsFetcher::configWidget(QWidget* parent_) const { 0533 return new DiscogsFetcher::ConfigWidget(parent_, this); 0534 } 0535 0536 QString DiscogsFetcher::defaultName() { 0537 return i18n("Discogs Audio Search"); 0538 } 0539 0540 QString DiscogsFetcher::defaultIcon() { 0541 return favIcon("http://www.discogs.com"); 0542 } 0543 0544 Tellico::StringHash DiscogsFetcher::allOptionalFields() { 0545 StringHash hash; 0546 hash[QStringLiteral("producer")] = i18n("Producer"); 0547 hash[QStringLiteral("nationality")] = i18n("Nationality"); 0548 hash[QStringLiteral("discogs")] = i18n("Discogs Link"); 0549 hash[QStringLiteral("barcode")] = i18n("Barcode"); 0550 hash[QStringLiteral("catno")] = i18n("Catalog Number"); 0551 return hash; 0552 } 0553 0554 DiscogsFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const DiscogsFetcher* fetcher_) 0555 : Fetch::ConfigWidget(parent_) { 0556 QGridLayout* l = new QGridLayout(optionsWidget()); 0557 l->setSpacing(4); 0558 l->setColumnStretch(1, 10); 0559 0560 int row = -1; 0561 QLabel* al = new QLabel(i18n("Registration is required for accessing this data source. " 0562 "If you agree to the terms and conditions, <a href='%1'>sign " 0563 "up for an account</a>, and enter your information below.", 0564 QLatin1String("https://www.discogs.com/developers/#page:authentication")), 0565 optionsWidget()); 0566 al->setOpenExternalLinks(true); 0567 al->setWordWrap(true); 0568 ++row; 0569 l->addWidget(al, row, 0, 1, 2); 0570 // richtext gets weird with size 0571 al->setMinimumWidth(al->sizeHint().width()); 0572 0573 QLabel* label = new QLabel(i18n("User token:"), optionsWidget()); 0574 l->addWidget(label, ++row, 0); 0575 0576 m_apiKeyEdit = new QLineEdit(optionsWidget()); 0577 connect(m_apiKeyEdit, &QLineEdit::textChanged, this, &ConfigWidget::slotSetModified); 0578 l->addWidget(m_apiKeyEdit, row, 1); 0579 label->setBuddy(m_apiKeyEdit); 0580 0581 l->setRowStretch(++row, 10); 0582 0583 if(fetcher_) { 0584 m_apiKeyEdit->setText(fetcher_->m_apiKey); 0585 } 0586 0587 // now add additional fields widget 0588 addFieldsWidget(DiscogsFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList()); 0589 } 0590 0591 void DiscogsFetcher::ConfigWidget::saveConfigHook(KConfigGroup& config_) { 0592 QString apiKey = m_apiKeyEdit->text().trimmed(); 0593 if(!apiKey.isEmpty()) { 0594 config_.writeEntry("API Key", apiKey); 0595 } 0596 } 0597 0598 QString DiscogsFetcher::ConfigWidget::preferredName() const { 0599 return DiscogsFetcher::defaultName(); 0600 }