diff --git a/src/albumcoverfetcher.cpp b/src/albumcoverfetcher.cpp index 276e21297..8fd120150 100644 --- a/src/albumcoverfetcher.cpp +++ b/src/albumcoverfetcher.cpp @@ -1,39 +1,80 @@ #include "albumcoverfetcher.h" #include +#include #include #include #include +const int AlbumCoverFetcher::kMaxConcurrentRequests = 5; + AlbumCoverFetcher::AlbumCoverFetcher(QObject* parent) - : QObject(parent) { + : QObject(parent), + next_id_(0), + request_starter_(new QTimer(this)) +{ + request_starter_->setInterval(1000); + connect(request_starter_, SIGNAL(timeout()), SLOT(StartRequests())); } -lastfm::Album AlbumCoverFetcher::FetchAlbumCover( +quint64 AlbumCoverFetcher::FetchAlbumCover( const QString& artist_name, const QString& album_name) { - lastfm::Artist artist(artist_name); - lastfm::Album album(artist, album_name); + QueuedRequest request; + request.album = album_name; + request.artist = artist_name; + request.id = next_id_ ++; - QNetworkReply* reply = album.getInfo(); - connect(reply, SIGNAL(finished()), SLOT(AlbumGetInfoFinished())); - requests_.insert(reply, album); - return album; + queued_requests_.enqueue(request); + + if (!request_starter_->isActive()) + request_starter_->start(); + + if (active_requests_.count() < kMaxConcurrentRequests) + StartRequests(); + + return request.id; +} + +void AlbumCoverFetcher::Clear() { + queued_requests_.clear(); +} + +void AlbumCoverFetcher::StartRequests() { + if (queued_requests_.isEmpty()) { + request_starter_->stop(); + return; + } + + while (!queued_requests_.isEmpty() && + active_requests_.count() < kMaxConcurrentRequests) { + QueuedRequest request = queued_requests_.dequeue(); + + lastfm::Artist artist(request.artist); + lastfm::Album album(artist, request.album); + + QNetworkReply* reply = album.getInfo(); + connect(reply, SIGNAL(finished()), SLOT(AlbumGetInfoFinished())); + active_requests_.insert(reply, request.id); + } } void AlbumCoverFetcher::AlbumGetInfoFinished() { QNetworkReply* reply = qobject_cast(sender()); reply->deleteLater(); + quint64 id = active_requests_.take(reply); - lastfm::XmlQuery query(lastfm::ws::parse(reply)); - qDebug() << query["album"]["image size=large"].text(); + try { + lastfm::XmlQuery query(lastfm::ws::parse(reply)); - QUrl image_url(query["album"]["image size=large"].text()); - QNetworkReply* image_reply = network_.get(QNetworkRequest(image_url)); - connect(image_reply, SIGNAL(finished()), SLOT(AlbumCoverFetchFinished())); + QUrl image_url(query["album"]["image size=large"].text()); + QNetworkReply* image_reply = network_.get(QNetworkRequest(image_url)); + connect(image_reply, SIGNAL(finished()), SLOT(AlbumCoverFetchFinished())); - lastfm::Album album = requests_.take(reply); - requests_[image_reply] = album; + active_requests_[image_reply] = id; + } catch (std::runtime_error&) { + emit AlbumCoverFetched(id, QImage()); + } } void AlbumCoverFetcher::AlbumCoverFetchFinished() { @@ -43,6 +84,6 @@ void AlbumCoverFetcher::AlbumCoverFetchFinished() { QImage image; image.loadFromData(reply->readAll()); - lastfm::Album album = requests_.take(reply); - emit AlbumCoverFetched(album, image); + quint64 id = active_requests_.take(reply); + emit AlbumCoverFetched(id, image); } diff --git a/src/albumcoverfetcher.h b/src/albumcoverfetcher.h index d5210a287..f27158890 100644 --- a/src/albumcoverfetcher.h +++ b/src/albumcoverfetcher.h @@ -5,6 +5,7 @@ #include #include #include +#include #include @@ -18,18 +19,34 @@ class AlbumCoverFetcher : public QObject { AlbumCoverFetcher(QObject* parent = 0); virtual ~AlbumCoverFetcher() {} - lastfm::Album FetchAlbumCover(const QString& artist, const QString& album); + static const int kMaxConcurrentRequests; + + quint64 FetchAlbumCover(const QString& artist, const QString& album); + + void Clear(); signals: - void AlbumCoverFetched(const lastfm::Album&, const QImage& cover); + void AlbumCoverFetched(quint64, const QImage& cover); private slots: void AlbumGetInfoFinished(); void AlbumCoverFetchFinished(); + void StartRequests(); private: + struct QueuedRequest { + quint64 id; + QString artist; + QString album; + }; + QNetworkAccessManager network_; - QMap requests_; + quint64 next_id_; + + QQueue queued_requests_; + QMap active_requests_; + + QTimer* request_starter_; }; #endif // ALBUMCOVERFETCHER_H diff --git a/src/albumcoverloader.cpp b/src/albumcoverloader.cpp index 9af885427..688624af3 100644 --- a/src/albumcoverloader.cpp +++ b/src/albumcoverloader.cpp @@ -1,6 +1,8 @@ #include "albumcoverloader.h" #include +#include +#include AlbumCoverLoader::AlbumCoverLoader(QObject* parent) : QObject(parent), @@ -9,6 +11,11 @@ AlbumCoverLoader::AlbumCoverLoader(QObject* parent) { } +QString AlbumCoverLoader::ImageCacheDir() { + return QString("%1/.config/%2/albumcovers/") + .arg(QDir::homePath(), QCoreApplication::organizationName()); +} + void AlbumCoverLoader::Clear() { QMutexLocker l(&mutex_); tasks_.clear(); diff --git a/src/albumcoverloader.h b/src/albumcoverloader.h index 9e1958d35..b28ffdbdd 100644 --- a/src/albumcoverloader.h +++ b/src/albumcoverloader.h @@ -14,6 +14,8 @@ class AlbumCoverLoader : public QObject { public: AlbumCoverLoader(QObject* parent = 0); + static QString ImageCacheDir(); + void SetDesiredHeight(int height) { height_ = height; } quint64 LoadImageAsync(const QString& art_automatic, const QString& art_manual); diff --git a/src/albumcovermanager.cpp b/src/albumcovermanager.cpp index e697df934..cb6b8337f 100644 --- a/src/albumcovermanager.cpp +++ b/src/albumcovermanager.cpp @@ -1,4 +1,5 @@ #include "albumcovermanager.h" +#include "albumcoverfetcher.h" #include "librarybackend.h" #include "libraryquery.h" @@ -6,12 +7,16 @@ #include #include #include +#include +#include +#include const char* AlbumCoverManager::kSettingsGroup = "CoverManager"; AlbumCoverManager::AlbumCoverManager(QWidget *parent) : QDialog(parent), cover_loader_(new BackgroundThread(this)), + cover_fetcher_(new AlbumCoverFetcher(this)), artist_icon_(":/artist.png"), all_artists_icon_(":/album.png") { @@ -50,6 +55,9 @@ AlbumCoverManager::AlbumCoverManager(QWidget *parent) connect(ui_.filter, SIGNAL(textChanged(QString)), SLOT(UpdateFilter())); connect(filter_group, SIGNAL(triggered(QAction*)), SLOT(UpdateFilter())); connect(ui_.view, SIGNAL(clicked()), ui_.view, SLOT(showMenu())); + connect(ui_.fetch, SIGNAL(clicked()), SLOT(FetchAlbumCovers())); + connect(cover_fetcher_, SIGNAL(AlbumCoverFetched(quint64,QImage)), + SLOT(AlbumCoverFetched(quint64,QImage))); // Restore settings QSettings s; @@ -64,6 +72,10 @@ AlbumCoverManager::AlbumCoverManager(QWidget *parent) cover_loader_->start(); } +AlbumCoverManager::~AlbumCoverManager() { + CancelRequests(); +} + void AlbumCoverManager::CoverLoaderInitialised() { connect(cover_loader_->Worker().get(), SIGNAL(ImageLoaded(quint64,QImage)), SLOT(CoverImageLoaded(quint64,QImage))); @@ -81,11 +93,24 @@ void AlbumCoverManager::showEvent(QShowEvent *) { } void AlbumCoverManager::closeEvent(QCloseEvent *) { + // Save geometry QSettings s; s.beginGroup(kSettingsGroup); s.setValue("geometry", saveGeometry()); s.setValue("splitter_state", ui_.splitter->saveState()); + + // Cancel any outstanding requests + CancelRequests(); +} + +void AlbumCoverManager::CancelRequests() { + cover_loading_tasks_.clear(); + cover_loader_->Worker()->Clear(); + + cover_fetching_tasks_.clear(); + cover_fetcher_->Clear(); + ui_.fetch->setEnabled(true); } void AlbumCoverManager::Reset() { @@ -114,11 +139,12 @@ void AlbumCoverManager::ArtistChanged(QListWidgetItem* current) { artist = current->text(); ui_.albums->clear(); - cover_loading_tasks_.clear(); - cover_loader_->Worker()->Clear(); + CancelRequests(); foreach (const LibraryBackend::AlbumArtInfo& info, backend_->GetAlbumArtInfo(artist)) { QListWidgetItem* item = new QListWidgetItem(no_cover_icon_, info.album_name, ui_.albums); + item->setData(Role_ArtistName, info.artist); + item->setData(Role_AlbumName, info.album_name); if (!info.art_automatic.isEmpty() || !info.art_manual.isEmpty()) { quint64 id = cover_loader_->Worker()->LoadImageAsync( @@ -158,3 +184,56 @@ void AlbumCoverManager::UpdateFilter() { (!has_cover && hide_without_covers)); } } + +void AlbumCoverManager::FetchAlbumCovers() { + for (int i=0 ; icount() ; ++i) { + QListWidgetItem* item = ui_.albums->item(i); + if (item->isHidden()) + continue; + if (item->icon().cacheKey() != no_cover_icon_.cacheKey()) + continue; + + quint64 id = cover_fetcher_->FetchAlbumCover( + item->data(Role_ArtistName).toString(), item->data(Role_AlbumName).toString()); + cover_fetching_tasks_[id] = item; + } + + if (!cover_fetching_tasks_.isEmpty()) + ui_.fetch->setEnabled(false); +} + +void AlbumCoverManager::AlbumCoverFetched(quint64 id, const QImage &image) { + if (!cover_fetching_tasks_.contains(id)) + return; + + QListWidgetItem* item = cover_fetching_tasks_.take(id); + if (!image.isNull()) { + const QString artist = item->data(Role_ArtistName).toString(); + const QString album = item->data(Role_AlbumName).toString(); + + // Hash the artist and album into a filename for the image + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(artist.toLower().toUtf8().constData()); + hash.addData(album.toLower().toUtf8().constData()); + + QString filename = hash.result().toHex() + ".jpg"; + QString path = AlbumCoverLoader::ImageCacheDir() + "/" + filename; + + // Make sure this directory exists first + QDir dir; + dir.mkdir(AlbumCoverLoader::ImageCacheDir()); + + // Save the image to disk + image.save(path, "JPG"); + + // Save the image in the database + backend_->UpdateManualAlbumArtAsync(artist, album, path); + + // Update the icon in our list + quint64 id = cover_loader_->Worker()->LoadImageAsync(QString(), path); + cover_loading_tasks_[id] = item; + } + + if (cover_fetching_tasks_.isEmpty()) + ui_.fetch->setEnabled(true); +} diff --git a/src/albumcovermanager.h b/src/albumcovermanager.h index 045c24f90..731fe8b0f 100644 --- a/src/albumcovermanager.h +++ b/src/albumcovermanager.h @@ -11,11 +11,13 @@ #include "albumcoverloader.h" class LibraryBackend; +class AlbumCoverFetcher; class AlbumCoverManager : public QDialog { Q_OBJECT public: AlbumCoverManager(QWidget *parent = 0); + ~AlbumCoverManager(); static const char* kSettingsGroup; @@ -33,6 +35,8 @@ class AlbumCoverManager : public QDialog { void CoverLoaderInitialised(); void CoverImageLoaded(quint64 id, const QImage& image); void UpdateFilter(); + void FetchAlbumCovers(); + void AlbumCoverFetched(quint64 id, const QImage& image); private: enum ArtistItemType { @@ -40,6 +44,13 @@ class AlbumCoverManager : public QDialog { Specific_Artist, }; + enum Role { + Role_ArtistName = Qt::UserRole + 1, + Role_AlbumName, + }; + + void CancelRequests(); + private: Ui::CoverManager ui_; boost::shared_ptr backend_; @@ -51,6 +62,9 @@ class AlbumCoverManager : public QDialog { BackgroundThread* cover_loader_; QMap cover_loading_tasks_; + AlbumCoverFetcher* cover_fetcher_; + QMap cover_fetching_tasks_; + QIcon artist_icon_; QIcon all_artists_icon_; QIcon no_cover_icon_; diff --git a/src/librarybackend.cpp b/src/librarybackend.cpp index c15b48f95..4cb4c0be9 100644 --- a/src/librarybackend.cpp +++ b/src/librarybackend.cpp @@ -526,7 +526,7 @@ QList const QueryOptions& opt) { QList ret; LibraryQuery query(opt); - query.SetColumnSpec("album, art_automatic, art_manual"); + query.SetColumnSpec("album, artist, compilation, sampler, art_automatic, art_manual"); query.SetOrderBy("album"); if (!artist.isNull()) @@ -541,13 +541,45 @@ QList if (q.value(0).toString() == last_album) continue; + bool compilation = q.value(2).toBool() | q.value(3).toBool(); + AlbumArtInfo info; + info.artist = compilation ? QString() : q.value(1).toString(); info.album_name = q.value(0).toString(); - info.art_automatic = q.value(1).toString(); - info.art_manual = q.value(2).toString(); + info.art_automatic = q.value(4).toString(); + info.art_manual = q.value(5).toString(); ret << info; last_album = info.album_name; } return ret; } + +void LibraryBackend::UpdateManualAlbumArtAsync(const QString &artist, + const QString &album, + const QString &art) { + metaObject()->invokeMethod(this, "UpdateManualAlbumArt", Qt::QueuedConnection, + Q_ARG(QString, artist), + Q_ARG(QString, album), + Q_ARG(QString, art)); +} + +void LibraryBackend::UpdateManualAlbumArt(const QString &artist, + const QString &album, + const QString &art) { + QSqlDatabase db(Connect()); + + QString sql("UPDATE songs SET art_manual = :art" + " WHERE album = :album"); + if (!artist.isNull()) + sql += " AND artist = :artist"; + + QSqlQuery q(sql, db); + q.bindValue(":art", art); + q.bindValue(":album", album); + if (!artist.isNull()) + q.bindValue(":artist", artist); + + q.exec(); + CheckErrors(q.lastError()); +} diff --git a/src/librarybackend.h b/src/librarybackend.h index 919b7c5c0..a02bfbf88 100644 --- a/src/librarybackend.h +++ b/src/librarybackend.h @@ -18,6 +18,7 @@ class LibraryBackend : public QObject { LibraryBackend(QObject* parent = 0); struct AlbumArtInfo { + QString artist; QString album_name; QString art_automatic; QString art_manual; @@ -44,6 +45,7 @@ class LibraryBackend : public QObject { SongList GetCompilationSongs(const QString& album, const QueryOptions& opt = QueryOptions()); QList GetAlbumArtInfo(const QString& artist = QString(), const QueryOptions& opt = QueryOptions()); + void UpdateManualAlbumArtAsync(const QString& artist, const QString& album, const QString& art); Song GetSongById(int id); @@ -71,6 +73,9 @@ class LibraryBackend : public QObject { void TotalSongCountUpdated(int total); + private slots: + void UpdateManualAlbumArt(const QString& artist, const QString& album, const QString& art); + private: struct CompilationInfo { CompilationInfo() : has_samplers(false), has_not_samplers(false) {}