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 }