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 }