Fetch missing album covers from last.fm :)

This commit is contained in:
David Sansome 2010-02-28 19:25:52 +00:00
parent eb3b286f0d
commit 605e3a87cc
8 changed files with 222 additions and 25 deletions

View File

@ -1,39 +1,80 @@
#include "albumcoverfetcher.h"
#include <QNetworkReply>
#include <QTimer>
#include <lastfm/Artist>
#include <lastfm/XmlQuery>
#include <lastfm/ws.h>
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<QNetworkReply*>(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);
}

View File

@ -5,6 +5,7 @@
#include <QMap>
#include <QNetworkAccessManager>
#include <QObject>
#include <QQueue>
#include <lastfm/Album>
@ -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<QNetworkReply*, lastfm::Album> requests_;
quint64 next_id_;
QQueue<QueuedRequest> queued_requests_;
QMap<QNetworkReply*, quint64> active_requests_;
QTimer* request_starter_;
};
#endif // ALBUMCOVERFETCHER_H

View File

@ -1,6 +1,8 @@
#include "albumcoverloader.h"
#include <QPainter>
#include <QDir>
#include <QCoreApplication>
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();

View File

@ -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);

View File

@ -1,4 +1,5 @@
#include "albumcovermanager.h"
#include "albumcoverfetcher.h"
#include "librarybackend.h"
#include "libraryquery.h"
@ -6,12 +7,16 @@
#include <QPainter>
#include <QMenu>
#include <QActionGroup>
#include <QListWidget>
#include <QCryptographicHash>
#include <QDir>
const char* AlbumCoverManager::kSettingsGroup = "CoverManager";
AlbumCoverManager::AlbumCoverManager(QWidget *parent)
: QDialog(parent),
cover_loader_(new BackgroundThread<AlbumCoverLoader>(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 ; i<ui_.albums->count() ; ++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);
}

View File

@ -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<LibraryBackend> backend_;
@ -51,6 +62,9 @@ class AlbumCoverManager : public QDialog {
BackgroundThread<AlbumCoverLoader>* cover_loader_;
QMap<quint64, QListWidgetItem*> cover_loading_tasks_;
AlbumCoverFetcher* cover_fetcher_;
QMap<quint64, QListWidgetItem*> cover_fetching_tasks_;
QIcon artist_icon_;
QIcon all_artists_icon_;
QIcon no_cover_icon_;

View File

@ -526,7 +526,7 @@ QList<LibraryBackend::AlbumArtInfo>
const QueryOptions& opt) {
QList<AlbumArtInfo> 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<LibraryBackend::AlbumArtInfo>
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());
}

View File

@ -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<AlbumArtInfo> 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) {}