Fetch missing album covers from last.fm :)
This commit is contained in:
parent
eb3b286f0d
commit
605e3a87cc
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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_;
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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) {}
|
||||
|
Loading…
x
Reference in New Issue
Block a user