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