File indexing completed on 2024-05-12 05:09:39

0001 /***************************************************************************
0002     Copyright (C) 2009-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 "musicbrainzfetcher.h"
0028 #include "../translators/xslthandler.h"
0029 #include "../translators/tellicoimporter.h"
0030 #include "../images/imagefactory.h"
0031 #include "../utils/guiproxy.h"
0032 #include "../utils/string_utils.h"
0033 #include "../collection.h"
0034 #include "../entry.h"
0035 #include "../utils/datafileregistry.h"
0036 #include "../utils/xmlhandler.h"
0037 #include "../tellico_debug.h"
0038 
0039 #include <KLocalizedString>
0040 #include <KIO/Job>
0041 #include <KJobUiDelegate>
0042 #include <KJobWidgets/KJobWidgets>
0043 #include <KConfigGroup>
0044 
0045 #include <QLabel>
0046 #include <QFile>
0047 #include <QTextStream>
0048 #include <QGridLayout>
0049 #include <QDomDocument>
0050 #include <QTextCodec>
0051 #include <QUrlQuery>
0052 #include <QThread>
0053 
0054 namespace {
0055   static const int MUSICBRAINZ_MAX_RETURNS_TOTAL = 10;
0056   static const char* MUSICBRAINZ_API_URL = "https://musicbrainz.org/ws/2/";
0057 }
0058 
0059 using namespace Tellico;
0060 using Tellico::Fetch::MusicBrainzFetcher;
0061 
0062 MusicBrainzFetcher::MusicBrainzFetcher(QObject* parent_)
0063     : Fetcher(parent_), m_xsltHandler(nullptr),
0064       m_limit(MUSICBRAINZ_MAX_RETURNS_TOTAL), m_total(-1), m_offset(0),
0065       m_job(nullptr), m_started(false) {
0066 }
0067 
0068 MusicBrainzFetcher::~MusicBrainzFetcher() {
0069   delete m_xsltHandler;
0070   m_xsltHandler = nullptr;
0071 }
0072 
0073 QString MusicBrainzFetcher::source() const {
0074   return m_name.isEmpty() ? defaultName() : m_name;
0075 }
0076 
0077 bool MusicBrainzFetcher::canSearch(Fetch::FetchKey k) const {
0078   return k == Title || k == Person || k == Keyword || k == UPC;
0079 }
0080 
0081 bool MusicBrainzFetcher::canFetch(int type) const {
0082   return type == Data::Collection::Album;
0083 }
0084 
0085 void MusicBrainzFetcher::readConfigHook(const KConfigGroup&) {
0086 }
0087 
0088 void MusicBrainzFetcher::setLimit(int limit_) {
0089   m_limit = qBound(1, limit_, MUSICBRAINZ_MAX_RETURNS_TOTAL);
0090 }
0091 
0092 void MusicBrainzFetcher::search() {
0093   m_started = true;
0094   m_total = -1;
0095   m_offset = 0;
0096   doSearch();
0097 }
0098 
0099 void MusicBrainzFetcher::continueSearch() {
0100   m_started = true;
0101   doSearch();
0102 }
0103 
0104 void MusicBrainzFetcher::doSearch() {
0105   QUrl u(QString::fromLatin1(MUSICBRAINZ_API_URL));
0106   // all searches are for musical releases since Tellico only tracks albums
0107   u.setPath(u.path() + QStringLiteral("release"));
0108 
0109   QString queryString;
0110   switch(request().key()) {
0111     case Title:
0112       queryString = QStringLiteral("release:\"%1\"").arg(request().value());
0113       break;
0114 
0115     case Person:
0116       queryString = QStringLiteral("artist:\"%1\"").arg(request().value());
0117       break;
0118 
0119     case UPC:
0120       queryString = QStringLiteral("barcode:\"%1\"").arg(request().value());
0121       break;
0122 
0123     case Keyword:
0124     case Raw:
0125       queryString = request().value();
0126       break;
0127 
0128     default:
0129       myWarning() << source() << "- key not recognized:" << request().key();
0130       stop();
0131       return;
0132   }
0133   QUrlQuery q;
0134   q.addQueryItem(QStringLiteral("query"), queryString);
0135   q.addQueryItem(QStringLiteral("limit"), QString::number(m_limit));
0136   q.addQueryItem(QStringLiteral("offset"), QString::number(m_offset));
0137   q.addQueryItem(QStringLiteral("fmt"), QStringLiteral("xml"));
0138   u.setQuery(q);
0139 //  myDebug() << "url: " << u.url();
0140 
0141   m_requestTimer.start();
0142   m_job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo);
0143   // see https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#Provide_meaningful_User-Agent_strings
0144   m_job->addMetaData(QLatin1String("SendUserAgent"), QLatin1String("true"));
0145   m_job->addMetaData(QStringLiteral("UserAgent"), QStringLiteral("Tellico/%1 ( https://tellico-project.org )")
0146                                                                 .arg(QStringLiteral(TELLICO_VERSION)));
0147   KJobWidgets::setWindow(m_job, GUI::Proxy::widget());
0148   connect(m_job.data(), &KJob::result,
0149           this, &MusicBrainzFetcher::slotComplete);
0150 }
0151 
0152 void MusicBrainzFetcher::stop() {
0153   if(!m_started) {
0154     return;
0155   }
0156   if(m_job) {
0157     m_job->kill();
0158     m_job = nullptr;
0159   }
0160   m_started = false;
0161   emit signalDone(this);
0162 }
0163 
0164 void MusicBrainzFetcher::slotComplete(KJob* ) {
0165   if(m_job->error()) {
0166     m_job->uiDelegate()->showErrorMessage();
0167     stop();
0168     return;
0169   }
0170 
0171   QByteArray data = m_job->data();
0172   if(data.isEmpty()) {
0173     myDebug() << "no data";
0174     stop();
0175     return;
0176   }
0177   // see bug 319662. If fetcher is cancelled, job is killed
0178   // if the pointer is retained, it gets double-deleted
0179   m_job = nullptr;
0180 
0181 #if 0
0182   myWarning() << "Remove debug from musicbrainzfetcher.cpp";
0183   QFile f(QStringLiteral("/tmp/test.xml"));
0184   if(f.open(QIODevice::WriteOnly)) {
0185     QTextStream t(&f);
0186     t.setCodec("UTF-8");
0187     t << data;
0188   }
0189   f.close();
0190 #endif
0191 
0192   if(m_total == -1) {
0193     QDomDocument dom;
0194     if(!dom.setContent(data, false)) {
0195       myWarning() << "server did not return valid XML:" << data;
0196       stop();
0197       return;
0198     }
0199     // total is /metadata/release-list/@count
0200     QDomNode n = dom.documentElement().namedItem(QStringLiteral("release-list"));
0201     QDomElement e = n.toElement();
0202     if(!e.isNull()) {
0203       m_total = e.attribute(QStringLiteral("count")).toInt();
0204 //      myDebug() << "total = " << m_total;
0205     }
0206   }
0207 
0208   if(!m_xsltHandler) {
0209     initXSLTHandler();
0210     if(!m_xsltHandler) { // probably an error somewhere in the stylesheet loading
0211       stop();
0212       return;
0213     }
0214   }
0215 
0216   // assume always utf-8
0217   QString str = m_xsltHandler->applyStylesheet(QString::fromUtf8(data.constData(), data.size()));
0218   Import::TellicoImporter imp(str);
0219   // be quiet when loading images
0220   imp.setOptions(imp.options() ^ Import::ImportShowImageErrors);
0221   Data::CollPtr coll = imp.collection();
0222   if(!coll) {
0223     myDebug() << "no collection pointer";
0224     stop();
0225     return;
0226   }
0227 
0228   int count = 0;
0229   Data::EntryList entries = coll->entries();
0230   foreach(Data::EntryPtr entry, entries) {
0231     if(count >= m_limit) {
0232       break;
0233     }
0234     if(!m_started) {
0235       // might get aborted
0236       break;
0237     }
0238 
0239     FetchResult* r = new FetchResult(this, entry);
0240     m_entries.insert(r->uid, Data::EntryPtr(entry));
0241     emit signalResultFound(r);
0242     ++count;
0243   }
0244 
0245   m_offset += count;
0246   m_hasMoreResults = (m_total > 0 && m_offset <= m_total);
0247 
0248   stop(); // required
0249 }
0250 
0251 Tellico::Data::EntryPtr MusicBrainzFetcher::fetchEntryHook(uint uid_) {
0252   Data::EntryPtr entry = m_entries[uid_];
0253   if(!entry) {
0254     myWarning() << "no entry in dict";
0255     return Data::EntryPtr();
0256   }
0257 
0258   QString mbid = entry->field(QStringLiteral("mbid"));
0259   if(mbid.isEmpty()) {
0260     return entry;
0261   }
0262 
0263   QUrl u(QString::fromLatin1(MUSICBRAINZ_API_URL));
0264   u.setPath(u.path() + QStringLiteral("release/") + mbid);
0265   QUrlQuery q;
0266   q.addQueryItem(QStringLiteral("fmt"), QStringLiteral("xml"));
0267   q.addQueryItem(QStringLiteral("inc"), QStringLiteral("artists+recordings+release-groups+labels+url-rels"));
0268   u.setQuery(q);
0269 //  myDebug() << u;
0270 
0271   // limit to one request per second
0272   while(m_requestTimer.elapsed() < 1000) {
0273     QThread::msleep(300);
0274   }
0275   m_requestTimer.start();
0276 
0277   KIO::StoredTransferJob* dataJob = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo);
0278   dataJob->addMetaData(QLatin1String("SendUserAgent"), QLatin1String("true"));
0279   dataJob->addMetaData(QStringLiteral("UserAgent"), QStringLiteral("Tellico/%1 ( http://tellico-project.org )")
0280                                                                 .arg(QStringLiteral(TELLICO_VERSION)));
0281   if(!dataJob->exec()) {
0282     myDebug() << "Failed to load" << u;
0283     return entry;
0284   }
0285   const QString output = XMLHandler::readXMLData(dataJob->data());
0286 #if 0
0287   myWarning() << "Remove output debug from musicbrainzfetcher.cpp";
0288   QFile f(QStringLiteral("/tmp/test2.xml"));
0289   if(f.open(QIODevice::WriteOnly)) {
0290     QTextStream t(&f);
0291     t.setCodec("UTF-8");
0292     t << output;
0293   }
0294   f.close();
0295 #endif
0296 
0297   Import::TellicoImporter imp(m_xsltHandler->applyStylesheet(output));
0298   // be quiet when loading images
0299   imp.setOptions(imp.options() ^ Import::ImportShowImageErrors);
0300   Data::CollPtr coll = imp.collection();
0301   if(!coll || coll->entries().isEmpty()) {
0302     myWarning() << "no collection pointer or no entries";
0303     return entry;
0304   }
0305 
0306   if(coll->entryCount() > 1) {
0307     myDebug() << "weird, more than one entry found";
0308   }
0309 
0310   // don't want to include id
0311   coll->removeField(QStringLiteral("mbid"));
0312 
0313   entry = coll->entries().front();
0314   m_entries.insert(uid_, entry); // replaces old value
0315   return entry;
0316 }
0317 
0318 void MusicBrainzFetcher::initXSLTHandler() {
0319   QString xsltfile = DataFileRegistry::self()->locate(QStringLiteral("musicbrainz2tellico.xsl"));
0320   if(xsltfile.isEmpty()) {
0321     myWarning() << "can not locate musicbrainz2tellico.xsl.";
0322     return;
0323   }
0324 
0325   QUrl u = QUrl::fromLocalFile(xsltfile);
0326 
0327   delete m_xsltHandler;
0328   m_xsltHandler = new XSLTHandler(u);
0329   if(!m_xsltHandler->isValid()) {
0330     myWarning() << "error in musicbrainz2tellico.xsl.";
0331     delete m_xsltHandler;
0332     m_xsltHandler = nullptr;
0333     return;
0334   }
0335 }
0336 
0337 Tellico::Fetch::FetchRequest MusicBrainzFetcher::updateRequest(Data::EntryPtr entry_) {
0338   const QString barcode = entry_->field(QStringLiteral("barcode"));
0339   if(!barcode.isEmpty()) {
0340     return FetchRequest(UPC, barcode);
0341   }
0342 
0343   const QString title = entry_->field(QStringLiteral("title"));
0344   const QString artist = entry_->field(QStringLiteral("artist"));
0345   if(artist.isEmpty() && !title.isEmpty()) {
0346     return FetchRequest(Title, title);
0347   } else if(title.isEmpty() && !artist.isEmpty()) {
0348     return FetchRequest(Person, artist);
0349   } else if(!title.isEmpty() && !artist.isEmpty()) {
0350     return FetchRequest(Raw, QStringLiteral("release:\"%1\" AND artist:\"%2\"").arg(title, artist));
0351   }
0352   return FetchRequest();
0353 }
0354 
0355 Tellico::Fetch::ConfigWidget* MusicBrainzFetcher::configWidget(QWidget* parent_) const {
0356   return new MusicBrainzFetcher::ConfigWidget(parent_, this);
0357 }
0358 
0359 QString MusicBrainzFetcher::defaultName() {
0360   return QStringLiteral("MusicBrainz"); // no translation
0361 }
0362 
0363 QString MusicBrainzFetcher::defaultIcon() {
0364   return favIcon("https://musicbrainz.org");
0365 }
0366 
0367 Tellico::StringHash MusicBrainzFetcher::allOptionalFields() {
0368   StringHash hash;
0369 //  hash[QStringLiteral("nationality")] = i18n("Nationality");
0370   hash[QStringLiteral("barcode")] = i18n("Barcode");
0371   return hash;
0372 }
0373 
0374 MusicBrainzFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const MusicBrainzFetcher* fetcher_)
0375     : Fetch::ConfigWidget(parent_) {
0376   QVBoxLayout* l = new QVBoxLayout(optionsWidget());
0377   l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget()));
0378   l->addStretch();
0379 
0380   // now add additional fields widget
0381   addFieldsWidget(MusicBrainzFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList());
0382 }
0383 
0384 void MusicBrainzFetcher::ConfigWidget::saveConfigHook(KConfigGroup&) {
0385 }
0386 
0387 QString MusicBrainzFetcher::ConfigWidget::preferredName() const {
0388   return MusicBrainzFetcher::defaultName();
0389 }