File indexing completed on 2024-05-12 16:45:57

0001 /***************************************************************************
0002     Copyright (C) 2009-2020 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() << "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(QStringLiteral("UserAgent"), QStringLiteral("Tellico/%1 ( http://tellico-project.org )")
0145                                                                 .arg(QStringLiteral(TELLICO_VERSION)));
0146   KJobWidgets::setWindow(m_job, GUI::Proxy::widget());
0147   connect(m_job.data(), &KJob::result,
0148           this, &MusicBrainzFetcher::slotComplete);
0149 }
0150 
0151 void MusicBrainzFetcher::stop() {
0152   if(!m_started) {
0153     return;
0154   }
0155   if(m_job) {
0156     m_job->kill();
0157     m_job = nullptr;
0158   }
0159   m_started = false;
0160   emit signalDone(this);
0161 }
0162 
0163 void MusicBrainzFetcher::slotComplete(KJob* ) {
0164   if(m_job->error()) {
0165     m_job->uiDelegate()->showErrorMessage();
0166     stop();
0167     return;
0168   }
0169 
0170   QByteArray data = m_job->data();
0171   if(data.isEmpty()) {
0172     myDebug() << "no data";
0173     stop();
0174     return;
0175   }
0176   // see bug 319662. If fetcher is cancelled, job is killed
0177   // if the pointer is retained, it gets double-deleted
0178   m_job = nullptr;
0179 
0180 #if 0
0181   myWarning() << "Remove debug from musicbrainzfetcher.cpp";
0182   QFile f(QStringLiteral("/tmp/test.xml"));
0183   if(f.open(QIODevice::WriteOnly)) {
0184     QTextStream t(&f);
0185     t.setCodec("UTF-8");
0186     t << data;
0187   }
0188   f.close();
0189 #endif
0190 
0191   if(m_total == -1) {
0192     QDomDocument dom;
0193     if(!dom.setContent(data, false)) {
0194       myWarning() << "server did not return valid XML:" << data;
0195       stop();
0196       return;
0197     }
0198     // total is /metadata/release-list/@count
0199     QDomNode n = dom.documentElement().namedItem(QStringLiteral("release-list"));
0200     QDomElement e = n.toElement();
0201     if(!e.isNull()) {
0202       m_total = e.attribute(QStringLiteral("count")).toInt();
0203 //      myDebug() << "total = " << m_total;
0204     }
0205   }
0206 
0207   if(!m_xsltHandler) {
0208     initXSLTHandler();
0209     if(!m_xsltHandler) { // probably an error somewhere in the stylesheet loading
0210       stop();
0211       return;
0212     }
0213   }
0214 
0215   // assume always utf-8
0216   QString str = m_xsltHandler->applyStylesheet(QString::fromUtf8(data.constData(), data.size()));
0217   Import::TellicoImporter imp(str);
0218   // be quiet when loading images
0219   imp.setOptions(imp.options() ^ Import::ImportShowImageErrors);
0220   Data::CollPtr coll = imp.collection();
0221   if(!coll) {
0222     myDebug() << "no collection pointer";
0223     stop();
0224     return;
0225   }
0226 
0227   int count = 0;
0228   Data::EntryList entries = coll->entries();
0229   foreach(Data::EntryPtr entry, entries) {
0230     if(count >= m_limit) {
0231       break;
0232     }
0233     if(!m_started) {
0234       // might get aborted
0235       break;
0236     }
0237 
0238     FetchResult* r = new FetchResult(this, entry);
0239     m_entries.insert(r->uid, Data::EntryPtr(entry));
0240     emit signalResultFound(r);
0241     ++count;
0242   }
0243 
0244   m_offset += count;
0245   m_hasMoreResults = m_offset <= m_total;
0246 
0247   stop(); // required
0248 }
0249 
0250 Tellico::Data::EntryPtr MusicBrainzFetcher::fetchEntryHook(uint uid_) {
0251   Data::EntryPtr entry = m_entries[uid_];
0252   if(!entry) {
0253     myWarning() << "no entry in dict";
0254     return Data::EntryPtr();
0255   }
0256 
0257   QString mbid = entry->field(QStringLiteral("mbid"));
0258   if(mbid.isEmpty()) {
0259     return entry;
0260   }
0261 
0262   QUrl u(QString::fromLatin1(MUSICBRAINZ_API_URL));
0263   u.setPath(u.path() + QStringLiteral("release/") + mbid);
0264   QUrlQuery q;
0265   q.addQueryItem(QStringLiteral("fmt"), QStringLiteral("xml"));
0266   q.addQueryItem(QStringLiteral("inc"), QStringLiteral("artists+recordings+release-groups+labels+url-rels"));
0267   u.setQuery(q);
0268 //  myDebug() << u;
0269 
0270   // limit to one request per second
0271   while(m_requestTimer.elapsed() < 1000) {
0272     QThread::msleep(300);
0273   }
0274   m_requestTimer.start();
0275 
0276   KIO::StoredTransferJob* dataJob = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo);
0277   dataJob->addMetaData(QStringLiteral("UserAgent"), QStringLiteral("Tellico/%1 ( http://tellico-project.org )")
0278                                                                 .arg(QStringLiteral(TELLICO_VERSION)));
0279   if(!dataJob->exec()) {
0280     myDebug() << "Failed to load" << u;
0281     return entry;
0282   }
0283   const QString output = XMLHandler::readXMLData(dataJob->data());
0284 #if 0
0285   myWarning() << "Remove output debug from musicbrainzfetcher.cpp";
0286   QFile f(QStringLiteral("/tmp/test2.xml"));
0287   if(f.open(QIODevice::WriteOnly)) {
0288     QTextStream t(&f);
0289     t.setCodec("UTF-8");
0290     t << output;
0291   }
0292   f.close();
0293 #endif
0294 
0295   Import::TellicoImporter imp(m_xsltHandler->applyStylesheet(output));
0296   // be quiet when loading images
0297   imp.setOptions(imp.options() ^ Import::ImportShowImageErrors);
0298   Data::CollPtr coll = imp.collection();
0299 //  getTracks(entry);
0300   if(!coll || coll->entries().isEmpty()) {
0301     myWarning() << "no collection pointer or no entries";
0302     return entry;
0303   }
0304 
0305   if(coll->entryCount() > 1) {
0306     myDebug() << "weird, more than one entry found";
0307   }
0308 
0309   // don't want to include id
0310   coll->removeField(QStringLiteral("mbid"));
0311 
0312   entry = coll->entries().front();
0313   m_entries.insert(uid_, entry); // replaces old value
0314   return entry;
0315 }
0316 
0317 void MusicBrainzFetcher::initXSLTHandler() {
0318   QString xsltfile = DataFileRegistry::self()->locate(QStringLiteral("musicbrainz2tellico.xsl"));
0319   if(xsltfile.isEmpty()) {
0320     myWarning() << "can not locate musicbrainz2tellico.xsl.";
0321     return;
0322   }
0323 
0324   QUrl u = QUrl::fromLocalFile(xsltfile);
0325 
0326   delete m_xsltHandler;
0327   m_xsltHandler = new XSLTHandler(u);
0328   if(!m_xsltHandler->isValid()) {
0329     myWarning() << "error in musicbrainz2tellico.xsl.";
0330     delete m_xsltHandler;
0331     m_xsltHandler = nullptr;
0332     return;
0333   }
0334 }
0335 
0336 Tellico::Fetch::FetchRequest MusicBrainzFetcher::updateRequest(Data::EntryPtr entry_) {
0337   const QString barcode = entry_->field(QStringLiteral("barcode"));
0338   if(!barcode.isEmpty()) {
0339     return FetchRequest(UPC, barcode);
0340   }
0341 
0342   const QString title = entry_->field(QStringLiteral("title"));
0343   const QString artist = entry_->field(QStringLiteral("artist"));
0344   if(artist.isEmpty() && !title.isEmpty()) {
0345     return FetchRequest(Title, title);
0346   } else if(title.isEmpty() && !artist.isEmpty()) {
0347     return FetchRequest(Person, artist);
0348   } else if(!title.isEmpty() && !artist.isEmpty()) {
0349     return FetchRequest(Raw, QStringLiteral("release:\"%1\" AND artist:\"%2\"").arg(title, artist));
0350   }
0351   return FetchRequest();
0352 }
0353 
0354 Tellico::Fetch::ConfigWidget* MusicBrainzFetcher::configWidget(QWidget* parent_) const {
0355   return new MusicBrainzFetcher::ConfigWidget(parent_, this);
0356 }
0357 
0358 QString MusicBrainzFetcher::defaultName() {
0359   return QStringLiteral("MusicBrainz"); // no translation
0360 }
0361 
0362 QString MusicBrainzFetcher::defaultIcon() {
0363   return favIcon("https://musicbrainz.org");
0364 }
0365 
0366 Tellico::StringHash MusicBrainzFetcher::allOptionalFields() {
0367   StringHash hash;
0368 //  hash[QStringLiteral("nationality")] = i18n("Nationality");
0369   hash[QStringLiteral("barcode")] = i18n("Barcode");
0370   return hash;
0371 }
0372 
0373 MusicBrainzFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const MusicBrainzFetcher* fetcher_)
0374     : Fetch::ConfigWidget(parent_) {
0375   QVBoxLayout* l = new QVBoxLayout(optionsWidget());
0376   l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget()));
0377   l->addStretch();
0378 
0379   // now add additional fields widget
0380   addFieldsWidget(MusicBrainzFetcher::allOptionalFields(), fetcher_ ? fetcher_->optionalFields() : QStringList());
0381 }
0382 
0383 void MusicBrainzFetcher::ConfigWidget::saveConfigHook(KConfigGroup&) {
0384 }
0385 
0386 QString MusicBrainzFetcher::ConfigWidget::preferredName() const {
0387   return MusicBrainzFetcher::defaultName();
0388 }