File indexing completed on 2024-05-05 04:48:21
0001 /**************************************************************************************** 0002 * Copyright (c) 2009 Rick W. Chen <stuffcorpse@archlinux.us> * 0003 * * 0004 * This program is free software; you can redistribute it and/or modify it under * 0005 * the terms of the GNU General Public License as published by the Free Software * 0006 * Foundation; either version 2 of the License, or (at your option) any later * 0007 * version. * 0008 * * 0009 * This program is distributed in the hope that it will be useful, but WITHOUT ANY * 0010 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * 0011 * PARTICULAR PURPOSE. See the GNU General Public License for more details. * 0012 * * 0013 * You should have received a copy of the GNU General Public License along with * 0014 * this program. If not, see <http://www.gnu.org/licenses/>. * 0015 ****************************************************************************************/ 0016 0017 #define DEBUG_PREFIX "CoverFetchUnit" 0018 0019 #include "CoverFetchUnit.h" 0020 0021 #include "core/support/Amarok.h" 0022 #include "core/support/Debug.h" 0023 0024 #include <QRegExp> 0025 #include <QSet> 0026 #include <QUrlQuery> 0027 #include <QXmlStreamReader> 0028 0029 #include <KLocalizedString> 0030 0031 /* 0032 * CoverFetchUnit 0033 */ 0034 0035 CoverFetchUnit::CoverFetchUnit( const Meta::AlbumPtr &album, 0036 const CoverFetchPayload *payload, 0037 CoverFetch::Option opt ) 0038 : m_album( album ) 0039 , m_options( opt ) 0040 , m_payload( payload ) 0041 { 0042 } 0043 0044 CoverFetchUnit::CoverFetchUnit( const CoverFetchPayload *payload, CoverFetch::Option opt ) 0045 : m_album( payload->album() ) 0046 , m_options( opt ) 0047 , m_payload( payload ) 0048 { 0049 } 0050 0051 CoverFetchUnit::CoverFetchUnit( const CoverFetchSearchPayload *payload ) 0052 : m_album( payload->album() ) 0053 , m_options( CoverFetch::WildInteractive ) 0054 , m_payload( payload ) 0055 { 0056 } 0057 0058 CoverFetchUnit::~CoverFetchUnit() 0059 { 0060 delete m_payload; 0061 } 0062 0063 Meta::AlbumPtr 0064 CoverFetchUnit::album() const 0065 { 0066 return m_album; 0067 } 0068 0069 const QStringList & 0070 CoverFetchUnit::errors() const 0071 { 0072 return m_errors; 0073 } 0074 0075 CoverFetch::Option 0076 CoverFetchUnit::options() const 0077 { 0078 return m_options; 0079 } 0080 0081 const CoverFetchPayload * 0082 CoverFetchUnit::payload() const 0083 { 0084 return m_payload; 0085 } 0086 0087 bool 0088 CoverFetchUnit::isInteractive() const 0089 { 0090 bool interactive( false ); 0091 switch( m_options ) 0092 { 0093 case CoverFetch::Automatic: 0094 interactive = false; 0095 break; 0096 case CoverFetch::Interactive: 0097 case CoverFetch::WildInteractive: 0098 interactive = true; 0099 break; 0100 } 0101 return interactive; 0102 } 0103 0104 template< typename T > 0105 void 0106 CoverFetchUnit::addError( const T &error ) 0107 { 0108 m_errors << error; 0109 } 0110 0111 bool CoverFetchUnit::operator==( const CoverFetchUnit &other ) const 0112 { 0113 return (m_album == other.m_album) && (m_options == other.m_options) && (m_payload == other.m_payload); 0114 } 0115 0116 bool CoverFetchUnit::operator!=( const CoverFetchUnit &other ) const 0117 { 0118 return !( *this == other ); 0119 } 0120 0121 0122 /* 0123 * CoverFetchPayload 0124 */ 0125 0126 CoverFetchPayload::CoverFetchPayload( const Meta::AlbumPtr &album, 0127 CoverFetchPayload::Type type, 0128 CoverFetch::Source src ) 0129 : m_src( src ) 0130 , m_album( album ) 0131 , m_method( ( type == Search ) ? QString( "album.search" ) 0132 : album && album->hasAlbumArtist() ? QString( "album.getinfo" ) 0133 : QString( "album.search" ) ) 0134 , m_type( type ) 0135 { 0136 } 0137 0138 CoverFetchPayload::~CoverFetchPayload() 0139 { 0140 } 0141 0142 Meta::AlbumPtr 0143 CoverFetchPayload::album() const 0144 { 0145 return m_album; 0146 } 0147 0148 QString 0149 CoverFetchPayload::sanitizeQuery( const QString &query ) 0150 { 0151 QString cooked( query ); 0152 cooked.remove( QChar('?') ); 0153 return cooked; 0154 } 0155 0156 CoverFetch::Source 0157 CoverFetchPayload::source() const 0158 { 0159 return m_src; 0160 } 0161 0162 CoverFetchPayload::Type 0163 CoverFetchPayload::type() const 0164 { 0165 return m_type; 0166 } 0167 0168 const CoverFetch::Urls & 0169 CoverFetchPayload::urls() const 0170 { 0171 return m_urls; 0172 } 0173 0174 const QString 0175 CoverFetchPayload::sourceString() const 0176 { 0177 QString source; 0178 switch( m_src ) 0179 { 0180 case CoverFetch::LastFm: 0181 source = "Last.fm"; 0182 break; 0183 case CoverFetch::Google: 0184 source = "Google"; 0185 break; 0186 case CoverFetch::Discogs: 0187 source = "Discogs"; 0188 break; 0189 default: 0190 source = "Unknown"; 0191 } 0192 return source; 0193 } 0194 0195 bool 0196 CoverFetchPayload::isPrepared() const 0197 { 0198 return !m_urls.isEmpty(); 0199 } 0200 0201 /* 0202 * CoverFetchInfoPayload 0203 */ 0204 0205 CoverFetchInfoPayload::CoverFetchInfoPayload( const Meta::AlbumPtr &album, const CoverFetch::Source src ) 0206 : CoverFetchPayload( album, CoverFetchPayload::Info, src ) 0207 { 0208 prepareUrls(); 0209 } 0210 0211 CoverFetchInfoPayload::CoverFetchInfoPayload( const CoverFetch::Source src, const QByteArray &data ) 0212 : CoverFetchPayload( Meta::AlbumPtr( nullptr ), CoverFetchPayload::Info, src ) 0213 { 0214 switch( src ) 0215 { 0216 default: 0217 prepareUrls(); 0218 break; 0219 case CoverFetch::Discogs: 0220 prepareDiscogsUrls( data ); 0221 break; 0222 } 0223 } 0224 0225 CoverFetchInfoPayload::~CoverFetchInfoPayload() 0226 { 0227 } 0228 0229 void 0230 CoverFetchInfoPayload::prepareUrls() 0231 { 0232 QUrl url; 0233 CoverFetch::Metadata metadata; 0234 0235 switch( m_src ) 0236 { 0237 default: 0238 case CoverFetch::LastFm: 0239 url.setScheme( "http" ); 0240 url.setHost( "ws.audioscrobbler.com" ); 0241 url.setPath( "/2.0/" ); 0242 QUrlQuery query; 0243 query.addQueryItem( "api_key", Amarok::lastfmApiKey() ); 0244 query.addQueryItem( "album", sanitizeQuery( album()->name() ) ); 0245 0246 if( album()->hasAlbumArtist() ) 0247 { 0248 query.addQueryItem( "artist", sanitizeQuery( album()->albumArtist()->name() ) ); 0249 } 0250 query.addQueryItem( "method", method() ); 0251 url.setQuery( query ); 0252 0253 metadata[ "source" ] = "Last.fm"; 0254 metadata[ "method" ] = method(); 0255 break; 0256 } 0257 0258 if( url.isValid() ) 0259 m_urls.insert( url, metadata ); 0260 } 0261 0262 void 0263 CoverFetchInfoPayload::prepareDiscogsUrls( const QByteArray &data ) 0264 { 0265 QXmlStreamReader xml( QString::fromUtf8(data) ); 0266 while( !xml.atEnd() && !xml.hasError() ) 0267 { 0268 xml.readNext(); 0269 if( xml.isStartElement() && xml.name() == "searchresults" ) 0270 { 0271 while( !xml.atEnd() && !xml.hasError() ) 0272 { 0273 xml.readNext(); 0274 const QStringRef &n = xml.name(); 0275 if( xml.isEndElement() && n == "searchresults" ) 0276 break; 0277 if( !xml.isStartElement() ) 0278 continue; 0279 if( n == "result" ) 0280 { 0281 while( !xml.atEnd() && !xml.hasError() ) 0282 { 0283 xml.readNext(); 0284 if( xml.isEndElement() && n == "result" ) 0285 break; 0286 if( !xml.isStartElement() ) 0287 continue; 0288 if( xml.name() == "uri" ) 0289 { 0290 QUrl releaseUrl( xml.readElementText() ); 0291 QString releaseStr = releaseUrl.adjusted(QUrl::StripTrailingSlash).toString(); 0292 QString releaseId = releaseStr.split( QLatin1Char('/') ).last(); 0293 0294 QUrl url; 0295 url.setScheme( "http" ); 0296 url.setHost( "www.discogs.com" ); 0297 url.setPath( "/release/" + releaseId ); 0298 QUrlQuery query; 0299 query.addQueryItem( "api_key", Amarok::discogsApiKey() ); 0300 query.addQueryItem( "f", "xml" ); 0301 url.setQuery( query ); 0302 0303 CoverFetch::Metadata metadata; 0304 metadata[ "source" ] = "Discogs"; 0305 0306 if( url.isValid() ) 0307 m_urls.insert( url, metadata ); 0308 } 0309 else 0310 xml.skipCurrentElement(); 0311 } 0312 } 0313 else 0314 xml.skipCurrentElement(); 0315 } 0316 } 0317 } 0318 } 0319 0320 /* 0321 * CoverFetchSearchPayload 0322 */ 0323 0324 CoverFetchSearchPayload::CoverFetchSearchPayload( const QString &query, 0325 const CoverFetch::Source src, 0326 unsigned int page, 0327 const Meta::AlbumPtr &album ) 0328 : CoverFetchPayload( album, CoverFetchPayload::Search, src ) 0329 , m_page( page ) 0330 , m_query( query ) 0331 { 0332 prepareUrls(); 0333 } 0334 0335 CoverFetchSearchPayload::~CoverFetchSearchPayload() 0336 { 0337 } 0338 0339 QString 0340 CoverFetchSearchPayload::query() const 0341 { 0342 return m_query; 0343 } 0344 0345 void 0346 CoverFetchSearchPayload::prepareUrls() 0347 { 0348 QUrl url; 0349 QUrlQuery query; 0350 url.setScheme( "http" ); 0351 CoverFetch::Metadata metadata; 0352 0353 switch( m_src ) 0354 { 0355 default: 0356 case CoverFetch::LastFm: 0357 url.setHost( "ws.audioscrobbler.com" ); 0358 url.setPath( "/2.0/" ); 0359 query.addQueryItem( "api_key", Amarok::lastfmApiKey() ); 0360 query.addQueryItem( "limit", QString::number( 20 ) ); 0361 query.addQueryItem( "page", QString::number( m_page ) ); 0362 query.addQueryItem( "album", sanitizeQuery( m_query ) ); 0363 query.addQueryItem( "method", method() ); 0364 metadata[ "source" ] = "Last.fm"; 0365 metadata[ "method" ] = method(); 0366 break; 0367 0368 case CoverFetch::Discogs: 0369 debug() << "Setting up a Discogs fetch"; 0370 url.setHost( "www.discogs.com" ); 0371 url.setPath( "/search" ); 0372 query.addQueryItem( "api_key", Amarok::discogsApiKey() ); 0373 query.addQueryItem( "page", QString::number( m_page + 1 ) ); 0374 query.addQueryItem( "type", "all" ); 0375 query.addQueryItem( "q", sanitizeQuery( m_query ) ); 0376 query.addQueryItem( "f", "xml" ); 0377 debug() << "Discogs Url: " << url; 0378 metadata[ "source" ] = "Discogs"; 0379 break; 0380 0381 case CoverFetch::Google: 0382 url.setHost( "images.google.com" ); 0383 url.setPath( "/images" ); 0384 query.addQueryItem( "q", sanitizeQuery( m_query ) ); 0385 query.addQueryItem( "gbv", QChar( '1' ) ); 0386 query.addQueryItem( "filter", QChar( '1' ) ); 0387 query.addQueryItem( "start", QString::number( 20 * m_page ) ); 0388 metadata[ "source" ] = "Google"; 0389 break; 0390 } 0391 url.setQuery( query ); 0392 debug() << "Fetching From URL: " << url; 0393 if( url.isValid() ) 0394 m_urls.insert( url, metadata ); 0395 } 0396 0397 /* 0398 * CoverFetchArtPayload 0399 */ 0400 0401 CoverFetchArtPayload::CoverFetchArtPayload( const Meta::AlbumPtr &album, 0402 const CoverFetch::ImageSize size, 0403 const CoverFetch::Source src, 0404 bool wild ) 0405 : CoverFetchPayload( album, CoverFetchPayload::Art, src ) 0406 , m_size( size ) 0407 , m_wild( wild ) 0408 { 0409 } 0410 0411 CoverFetchArtPayload::CoverFetchArtPayload( const CoverFetch::ImageSize size, 0412 const CoverFetch::Source src, 0413 bool wild ) 0414 : CoverFetchPayload( Meta::AlbumPtr( nullptr ), CoverFetchPayload::Art, src ) 0415 , m_size( size ) 0416 , m_wild( wild ) 0417 { 0418 } 0419 0420 CoverFetchArtPayload::~CoverFetchArtPayload() 0421 { 0422 } 0423 0424 bool 0425 CoverFetchArtPayload::isWild() const 0426 { 0427 return m_wild; 0428 } 0429 0430 CoverFetch::ImageSize 0431 CoverFetchArtPayload::imageSize() const 0432 { 0433 return m_size; 0434 } 0435 0436 void 0437 CoverFetchArtPayload::setXml( const QByteArray &xml ) 0438 { 0439 m_xml = QString::fromUtf8( xml ); 0440 prepareUrls(); 0441 } 0442 0443 void 0444 CoverFetchArtPayload::prepareUrls() 0445 { 0446 if( m_src == CoverFetch::Google ) 0447 { 0448 // google is special 0449 prepareGoogleUrls(); 0450 return; 0451 } 0452 0453 QXmlStreamReader xml( m_xml ); 0454 xml.setNamespaceProcessing( false ); 0455 switch( m_src ) 0456 { 0457 default: 0458 case CoverFetch::LastFm: 0459 prepareLastFmUrls( xml ); 0460 break; 0461 case CoverFetch::Discogs: 0462 prepareDiscogsUrls( xml ); 0463 break; 0464 } 0465 0466 if( xml.hasError() ) 0467 { 0468 warning() << QString( "Error occurred when preparing %1 urls for %2: %3" ) 0469 .arg( sourceString(), (album() ? album()->name() : "'unknown'"), xml.errorString() ); 0470 debug() << urls(); 0471 } 0472 } 0473 0474 void 0475 CoverFetchArtPayload::prepareDiscogsUrls( QXmlStreamReader &xml ) 0476 { 0477 while( !xml.atEnd() && !xml.hasError() ) 0478 { 0479 xml.readNext(); 0480 if( !xml.isStartElement() || xml.name() != "release" ) 0481 continue; 0482 0483 const QString releaseId = xml.attributes().value( "id" ).toString(); 0484 while( !xml.atEnd() && !xml.hasError() ) 0485 { 0486 xml.readNext(); 0487 const QStringRef &n = xml.name(); 0488 if( xml.isEndElement() && n == "release" ) 0489 break; 0490 if( !xml.isStartElement() ) 0491 continue; 0492 0493 CoverFetch::Metadata metadata; 0494 metadata[ "source" ] = "Discogs"; 0495 if( n == "title" ) 0496 metadata[ "title" ] = xml.readElementText(); 0497 else if( n == "country" ) 0498 metadata[ "country" ] = xml.readElementText(); 0499 else if( n == "released" ) 0500 metadata[ "released" ] = xml.readElementText(); 0501 else if( n == "notes" ) 0502 metadata[ "notes" ] = xml.readElementText(); 0503 else if( n == "images" ) 0504 { 0505 while( !xml.atEnd() && !xml.hasError() ) 0506 { 0507 xml.readNext(); 0508 if( xml.isEndElement() && xml.name() == "images" ) 0509 break; 0510 if( !xml.isStartElement() ) 0511 continue; 0512 if( xml.name() == "image" ) 0513 { 0514 const QXmlStreamAttributes &attr = xml.attributes(); 0515 const QUrl thburl( attr.value( "uri150" ).toString() ); 0516 const QUrl uri( attr.value( "uri" ).toString() ); 0517 const QUrl url = (m_size == CoverFetch::ThumbSize) ? thburl : uri; 0518 if( !url.isValid() ) 0519 continue; 0520 0521 metadata[ "releaseid" ] = releaseId; 0522 metadata[ "releaseurl" ] = "http://discogs.com/release/" + releaseId; 0523 metadata[ "normalarturl" ] = uri.url(); 0524 metadata[ "thumbarturl" ] = thburl.url(); 0525 metadata[ "width" ] = attr.value( "width" ).toString(); 0526 metadata[ "height" ] = attr.value( "height" ).toString(); 0527 metadata[ "type" ] = attr.value( "type" ).toString(); 0528 m_urls.insert( url, metadata ); 0529 } 0530 else 0531 xml.skipCurrentElement(); 0532 } 0533 } 0534 else 0535 xml.skipCurrentElement(); 0536 } 0537 } 0538 } 0539 0540 void 0541 CoverFetchArtPayload::prepareGoogleUrls() 0542 { 0543 // code based on Audex CDDA Extractor 0544 QRegExp rx( "<a\\shref=\"(\\/imgres\\?imgurl=[^\"]+)\">[\\s\\n]*<img[^>]+src=\"([^\"]+)\"" ); 0545 rx.setCaseSensitivity( Qt::CaseInsensitive ); 0546 rx.setMinimal( true ); 0547 0548 int pos = 0; 0549 QString html = m_xml.replace( QLatin1String("&"), QLatin1String("&") ); 0550 0551 while( ( (pos = rx.indexIn( html, pos ) ) != -1 ) ) 0552 { 0553 QUrl url( "http://www.google.com" + rx.cap( 1 ) ); 0554 QUrlQuery query( url.query() ); 0555 0556 CoverFetch::Metadata metadata; 0557 metadata[ "width" ] = query.queryItemValue( "w" ); 0558 metadata[ "height" ] = query.queryItemValue( "h" ); 0559 metadata[ "size" ] = query.queryItemValue( "sz" ); 0560 metadata[ "imgrefurl" ] = query.queryItemValue( "imgrefurl" ); 0561 metadata[ "normalarturl" ] = query.queryItemValue("imgurl"); 0562 metadata[ "source" ] = "Google"; 0563 0564 if( !rx.cap( 2 ).isEmpty() ) 0565 metadata[ "thumbarturl" ] = rx.cap( 2 ); 0566 0567 url.clear(); 0568 switch( m_size ) 0569 { 0570 default: 0571 case CoverFetch::ThumbSize: 0572 url = QUrl( metadata.value( "thumbarturl" ) ); 0573 break; 0574 case CoverFetch::NormalSize: 0575 url = QUrl( metadata.value( "normalarturl" ) ); 0576 break; 0577 } 0578 0579 if( url.isValid() ) 0580 m_urls.insert( url, metadata ); 0581 0582 pos += rx.matchedLength(); 0583 } 0584 } 0585 0586 void 0587 CoverFetchArtPayload::prepareLastFmUrls( QXmlStreamReader &xml ) 0588 { 0589 QSet<QString> artistSet; 0590 if( method() == "album.getinfo" ) 0591 { 0592 artistSet << normalize( ( album() && album()->albumArtist() ) 0593 ? album()->albumArtist()->name() 0594 : i18n( "Unknown Artist" ) ); 0595 } 0596 else if( method() == "album.search" ) 0597 { 0598 if( !m_wild && album() ) 0599 { 0600 const Meta::TrackList tracks = album()->tracks(); 0601 QStringList artistNames( "Various Artists" ); 0602 foreach( const Meta::TrackPtr &track, tracks ) 0603 artistNames << ( track->artist() ? track->artist()->name() 0604 : i18n( "Unknown Artist" ) ); 0605 QStringList artistNamesNormalized = normalize( artistNames ); 0606 QSet<QString> addArtistSet(artistNamesNormalized.begin(), artistNamesNormalized.end()); 0607 artistSet += addArtistSet; 0608 } 0609 } 0610 else return; 0611 0612 while( !xml.atEnd() && !xml.hasError() ) 0613 { 0614 xml.readNext(); 0615 if( !xml.isStartElement() || xml.name() != "album" ) 0616 continue; 0617 0618 QHash<QString, QString> coverUrlHash; 0619 CoverFetch::Metadata metadata; 0620 metadata[ "source" ] = "Last.fm"; 0621 while( !xml.atEnd() && !xml.hasError() ) 0622 { 0623 xml.readNext(); 0624 const QStringRef &n = xml.name(); 0625 if( xml.isEndElement() && n == "album" ) 0626 break; 0627 if( !xml.isStartElement() ) 0628 continue; 0629 0630 if( n == "name" ) 0631 { 0632 metadata[ "name" ] = xml.readElementText(); 0633 } 0634 else if( n == "artist" ) 0635 { 0636 const QString &artist = xml.readElementText(); 0637 if( !artistSet.contains( artist ) ) 0638 continue; 0639 metadata[ "artist" ] = artist; 0640 } 0641 else if( n == "url" ) 0642 { 0643 metadata[ "releaseurl" ] = xml.readElementText(); 0644 } 0645 else if( n == "image" ) 0646 { 0647 QString sizeStr = xml.attributes().value("size").toString(); 0648 coverUrlHash[ sizeStr ] = xml.readElementText(); 0649 } 0650 } 0651 0652 QStringList acceptableSizes; 0653 acceptableSizes << "large" << "medium" << "small"; 0654 metadata[ "thumbarturl" ] = firstAvailableValue( acceptableSizes, coverUrlHash ); 0655 0656 acceptableSizes.clear(); 0657 acceptableSizes << "extralarge" << "large"; 0658 metadata[ "normalarturl" ] = firstAvailableValue( acceptableSizes, coverUrlHash ); 0659 0660 QUrl url( m_size == CoverFetch::ThumbSize ? metadata["thumbarturl"] : metadata["normalarturl"] ); 0661 if( url.isValid() ) 0662 m_urls.insert( url , metadata ); 0663 } 0664 } 0665 0666 QString 0667 CoverFetchArtPayload::firstAvailableValue( const QStringList &keys, const QHash<QString, QString> &hash ) 0668 { 0669 for( int i = 0, size = keys.size(); i < size; ++i ) 0670 { 0671 QString value( hash.value( keys.at(i) ) ); 0672 if( !value.isEmpty() ) 0673 return value; 0674 } 0675 return QString(); 0676 } 0677 0678 QString 0679 CoverFetchArtPayload::normalize( const QString &raw ) 0680 { 0681 const QRegExp spaceRegExp = QRegExp( "\\s" ); 0682 return raw.toLower().remove( spaceRegExp ).normalized( QString::NormalizationForm_KC ); 0683 } 0684 0685 QStringList 0686 CoverFetchArtPayload::normalize( const QStringList &rawList ) 0687 { 0688 QStringList cooked; 0689 foreach( const QString &raw, rawList ) 0690 { 0691 cooked << normalize( raw ); 0692 } 0693 return cooked; 0694 } 0695