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("&amp;"), 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