File indexing completed on 2024-05-05 04:48:20

0001 /****************************************************************************************
0002  * Copyright (c) 2004 Mark Kretschmann <kretschmann@kde.org>                            *
0003  * Copyright (c) 2004 Stefan Bogner <bochi@online.ms>                                   *
0004  * Copyright (c) 2004 Max Howell <max.howell@methylblue.com>                            *
0005  * Copyright (c) 2007 Dan Meltzer <parallelgrapefruit@gmail.com>                        *
0006  * Copyright (c) 2009 Martin Sandsmark <sandsmark@samfundet.no>                         *
0007  *                                                                                      *
0008  * This program is free software; you can redistribute it and/or modify it under        *
0009  * the terms of the GNU General Public License as published by the Free Software        *
0010  * Foundation; either version 2 of the License, or (at your option) any later           *
0011  * version.                                                                             *
0012  *                                                                                      *
0013  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0014  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0015  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0016  *                                                                                      *
0017  * You should have received a copy of the GNU General Public License along with         *
0018  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0019  ****************************************************************************************/
0020 
0021 #define DEBUG_PREFIX "CoverFetcher"
0022 
0023 #include "CoverFetcher.h"
0024 
0025 #include "amarokconfig.h"
0026 #include "core/logger/Logger.h"
0027 #include "core/meta/Meta.h"
0028 #include "core/support/Amarok.h"
0029 #include "core/support/Components.h"
0030 #include "core/support/Debug.h"
0031 #include "CoverFetchQueue.h"
0032 #include "CoverFoundDialog.h"
0033 #include "CoverFetchUnit.h"
0034 
0035 #include <QBuffer>
0036 #include <QImageReader>
0037 #include <QThread>
0038 #include <QUrl>
0039 
0040 #include <KConfigGroup>
0041 #include <KLocalizedString>
0042 
0043 #include <functional>
0044 #include <thread>
0045 
0046 
0047 CoverFetcher* CoverFetcher::s_instance = nullptr;
0048 
0049 CoverFetcher*
0050 CoverFetcher::instance()
0051 {
0052     return s_instance ? s_instance : new CoverFetcher();
0053 }
0054 
0055 void CoverFetcher::destroy()
0056 {
0057     if( s_instance )
0058     {
0059         delete s_instance;
0060         s_instance = nullptr;
0061     }
0062 }
0063 
0064 CoverFetcher::CoverFetcher()
0065     : QObject()
0066 {
0067     DEBUG_BLOCK
0068     setObjectName( "CoverFetcher" );
0069     qRegisterMetaType<CoverFetchUnit::Ptr>("CoverFetchUnit::Ptr");
0070 
0071     s_instance = this;
0072 
0073     m_queueThread = new QThread( this );
0074     m_queueThread->start();
0075     m_queue = new CoverFetchQueue;
0076     m_queue->moveToThread( m_queueThread );
0077 
0078     connect( m_queue, &CoverFetchQueue::fetchUnitAdded,
0079              this, &CoverFetcher::slotFetch );
0080 
0081     connect( The::networkAccessManager(), &NetworkAccessManagerProxy::requestRedirectedReply,
0082              this, &CoverFetcher::fetchRequestRedirected );
0083 }
0084 
0085 CoverFetcher::~CoverFetcher()
0086 {
0087     m_queue->deleteLater();
0088     m_queueThread->quit();
0089     m_queueThread->wait();
0090 }
0091 
0092 void
0093 CoverFetcher::manualFetch( Meta::AlbumPtr album )
0094 {
0095     debug() << QStringLiteral("Adding interactive cover fetch for: '%1' from %2")
0096         .arg( album->name(),
0097               Amarok::config("Cover Fetcher").readEntry("Interactive Image Source", "LastFm") );
0098     switch( fetchSource() )
0099     {
0100     case CoverFetch::LastFm:
0101         QTimer::singleShot( 0, m_queue, [=] () { m_queue->add( album, CoverFetch::Interactive, fetchSource() ); } );
0102         break;
0103 
0104     case CoverFetch::Discogs:
0105     case CoverFetch::Google:
0106         queueQueryForAlbum( album );
0107         break;
0108 
0109     default:
0110         break;
0111     }
0112 }
0113 
0114 void
0115 CoverFetcher::queueAlbum( Meta::AlbumPtr album )
0116 {
0117     QTimer::singleShot( 0, m_queue, [=] () { m_queue->add( album, CoverFetch::Automatic ); } );
0118     debug() << "Queueing automatic cover fetch for:" << album->name();
0119 }
0120 
0121 void
0122 CoverFetcher::queueAlbums( Meta::AlbumList albums )
0123 {
0124     foreach( Meta::AlbumPtr album, albums )
0125     {
0126         QTimer::singleShot( 0, m_queue, [=] () { m_queue->add( album, CoverFetch::Automatic ); } );
0127     }
0128 }
0129 
0130 void
0131 CoverFetcher::queueQuery( const Meta::AlbumPtr &album, const QString &query, int page )
0132 {
0133     QTimer::singleShot( 0, m_queue, [=] () { m_queue->addQuery( query, fetchSource(), page, album ); } );
0134     debug() << QString( "Queueing cover fetch query: '%1' (page %2)" ).arg( query, QString::number( page ) );
0135 }
0136 
0137 void
0138 CoverFetcher::queueQueryForAlbum( Meta::AlbumPtr album )
0139 {
0140     QString query( album->name() );
0141     if( album->hasAlbumArtist() )
0142         query += ' ' + album->albumArtist()->name();
0143     queueQuery( album, query, 1 );
0144 }
0145 
0146 void
0147 CoverFetcher::slotFetch( CoverFetchUnit::Ptr unit )
0148 {
0149     if( !unit )
0150         return;
0151 
0152     const CoverFetchPayload *payload = unit->payload();
0153     const CoverFetch::Urls urls = payload->urls();
0154 
0155     // show the dialog straight away if fetch is interactive
0156     if( !m_dialog && unit->isInteractive() )
0157     {
0158         showCover( unit, QImage() );
0159     }
0160     else if( urls.isEmpty() )
0161     {
0162         finish( unit, NotFound );
0163         return;
0164     }
0165 
0166     const QList<QUrl> uniqueUrls = urls.uniqueKeys();
0167     foreach( const QUrl &url, uniqueUrls )
0168     {
0169         if( !url.isValid() )
0170             continue;
0171 
0172         QNetworkReply *reply = The::networkAccessManager()->getData( url, this, &CoverFetcher::slotResult );
0173         m_urls.insert( url, unit );
0174 
0175         if( payload->type() == CoverFetchPayload::Art )
0176         {
0177             if( unit->isInteractive() )
0178                 Amarok::Logger::newProgressOperation( reply, i18n( "Fetching Cover" ) );
0179             else
0180                 return; // only one is needed when the fetch is non-interactive
0181         }
0182     }
0183 }
0184 
0185 void
0186 CoverFetcher::slotResult( const QUrl &url, const QByteArray &data, const NetworkAccessManagerProxy::Error &e )
0187 {
0188     DEBUG_BLOCK
0189     if( !m_urls.contains( url ) )
0190         return;
0191 //     debug() << "Data dump from the result: " << data;
0192 
0193     const CoverFetchUnit::Ptr unit( m_urls.take( url ) );
0194     if( !unit )
0195     {
0196         QTimer::singleShot( 0, m_queue, [=] () { m_queue->remove( unit ); } );
0197         return;
0198     }
0199 
0200     if( e.code != QNetworkReply::NoError )
0201     {
0202         finish( unit, Error, i18n("There was an error communicating with cover provider: %1", e.description) );
0203         return;
0204     }
0205 
0206     const CoverFetchPayload *payload = unit->payload();
0207     switch( payload->type() )
0208     {
0209     case CoverFetchPayload::Info:
0210         QTimer::singleShot( 0, m_queue, [=] () { m_queue->add( unit->album(), unit->options(), payload->source(), data );
0211                                                  m_queue->remove( unit ); } );
0212         break;
0213 
0214     case CoverFetchPayload::Search:
0215         QTimer::singleShot( 0, m_queue, [=] () { m_queue->add( unit->options(), fetchSource(), data );
0216                                                  m_queue->remove( unit ); } );
0217         break;
0218 
0219     case CoverFetchPayload::Art:
0220         handleCoverPayload( unit, data, url );
0221         break;
0222     }
0223 }
0224 
0225 void
0226 CoverFetcher::handleCoverPayload( const CoverFetchUnit::Ptr &unit, const QByteArray &data, const QUrl &url )
0227 {
0228     if( data.isEmpty() )
0229     {
0230         finish( unit, NotFound );
0231         return;
0232     }
0233 
0234     QBuffer buffer;
0235     buffer.setData( data );
0236     buffer.open( QIODevice::ReadOnly );
0237     QImageReader reader( &buffer );
0238     if( !reader.canRead() )
0239     {
0240         finish( unit, Error, reader.errorString() );
0241         return;
0242     }
0243 
0244     QSize imageSize = reader.size();
0245     const CoverFetchArtPayload *payload = static_cast<const CoverFetchArtPayload*>( unit->payload() );
0246     const CoverFetch::Metadata &metadata = payload->urls().value( url );
0247 
0248     if( payload->imageSize() == CoverFetch::ThumbSize )
0249     {
0250         if( imageSize.isEmpty() )
0251         {
0252             imageSize.setWidth( metadata.value( QLatin1String("width") ).toInt() );
0253             imageSize.setHeight( metadata.value( QLatin1String("height") ).toInt() );
0254         }
0255         imageSize.scale( 120, 120, Qt::KeepAspectRatio );
0256         reader.setScaledSize( imageSize );
0257         // This will force the JPEG decoder to use JDCT_IFAST
0258         reader.setQuality( 49 );
0259     }
0260 
0261     if( unit->isInteractive() )
0262     {
0263         QImage image;
0264         if( reader.read( &image ) )
0265         {
0266             showCover( unit, image, metadata );
0267             QTimer::singleShot( 0, m_queue, [=] () {  m_queue->remove( unit ); } );
0268             return;
0269         }
0270     }
0271     else
0272     {
0273         QImage image;
0274         if( reader.read( &image ) )
0275         {
0276             m_selectedImages.insert( unit, image );
0277             finish( unit );
0278             return;
0279         }
0280     }
0281     finish( unit, Error, reader.errorString() );
0282 }
0283 
0284 void
0285 CoverFetcher::slotDialogFinished()
0286 {
0287     const CoverFetchUnit::Ptr unit = m_dialog->unit();
0288     switch( m_dialog->result() )
0289     {
0290     case QDialog::Accepted:
0291         m_selectedImages.insert( unit, m_dialog->image() );
0292         finish( unit );
0293         break;
0294 
0295     case QDialog::Rejected:
0296         finish( unit, Cancelled );
0297         break;
0298 
0299     default:
0300         finish( unit, Error );
0301     }
0302 
0303     /*
0304      * Remove all manual fetch jobs from the queue if the user accepts, cancels,
0305      * or closes the cover found dialog. This way, the dialog will not reappear
0306      * if there are still covers yet to be retrieved.
0307      */
0308     QList< CoverFetchUnit::Ptr > units = m_urls.values();
0309     foreach( const CoverFetchUnit::Ptr &unit, units )
0310     {
0311         if( unit->isInteractive() )
0312             abortFetch( unit );
0313     }
0314 
0315     m_dialog->hide();
0316     m_dialog->deleteLater();
0317 }
0318 
0319 void
0320 CoverFetcher::fetchRequestRedirected( QNetworkReply *oldReply,
0321                                       QNetworkReply *newReply )
0322 {
0323     QUrl oldUrl = oldReply->request().url();
0324     QUrl newUrl = newReply->request().url();
0325 
0326     // Since we were redirected we have to check if the redirect
0327     // was for one of our URLs and if the new URL is not handled
0328     // already.
0329     if( m_urls.contains( oldUrl ) && !m_urls.contains( newUrl ) )
0330     {
0331         // Get the unit for the old URL.
0332         CoverFetchUnit::Ptr unit = m_urls.value( oldUrl );
0333 
0334         // Add the unit with the new URL and remove the old one.
0335         m_urls.insert( newUrl, unit );
0336         m_urls.remove( oldUrl );
0337 
0338         // If the unit is an interactive one we have to incidate that we're
0339         // still fetching the cover.
0340         if( unit->isInteractive() )
0341             Amarok::Logger::newProgressOperation( newReply, i18n( "Fetching Cover" ) );
0342     }
0343 }
0344 
0345 void
0346 CoverFetcher::showCover( const CoverFetchUnit::Ptr &unit,
0347                          const QImage &cover,
0348                          const CoverFetch::Metadata &data )
0349 {
0350     if( !m_dialog )
0351     {
0352         const Meta::AlbumPtr album = unit->album();
0353         if( !album )
0354         {
0355             finish( unit, Error );
0356             return;
0357         }
0358 
0359         m_dialog = new CoverFoundDialog( unit, data );
0360         connect( m_dialog.data(), &CoverFoundDialog::newCustomQuery,
0361                  this, &CoverFetcher::queueQuery );
0362         connect( m_dialog.data(), &CoverFoundDialog::accepted,
0363                  this, &CoverFetcher::slotDialogFinished );
0364         connect( m_dialog.data(),&CoverFoundDialog::rejected,
0365                  this, &CoverFetcher::slotDialogFinished );
0366 
0367         if( fetchSource() == CoverFetch::LastFm )
0368             queueQueryForAlbum( album );
0369         m_dialog->setQueryPage( 1 );
0370 
0371         m_dialog->show();
0372         m_dialog->raise();
0373         m_dialog->activateWindow();
0374     }
0375     else
0376     {
0377         if( !cover.isNull() )
0378         {
0379             typedef CoverFetchArtPayload CFAP;
0380             const CFAP *payload = dynamic_cast< const CFAP* >( unit->payload() );
0381             if( payload )
0382                 m_dialog->add( cover, data, payload->imageSize() );
0383         }
0384     }
0385 }
0386 
0387 void
0388 CoverFetcher::abortFetch( const CoverFetchUnit::Ptr &unit )
0389 {
0390     QTimer::singleShot( 0, m_queue, [=] () {  m_queue->remove( unit ); } );
0391     m_selectedImages.remove( unit );
0392     QList<QUrl> urls = m_urls.keys( unit );
0393     foreach( const QUrl &url, urls )
0394         m_urls.remove( url );
0395     The::networkAccessManager()->abortGet( urls );
0396 }
0397 
0398 void
0399 CoverFetcher::finish( const CoverFetchUnit::Ptr &unit,
0400                       CoverFetcher::FinishState state,
0401                       const QString &message )
0402 {
0403     Meta::AlbumPtr album = unit->album();
0404     const QString albumName = album ? album->name() : QString();
0405 
0406     switch( state )
0407     {
0408     case Success:
0409     {
0410         if( !albumName.isEmpty() )
0411         {
0412             const QString text = i18n( "Retrieved cover successfully for '%1'.", albumName );
0413             Amarok::Logger::shortMessage( text );
0414             debug() << "Finished successfully for album" << albumName;
0415         }
0416         QImage image = m_selectedImages.take( unit );
0417         std::thread thread( std::bind( &Meta::Album::setImage, album, image ) );
0418         thread.detach();
0419         abortFetch( unit );
0420         break;
0421     }
0422     case Error:
0423         if( !albumName.isEmpty() )
0424         {
0425             const QString text = i18n( "Fetching cover for '%1' failed.", albumName );
0426             Amarok::Logger::shortMessage( text );
0427             QString debugMessage;
0428             if( !message.isEmpty() )
0429                 debugMessage = '[' + message + ']';
0430             debug() << "Finished with errors for album" << albumName << debugMessage;
0431         }
0432         m_errors += message;
0433         break;
0434 
0435     case Cancelled:
0436         if( !albumName.isEmpty() )
0437         {
0438             const QString text = i18n( "Canceled fetching cover for '%1'.", albumName );
0439             Amarok::Logger::shortMessage( text );
0440             debug() << "Finished, cancelled by user for album" << albumName;
0441         }
0442         break;
0443 
0444     case NotFound:
0445         if( !albumName.isEmpty() )
0446         {
0447             const QString text = i18n( "Unable to find a cover for '%1'.", albumName );
0448             //FIXME: Not visible behind cover manager
0449             Amarok::Logger::shortMessage( text );
0450             m_errors += text;
0451             debug() << "Finished due to cover not found for album" << albumName;
0452         }
0453         break;
0454     }
0455 
0456     QTimer::singleShot( 0, m_queue, [=] () { m_queue->remove( unit ); } );
0457 
0458     Q_EMIT finishedSingle( static_cast< int >( state ) );
0459 }
0460 
0461 CoverFetch::Source
0462 CoverFetcher::fetchSource() const
0463 {
0464     const KConfigGroup config = Amarok::config( "Cover Fetcher" );
0465     const QString sourceEntry = config.readEntry( "Interactive Image Source", "LastFm" );
0466     CoverFetch::Source source;
0467     if( sourceEntry == "LastFm" )
0468         source = CoverFetch::LastFm;
0469     else if( sourceEntry == "Google" )
0470         source = CoverFetch::Google;
0471     else
0472         source = CoverFetch::Discogs;
0473     return source;
0474 }
0475 
0476