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 }