Add support for saving embedded album covers

Fixes #286
This commit is contained in:
Jonas Kvinge 2021-02-26 21:03:51 +01:00
parent e4c89c1aed
commit 133f094d72
79 changed files with 3509 additions and 1804 deletions

View File

@ -34,6 +34,7 @@ set(SOURCES
core/thread.cpp
core/urlhandler.cpp
core/utilities.cpp
core/imageutils.cpp
core/iconloader.cpp
core/qtsystemtrayicon.cpp
core/standarditemiconloader.cpp

View File

@ -52,8 +52,6 @@
#include "collectionquery.h"
#include "sqlrow.h"
const char *CollectionBackend::kSettingsGroup = "Collection";
CollectionBackend::CollectionBackend(QObject *parent) :
CollectionBackendInterface(parent),
db_(nullptr),
@ -671,19 +669,32 @@ CollectionBackend::AlbumList CollectionBackend::GetAlbumsByArtist(const QString
return GetAlbums(artist, false, opt);
}
SongList CollectionBackend::GetSongsByAlbum(const QString &album, const QueryOptions &opt) {
SongList CollectionBackend::GetArtistSongs(const QString &effective_albumartist, const QueryOptions &opt) {
CollectionQuery query(opt);
query.AddCompilationRequirement(false);
query.AddWhere("album", album);
query.AddWhere("effective_albumartist", effective_albumartist);
return ExecCollectionQuery(&query);
}
SongList CollectionBackend::GetSongs(const QString &artist, const QString &album, const QueryOptions &opt) {
SongList CollectionBackend::GetAlbumSongs(const QString &effective_albumartist, const QString &album, const QueryOptions &opt) {
CollectionQuery query(opt);
query.AddCompilationRequirement(false);
query.AddWhere("artist", artist);
query.AddWhere("effective_albumartist", effective_albumartist);
query.AddWhere("album", album);
return ExecCollectionQuery(&query);
}
SongList CollectionBackend::GetSongsByAlbum(const QString &album, const QueryOptions &opt) {
CollectionQuery query(opt);
query.AddCompilationRequirement(false);
query.AddWhere("album", album);
return ExecCollectionQuery(&query);
}
SongList CollectionBackend::ExecCollectionQuery(CollectionQuery *query) {
@ -1006,15 +1017,15 @@ void CollectionBackend::UpdateCompilations(QSqlQuery &find_song, QSqlQuery &upda
CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist, const bool compilation_required, const QueryOptions &opt) {
CollectionQuery query(opt);
query.SetColumnSpec("url, artist, albumartist, album, compilation_effective, art_automatic, art_manual");
query.SetOrderBy("album");
query.SetColumnSpec("url, effective_albumartist, album, compilation_effective, art_automatic, art_manual, filetype, cue_path");
query.SetOrderBy("effective_albumartist, album, url");
if (compilation_required) {
query.AddCompilationRequirement(true);
}
else if (!artist.isEmpty()) {
query.AddCompilationRequirement(false);
query.AddWhereArtist(artist);
query.AddWhere("effective_albumartist", artist);
}
{
@ -1024,17 +1035,16 @@ CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist,
QMap<QString, Album> albums;
while (query.Next()) {
bool is_compilation = query.Value(4).toBool();
bool is_compilation = query.Value(3).toBool();
Album info;
info.first_url = QUrl::fromEncoded(query.Value(0).toByteArray());
QUrl url = QUrl::fromEncoded(query.Value(0).toByteArray());
if (!is_compilation) {
info.artist = query.Value(1).toString();
info.album_artist = query.Value(2).toString();
info.album_artist = query.Value(1).toString();
}
info.album_name = query.Value(3).toString();
info.album = query.Value(2).toString();
QString art_automatic = query.Value(5).toString();
QString art_automatic = query.Value(4).toString();
if (art_automatic.contains(QRegularExpression("..+:.*"))) {
info.art_automatic = QUrl::fromEncoded(art_automatic.toUtf8());
}
@ -1042,7 +1052,7 @@ CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist,
info.art_automatic = QUrl::fromLocalFile(art_automatic);
}
QString art_manual = query.Value(6).toString();
QString art_manual = query.Value(5).toString();
if (art_manual.contains(QRegularExpression("..+:.*"))) {
info.art_manual = QUrl::fromEncoded(art_manual.toUtf8());
}
@ -1050,19 +1060,31 @@ CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist,
info.art_manual = QUrl::fromLocalFile(art_manual);
}
QString effective_albumartist = info.album_artist.isEmpty() ? info.artist : info.album_artist;
info.filetype = Song::FileType(query.Value(6).toInt());
QString filetype = Song::TextForFiletype(info.filetype);
info.cue_path = query.Value(7).toString();
QString key;
if (!effective_albumartist.isEmpty()) {
key.append(effective_albumartist);
if (!info.album_artist.isEmpty()) {
key.append(info.album_artist);
}
if (!info.album_name.isEmpty()) {
if (!info.album.isEmpty()) {
if (!key.isEmpty()) key.append("-");
key.append(info.album_name);
key.append(info.album);
}
if (!filetype.isEmpty()) {
key.append(filetype);
}
if (key.isEmpty()) continue;
if (!albums.contains(key)) albums.insert(key, info);
if (albums.contains(key)) {
albums[key].urls.append(url);
}
else {
info.urls << url;
albums.insert(key, info);
}
}
@ -1070,20 +1092,16 @@ CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist,
}
CollectionBackend::Album CollectionBackend::GetAlbumArt(const QString &artist, const QString &albumartist, const QString &album) {
CollectionBackend::Album CollectionBackend::GetAlbumArt(const QString &effective_albumartist, const QString &album) {
Album ret;
ret.album_name = album;
ret.artist = artist;
ret.album_artist = albumartist;
ret.album = album;
ret.album_artist = effective_albumartist;
CollectionQuery query = CollectionQuery(QueryOptions());
query.SetColumnSpec("art_automatic, art_manual, url");
if (!albumartist.isEmpty()) {
query.AddWhere("albumartist", albumartist);
}
else if (!artist.isEmpty()) {
query.AddWhere("artist", artist);
if (!effective_albumartist.isEmpty()) {
query.AddWhere("effective_albumartist", effective_albumartist);
}
query.AddWhere("album", album);
@ -1093,20 +1111,20 @@ CollectionBackend::Album CollectionBackend::GetAlbumArt(const QString &artist, c
if (query.Next()) {
ret.art_automatic = QUrl::fromEncoded(query.Value(0).toByteArray());
ret.art_manual = QUrl::fromEncoded(query.Value(1).toByteArray());
ret.first_url = QUrl::fromEncoded(query.Value(2).toByteArray());
ret.urls << QUrl::fromEncoded(query.Value(2).toByteArray());
}
return ret;
}
void CollectionBackend::UpdateManualAlbumArtAsync(const QString &artist, const QString &albumartist, const QString &album, const QUrl &cover_url) {
void CollectionBackend::UpdateManualAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic) {
metaObject()->invokeMethod(this, "UpdateManualAlbumArt", Qt::QueuedConnection, Q_ARG(QString, artist), Q_ARG(QString, albumartist), Q_ARG(QString, album), Q_ARG(QUrl, cover_url));
metaObject()->invokeMethod(this, "UpdateManualAlbumArt", Qt::QueuedConnection, Q_ARG(QString, effective_albumartist), Q_ARG(QString, album), Q_ARG(QUrl, cover_url), Q_ARG(bool, clear_art_automatic));
}
void CollectionBackend::UpdateManualAlbumArt(const QString &artist, const QString &albumartist, const QString &album, const QUrl &cover_url) {
void CollectionBackend::UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
@ -1114,15 +1132,9 @@ void CollectionBackend::UpdateManualAlbumArt(const QString &artist, const QStrin
// Get the songs before they're updated
CollectionQuery query;
query.SetColumnSpec("ROWID, " + Song::kColumnSpec);
query.AddWhere("effective_albumartist", effective_albumartist);
query.AddWhere("album", album);
if (!albumartist.isEmpty()) {
query.AddWhere("albumartist", albumartist);
}
else if (!artist.isEmpty()) {
query.AddWhere("artist", artist);
}
if (!ExecQuery(&query)) return;
SongList deleted_songs;
@ -1133,26 +1145,73 @@ void CollectionBackend::UpdateManualAlbumArt(const QString &artist, const QStrin
}
// Update the songs
QString sql(QString("UPDATE %1 SET art_manual = :cover WHERE album = :album AND unavailable = 0").arg(songs_table_));
if (!albumartist.isEmpty()) {
sql += " AND albumartist = :albumartist";
}
else if (!artist.isNull()) {
sql += " AND artist = :artist";
QString sql(QString("UPDATE %1 SET art_manual = :cover ").arg(songs_table_));
if (clear_art_automatic) {
sql += "AND art_automatic = '' ";
}
sql += "WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0";
QSqlQuery q(db);
q.prepare(sql);
q.bindValue(":cover", cover_url.toString(QUrl::FullyEncoded));
q.bindValue(":cover", cover_url.isValid() ? cover_url.toString(QUrl::FullyEncoded) : "");
q.bindValue(":effective_albumartist", effective_albumartist);
q.bindValue(":album", album);
if (!albumartist.isEmpty()) {
q.bindValue(":albumartist", albumartist);
q.exec();
db_->CheckErrors(q);
// Now get the updated songs
if (!ExecQuery(&query)) return;
SongList added_songs;
while (query.Next()) {
Song song(source_);
song.InitFromQuery(query, true);
added_songs << song;
}
else if (!artist.isEmpty()) {
q.bindValue(":artist", artist);
if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) {
emit SongsDeleted(deleted_songs);
emit SongsDiscovered(added_songs);
}
}
void CollectionBackend::UpdateAutomaticAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url) {
metaObject()->invokeMethod(this, "UpdateAutomaticAlbumArt", Qt::QueuedConnection, Q_ARG(QString, effective_albumartist), Q_ARG(QString, album), Q_ARG(QUrl, cover_url));
}
void CollectionBackend::UpdateAutomaticAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &cover_url) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
// Get the songs before they're updated
CollectionQuery query;
query.SetColumnSpec("ROWID, " + Song::kColumnSpec);
query.AddWhere("effective_albumartist", effective_albumartist);
query.AddWhere("album", album);
if (!ExecQuery(&query)) return;
SongList deleted_songs;
while (query.Next()) {
Song song(source_);
song.InitFromQuery(query, true);
deleted_songs << song;
}
// Update the songs
QString sql(QString("UPDATE %1 SET art_automatic = :cover WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_));
QSqlQuery q(db);
q.prepare(sql);
q.bindValue(":cover", cover_url.isValid() ? cover_url.toString(QUrl::FullyEncoded) : "");
q.bindValue(":effective_albumartist", effective_albumartist);
q.bindValue(":album", album);
q.exec();
db_->CheckErrors(q);

View File

@ -50,25 +50,23 @@ class CollectionBackendInterface : public QObject {
struct Album {
Album() {}
Album(const QString &_artist, const QString &_album_artist, const QString &_album_name, const QUrl &_art_automatic, const QUrl &_art_manual, const QUrl &_first_url) :
artist(_artist),
Album(const QString &_album_artist, const QString &_album, const QUrl &_art_automatic, const QUrl &_art_manual, const QList<QUrl> &_urls, const Song::FileType _filetype, const QString &_cue_path) :
album_artist(_album_artist),
album_name(_album_name),
album(_album),
art_automatic(_art_automatic),
art_manual(_art_manual),
first_url(_first_url) {}
urls(_urls),
filetype(_filetype),
cue_path(_cue_path) {}
const QString &effective_albumartist() const {
return album_artist.isEmpty() ? artist : album_artist;
}
QString artist;
QString album_artist;
QString album_name;
QString album;
QUrl art_automatic;
QUrl art_manual;
QUrl first_url;
QList<QUrl> urls;
Song::FileType filetype;
QString cue_path;
};
typedef QList<Album> AlbumList;
@ -88,8 +86,9 @@ class CollectionBackendInterface : public QObject {
virtual QStringList GetAllArtists(const QueryOptions &opt = QueryOptions()) = 0;
virtual QStringList GetAllArtistsWithAlbums(const QueryOptions &opt = QueryOptions()) = 0;
virtual SongList GetArtistSongs(const QString &effective_albumartist, const QueryOptions &opt = QueryOptions()) = 0;
virtual SongList GetAlbumSongs(const QString &effective_albumartist, const QString &album, const QueryOptions &opt = QueryOptions()) = 0;
virtual SongList GetSongsByAlbum(const QString &album, const QueryOptions &opt = QueryOptions()) = 0;
virtual SongList GetSongs(const QString &artist, const QString &album, const QueryOptions &opt = QueryOptions()) = 0;
virtual SongList GetCompilationSongs(const QString &album, const QueryOptions &opt = QueryOptions()) = 0;
@ -97,8 +96,10 @@ class CollectionBackendInterface : public QObject {
virtual AlbumList GetAlbumsByArtist(const QString &artist, const QueryOptions &opt = QueryOptions()) = 0;
virtual AlbumList GetCompilationAlbums(const QueryOptions &opt = QueryOptions()) = 0;
virtual void UpdateManualAlbumArtAsync(const QString &artist, const QString &albumartist, const QString &album, const QUrl &cover_url) = 0;
virtual Album GetAlbumArt(const QString &artist, const QString &albumartist, const QString &album) = 0;
virtual void UpdateManualAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic = false) = 0;
virtual void UpdateAutomaticAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url) = 0;
virtual Album GetAlbumArt(const QString &effective_albumartist, const QString &album) = 0;
virtual Song GetSongById(const int id) = 0;
@ -118,7 +119,6 @@ class CollectionBackend : public CollectionBackendInterface {
Q_OBJECT
public:
static const char *kSettingsGroup;
Q_INVOKABLE explicit CollectionBackend(QObject *parent = nullptr);
@ -148,8 +148,9 @@ class CollectionBackend : public CollectionBackendInterface {
QStringList GetAll(const QString &column, const QueryOptions &opt = QueryOptions());
QStringList GetAllArtists(const QueryOptions &opt = QueryOptions()) override;
QStringList GetAllArtistsWithAlbums(const QueryOptions &opt = QueryOptions()) override;
SongList GetArtistSongs(const QString &effective_albumartist, const QueryOptions &opt = QueryOptions()) override;
SongList GetAlbumSongs(const QString &effective_albumartist, const QString &album, const QueryOptions &opt = QueryOptions()) override;
SongList GetSongsByAlbum(const QString &album, const QueryOptions &opt = QueryOptions()) override;
SongList GetSongs(const QString &artist, const QString &album, const QueryOptions &opt = QueryOptions()) override;
SongList GetCompilationSongs(const QString &album, const QueryOptions &opt = QueryOptions()) override;
@ -157,8 +158,10 @@ class CollectionBackend : public CollectionBackendInterface {
AlbumList GetCompilationAlbums(const QueryOptions &opt = QueryOptions()) override;
AlbumList GetAlbumsByArtist(const QString &artist, const QueryOptions &opt = QueryOptions()) override;
void UpdateManualAlbumArtAsync(const QString &artist, const QString &albumartist, const QString &album, const QUrl &cover_url) override;
Album GetAlbumArt(const QString &artist, const QString &albumartist, const QString &album) override;
void UpdateManualAlbumArtAsync(const QString &album_artist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic = false) override;
void UpdateAutomaticAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url) override;
Album GetAlbumArt(const QString &album_artist, const QString &album) override;
Song GetSongById(const int id) override;
SongList GetSongsById(const QList<int> &ids);
@ -205,7 +208,8 @@ class CollectionBackend : public CollectionBackendInterface {
void MarkSongsUnavailable(const SongList &songs, const bool unavailable = true);
void AddOrUpdateSubdirs(const SubdirectoryList &subdirs);
void CompilationsNeedUpdating();
void UpdateManualAlbumArt(const QString &artist, const QString &albumartist, const QString &album, const QUrl &cover_url);
void UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic = false);
void UpdateAutomaticAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &cover_url);
void ForceCompilation(const QString &album, const QList<QString> &artists, const bool on);
void IncrementPlayCount(const int id);
void IncrementSkipCount(const int id, const float progress);

View File

@ -102,9 +102,10 @@ CollectionModel::CollectionModel(CollectionBackend *backend, Application *app, Q
group_by_[1] = GroupBy_AlbumDisc;
group_by_[2] = GroupBy_None;
cover_loader_options_.desired_height_ = kPrettyCoverSize;
cover_loader_options_.pad_output_image_ = true;
cover_loader_options_.get_image_data_ = false;
cover_loader_options_.scale_output_image_ = true;
cover_loader_options_.pad_output_image_ = true;
cover_loader_options_.desired_height_ = kPrettyCoverSize;
if (app_) {
QObject::connect(app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &CollectionModel::AlbumCoverLoaded);
@ -677,7 +678,7 @@ void CollectionModel::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderR
pending_cache_keys_.remove(cache_key);
// Insert this image in the cache.
if (result.image_scaled.isNull()) {
if (!result.success || result.image_scaled.isNull() || result.type == AlbumCoverLoaderResult::Type_ManuallyUnset) {
// Set the no_cover image so we don't continually try to load art.
QPixmapCache::insert(cache_key, no_cover_icon_);
}
@ -688,9 +689,9 @@ void CollectionModel::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderR
}
// If we have a valid cover not already in the disk cache
if (use_disk_cache_ && sIconCache) {
if (use_disk_cache_ && sIconCache && result.success && !result.image_scaled.isNull()) {
std::unique_ptr<QIODevice> cached_img(sIconCache->data(QUrl(cache_key)));
if (!cached_img && !result.image_scaled.isNull()) {
if (!cached_img) {
QNetworkCacheMetaData item_metadata;
item_metadata.setSaveToDisk(true);
item_metadata.setUrl(QUrl(cache_key));

View File

@ -471,11 +471,7 @@ void CollectionView::SetShowInVarious(const bool on) {
}
}
if (other_artists.count() > 0) {
if (QMessageBox::question(this,
tr("There are other songs in this album"),
tr("Would you like to move the other songs on this album to Various Artists as well?"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::Yes) == QMessageBox::Yes) {
if (QMessageBox::question(this, tr("There are other songs in this album"), tr("Would you like to move the other songs on this album to Various Artists as well?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes) {
for (const QString &s : other_artists) {
albums.insert(album, s);
}

View File

@ -534,7 +534,7 @@ void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file, const So
// If a cue got deleted, we turn it's first section into the new 'raw' (cueless) song and we just remove the rest of the sections from the collection
if (cue_deleted) {
for (const Song &song : backend_->GetSongsByUrl(QUrl::fromLocalFile(file))) {
if (!song.IsMetadataEqual(matching_song)) {
if (!song.IsMetadataAndArtEqual(matching_song)) {
t->deleted_songs << song;
}
}
@ -611,7 +611,7 @@ void CollectionWatcher::PreserveUserSetData(const QString &file, const QUrl &ima
t->new_songs << *out;
}
else if (!matching_song.IsMetadataEqual(*out)) {
else if (!matching_song.IsMetadataAndArtEqual(*out)) {
qLog(Debug) << file << "metadata changed";
// Update the song in the DB

View File

@ -37,8 +37,8 @@
#include <QContextMenuEvent>
#include <QPaintEvent>
#include "core/imageutils.h"
#include "covermanager/albumcoverchoicecontroller.h"
#include "covermanager/albumcoverloader.h"
#include "contextview.h"
#include "contextalbum.h"
@ -62,8 +62,8 @@ ContextAlbum::ContextAlbum(QWidget *parent) :
cover_loader_options_.desired_height_ = 600;
cover_loader_options_.pad_output_image_ = true;
cover_loader_options_.scale_output_image_ = true;
QPair<QImage, QImage> images = AlbumCoverLoader::ScaleAndPad(cover_loader_options_, image_strawberry_);
pixmap_current_ = QPixmap::fromImage(images.first);
QImage image = ImageUtils::ScaleAndPad(image_strawberry_, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_);
if (!image.isNull()) pixmap_current_ = QPixmap::fromImage(image);
QObject::connect(timeline_fade_, &QTimeLine::valueChanged, this, &ContextAlbum::FadePreviousTrack);
timeline_fade_->setDirection(QTimeLine::Backward); // 1.0 -> 0.0
@ -78,9 +78,10 @@ void ContextAlbum::Init(ContextView *context_view, AlbumCoverChoiceController *a
QObject::connect(album_cover_choice_controller_, &AlbumCoverChoiceController::AutomaticCoverSearchDone, this, &ContextAlbum::AutomaticCoverSearchDone);
QList<QAction*> cover_actions = album_cover_choice_controller_->GetAllActions();
cover_actions.append(album_cover_choice_controller_->search_cover_auto_action());
menu_->addActions(cover_actions);
menu_->addSeparator();
menu_->addAction(album_cover_choice_controller_->search_cover_auto_action());
menu_->addSeparator();
}
@ -115,7 +116,9 @@ void ContextAlbum::DrawImage(QPainter *p) {
if (width() != prev_width_) {
cover_loader_options_.desired_height_ = width() - kWidgetSpacing;
pixmap_current_ = QPixmap::fromImage(AlbumCoverLoader::ScaleAndPad(cover_loader_options_, image_original_).first);
QImage image = ImageUtils::ScaleAndPad(image_original_, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_);
if (image.isNull()) pixmap_current_ = QPixmap();
else pixmap_current_ = QPixmap::fromImage(image);
prev_width_ = width();
}
@ -144,7 +147,9 @@ void ContextAlbum::FadePreviousTrack(const qreal value) {
void ContextAlbum::ScaleCover() {
cover_loader_options_.desired_height_ = width() - kWidgetSpacing;
pixmap_current_ = QPixmap::fromImage(AlbumCoverLoader::ScaleAndPad(cover_loader_options_, image_original_).first);
QImage image = ImageUtils::ScaleAndPad(image_original_, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_);
if (image.isNull()) pixmap_current_ = QPixmap();
else pixmap_current_ = QPixmap::fromImage(image);
prev_width_ = width();
update();

View File

@ -67,9 +67,10 @@ ContextAlbumsModel::ContextAlbumsModel(CollectionBackend *backend, Application *
root_->lazy_loaded = true;
cover_loader_options_.desired_height_ = kPrettyCoverSize;
cover_loader_options_.pad_output_image_ = true;
cover_loader_options_.get_image_data_ = false;
cover_loader_options_.scale_output_image_ = true;
cover_loader_options_.pad_output_image_ = true;
cover_loader_options_.desired_height_ = kPrettyCoverSize;
QObject::connect(app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &ContextAlbumsModel::AlbumCoverLoaded);
@ -159,7 +160,7 @@ void ContextAlbumsModel::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoad
pending_cache_keys_.remove(cache_key);
// Insert this image in the cache.
if (result.image_scaled.isNull()) {
if (!result.success || result.image_scaled.isNull() || result.type == AlbumCoverLoaderResult::Type_ManuallyUnset) {
// Set the no_cover image so we don't continually try to load art.
QPixmapCache::insert(cache_key, no_cover_icon_);
}

View File

@ -603,13 +603,13 @@ void ContextView::SetSong() {
const QueryOptions opt;
CollectionBackend::AlbumList albumlist;
widget_albums_->albums_model()->Reset();
albumlist = app_->collection_backend()->GetAlbumsByArtist(song_playing_.artist(), opt);
albumlist = app_->collection_backend()->GetAlbumsByArtist(song_playing_.effective_albumartist(), opt);
if (albumlist.count() > 1) {
label_play_albums_->show();
widget_albums_->show();
label_play_albums_->setText("<b>" + tr("Albums by %1").arg( song_playing_.artist().toHtmlEscaped()) + "</b>");
for (CollectionBackend::Album album : albumlist) {
SongList songs = app_->collection_backend()->GetSongs(song_playing_.artist(), album.album_name, opt);
label_play_albums_->setText("<b>" + tr("Albums by %1").arg(song_playing_.effective_albumartist().toHtmlEscaped()) + "</b>");
for (const CollectionBackend::Album &album : albumlist) {
SongList songs = app_->collection_backend()->GetAlbumSongs(song_playing_.effective_albumartist(), album.album, opt);
widget_albums_->albums_model()->AddSongs(songs);
}
spacer_play_albums_->changeSize(20, 10, QSizePolicy::Fixed);

194
src/core/imageutils.cpp Normal file
View File

@ -0,0 +1,194 @@
/*
* Strawberry Music Player
* Copyright 2019, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QList>
#include <QBuffer>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QImage>
#include <QImageReader>
#include <QPixmap>
#include <QPainter>
#include <QSize>
#include <QSettings>
#include "imageutils.h"
#include "core/utilities.h"
#include "core/tagreaderclient.h"
QStringList ImageUtils::kSupportedImageMimeTypes;
QStringList ImageUtils::kSupportedImageFormats;
QStringList ImageUtils::SupportedImageMimeTypes() {
if (kSupportedImageMimeTypes.isEmpty()) {
for (const QByteArray &mimetype : QImageReader::supportedMimeTypes()) {
kSupportedImageMimeTypes << mimetype;
}
}
return kSupportedImageMimeTypes;
}
QStringList ImageUtils::SupportedImageFormats() {
if (kSupportedImageFormats.isEmpty()) {
for (const QByteArray &filetype : QImageReader::supportedImageFormats()) {
kSupportedImageFormats << filetype;
}
}
return kSupportedImageFormats;
}
QList<QByteArray> ImageUtils::ImageFormatsForMimeType(const QByteArray &mimetype) {
#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
return QImageReader::imageFormatsForMimeType(mimetype);
#else
if (mimetype == "image/bmp") return QList<QByteArray>() << "BMP";
else if (mimetype == "image/gif") return QList<QByteArray>() << "GIF";
else if (mimetype == "image/jpeg") return QList<QByteArray>() << "JPG";
else if (mimetype == "image/png") return QList<QByteArray>() << "PNG";
else return QList<QByteArray>();
#endif
}
QPixmap ImageUtils::TryLoadPixmap(const QUrl &art_automatic, const QUrl &art_manual, const QUrl &url) {
QPixmap ret;
if (!art_manual.path().isEmpty()) {
if (art_manual.path() == Song::kManuallyUnsetCover) return ret;
else if (art_manual.isLocalFile()) {
ret.load(art_manual.toLocalFile());
}
else if (art_manual.scheme().isEmpty()) {
ret.load(art_manual.path());
}
}
if (ret.isNull() && !art_automatic.path().isEmpty()) {
if (art_automatic.path() == Song::kEmbeddedCover && !url.isEmpty() && url.isLocalFile()) {
ret = QPixmap::fromImage(TagReaderClient::Instance()->LoadEmbeddedArtAsImageBlocking(url.toLocalFile()));
}
else if (art_automatic.isLocalFile()) {
ret.load(art_automatic.toLocalFile());
}
else if (art_automatic.scheme().isEmpty()) {
ret.load(art_automatic.path());
}
}
return ret;
}
QByteArray ImageUtils::SaveImageToJpegData(const QImage &image) {
if (image.isNull()) return QByteArray();
QByteArray image_data;
QBuffer buffer(&image_data);
if (buffer.open(QIODevice::WriteOnly)) {
image.save(&buffer, "JPEG");
buffer.close();
}
return image_data;
}
QImage ImageUtils::ScaleAndPad(const QImage &image, const bool scale, const bool pad, const int desired_height) {
if (image.isNull()) return image;
// Scale the image down
QImage image_scaled;
if (scale) {
image_scaled = image.scaled(QSize(desired_height, desired_height), Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
else {
image_scaled = image;
}
// Pad the image to height x height
if (pad) {
QImage image_padded(desired_height, desired_height, QImage::Format_ARGB32);
image_padded.fill(0);
QPainter p(&image_padded);
p.drawImage((desired_height - image_scaled.width()) / 2, (desired_height - image_scaled.height()) / 2, image_scaled);
p.end();
image_scaled = image_padded;
}
return image_scaled;
}
QImage ImageUtils::CreateThumbnail(const QImage &image, const bool pad, const QSize size) {
if (image.isNull()) return image;
QImage image_thumbnail;
if (pad) {
image_thumbnail = image.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QImage image_padded(size, QImage::Format_ARGB32_Premultiplied);
image_padded.fill(0);
QPainter p(&image_padded);
p.drawImage((image_padded.width() - image_thumbnail.width()) / 2, (image_padded.height() - image_thumbnail.height()) / 2, image_thumbnail);
p.end();
image_thumbnail = image_padded;
}
else {
image_thumbnail = image.scaledToHeight(size.height(), Qt::SmoothTransformation);
}
return image_thumbnail;
}
QImage ImageUtils::GenerateNoCoverImage(const QSize size) {
QImage image(":/pictures/cdcase.png");
// Get a square version of the nocover image with some transparency:
QImage image_scaled = image.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QImage image_square(size, QImage::Format_ARGB32);
image_square.fill(0);
QPainter p(&image_square);
p.setOpacity(0.4);
p.drawImage((size.width() - image_scaled.width()) / 2, (size.height() - image_scaled.height()) / 2, image_scaled);
p.end();
return image_square;
}

51
src/core/imageutils.h Normal file
View File

@ -0,0 +1,51 @@
/*
* Strawberry Music Player
* Copyright 2019, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef IMAGEUTILS_H
#define IMAGEUTILS_H
#include "config.h"
#include <QList>
#include <QByteArray>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QImage>
#include <QPixmap>
class ImageUtils {
private:
static QStringList kSupportedImageMimeTypes;
static QStringList kSupportedImageFormats;
public:
static QStringList SupportedImageMimeTypes();
static QStringList SupportedImageFormats();
static QList<QByteArray> ImageFormatsForMimeType(const QByteArray &mimetype);
static QByteArray SaveImageToJpegData(const QImage &image = QImage());
static QPixmap TryLoadPixmap(const QUrl &automatic, const QUrl &manual, const QUrl &url = QUrl());
static QImage ScaleAndPad(const QImage &image, const bool scale, const bool pad, const int desired_height);
static QImage CreateThumbnail(const QImage &image, const bool pad, const QSize size);
static QImage GenerateNoCoverImage(const QSize size = QSize());
};
#endif // IMAGEUTILS_H

View File

@ -143,6 +143,7 @@
#include "covermanager/albumcoverloaderresult.h"
#include "covermanager/currentalbumcoverloader.h"
#include "covermanager/coverproviders.h"
#include "covermanager/albumcoverimageresult.h"
#include "lyrics/lyricsproviders.h"
#ifndef Q_OS_WIN
# include "device/devicemanager.h"
@ -613,6 +614,8 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
QObject::connect(album_cover_choice_controller_->cover_from_url_action(), &QAction::triggered, this, &MainWindow::LoadCoverFromURL);
QObject::connect(album_cover_choice_controller_->search_for_cover_action(), &QAction::triggered, this, &MainWindow::SearchForCover);
QObject::connect(album_cover_choice_controller_->unset_cover_action(), &QAction::triggered, this, &MainWindow::UnsetCover);
QObject::connect(album_cover_choice_controller_->clear_cover_action(), &QAction::triggered, this, &MainWindow::ClearCover);
QObject::connect(album_cover_choice_controller_->delete_cover_action(), &QAction::triggered, this, &MainWindow::DeleteCover);
QObject::connect(album_cover_choice_controller_->show_cover_action(), &QAction::triggered, this, &MainWindow::ShowCover);
QObject::connect(album_cover_choice_controller_->search_cover_auto_action(), &QAction::triggered, this, &MainWindow::SearchCoverAutomatically);
QObject::connect(album_cover_choice_controller_->search_cover_auto_action(), &QAction::toggled, this, &MainWindow::ToggleSearchCoverAuto);
@ -1234,7 +1237,7 @@ void MainWindow::MediaStopped() {
song_playing_ = Song();
song_ = Song();
image_original_ = QImage();
album_cover_ = AlbumCoverImageResult();
app_->scrobbler()->ClearPlaying();
@ -1312,6 +1315,14 @@ void MainWindow::SongChanged(const Song &song) {
SendNowPlaying();
const bool enable_cover_options = song.url().isLocalFile() && !song.effective_albumartist().isEmpty() && !song.album().isEmpty();
album_cover_choice_controller_->cover_from_file_action()->setEnabled(enable_cover_options);
album_cover_choice_controller_->cover_from_url_action()->setEnabled(enable_cover_options);
album_cover_choice_controller_->search_for_cover_action()->setEnabled(enable_cover_options);
album_cover_choice_controller_->unset_cover_action()->setEnabled(enable_cover_options);
album_cover_choice_controller_->delete_cover_action()->setEnabled(enable_cover_options);
album_cover_choice_controller_->clear_cover_action()->setEnabled(enable_cover_options);
}
void MainWindow::TrackSkipped(PlaylistItemPtr item) {
@ -2073,7 +2084,7 @@ void MainWindow::RenumberTracks() {
song.set_track(track);
TagReaderReply *reply = TagReaderClient::Instance()->SaveFile(song.url().toLocalFile(), song);
QPersistentModelIndex persistent_index = QPersistentModelIndex(source_index);
QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, persistent_index]() { SongSaveComplete(reply, persistent_index); });
QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, persistent_index]() { SongSaveComplete(reply, persistent_index); }, Qt::QueuedConnection);
}
++track;
}
@ -2085,7 +2096,7 @@ void MainWindow::SongSaveComplete(TagReaderReply *reply, const QPersistentModelI
if (reply->is_successful() && idx.isValid()) {
app_->playlist_manager()->current()->ReloadItems(QList<int>()<< idx.row());
}
reply->deleteLater();
metaObject()->invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
}
@ -2104,7 +2115,7 @@ void MainWindow::SelectionSetValue() {
if (Playlist::set_column_value(song, column, column_value)) {
TagReaderReply *reply = TagReaderClient::Instance()->SaveFile(song.url().toLocalFile(), song);
QPersistentModelIndex persistent_index = QPersistentModelIndex(source_index);
QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, persistent_index]() { SongSaveComplete(reply, persistent_index); });
QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, persistent_index]() { SongSaveComplete(reply, persistent_index); }, Qt::QueuedConnection);
}
}
@ -2914,15 +2925,23 @@ void MainWindow::SearchForCover() {
}
void MainWindow::SaveCoverToFile() {
album_cover_choice_controller_->SaveCoverToFileManual(song_, image_original_);
album_cover_choice_controller_->SaveCoverToFileManual(song_, album_cover_);
}
void MainWindow::UnsetCover() {
album_cover_choice_controller_->UnsetCover(&song_);
}
void MainWindow::ClearCover() {
album_cover_choice_controller_->ClearCover(&song_);
}
void MainWindow::DeleteCover() {
album_cover_choice_controller_->DeleteCover(&song_);
}
void MainWindow::ShowCover() {
album_cover_choice_controller_->ShowCover(song_, image_original_);
album_cover_choice_controller_->ShowCover(song_, album_cover_.image);
}
void MainWindow::SearchCoverAutomatically() {
@ -2936,9 +2955,9 @@ void MainWindow::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult
if (song != song_playing_) return;
song_ = song;
image_original_ = result.image_original;
album_cover_ = result.album_cover;
emit AlbumCoverReady(song, result.image_original);
emit AlbumCoverReady(song, result.album_cover.image);
GetCoverAutomatically();

View File

@ -61,6 +61,7 @@
#include "settings/settingsdialog.h"
#include "settings/behavioursettingspage.h"
#include "covermanager/albumcoverloaderresult.h"
#include "covermanager/albumcoverimageresult.h"
class About;
class Console;
@ -255,6 +256,8 @@ class MainWindow : public QMainWindow, public PlatformInterface {
void LoadCoverFromURL();
void SearchForCover();
void UnsetCover();
void ClearCover();
void DeleteCover();
void ShowCover();
void SearchCoverAutomatically();
void AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &result);
@ -386,7 +389,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
Song song_;
Song song_playing_;
QImage image_original_;
AlbumCoverImageResult album_cover_;
int exit_count_;
bool delete_files_;

View File

@ -115,10 +115,10 @@ void RegisterMetaTypes() {
qRegisterMetaType<PlaylistSequence::ShuffleMode>("PlaylistSequence::ShuffleMode");
qRegisterMetaType<AlbumCoverLoaderResult>("AlbumCoverLoaderResult");
qRegisterMetaType<AlbumCoverLoaderResult::Type>("AlbumCoverLoaderResult::Type");
qRegisterMetaType<CoverSearchResult>("CoverSearchResult");
qRegisterMetaType<CoverProviderSearchResult>("CoverProviderSearchResult");
qRegisterMetaType<CoverSearchStatistics>("CoverSearchStatistics");
qRegisterMetaType<QList<CoverSearchResult> >("QList<CoverSearchResult>");
qRegisterMetaType<CoverSearchResults>("CoverSearchResults");
qRegisterMetaType<QList<CoverProviderSearchResult> >("QList<CoverProviderSearchResult>");
qRegisterMetaType<CoverProviderSearchResults>("CoverProviderSearchResults");
qRegisterMetaType<Equalizer::Params>("Equalizer::Params");
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
qRegisterMetaTypeStreamOperators<Equalizer::Params>("Equalizer::Params");

View File

@ -394,8 +394,8 @@ void Mpris2::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &re
AddMetadata("mpris:trackid", current_track_id(), &last_metadata_);
QUrl cover_url;
if (result.cover_url.isValid() && result.cover_url.isLocalFile() && QFile(result.cover_url.toLocalFile()).exists()) {
cover_url = result.cover_url;
if (result.album_cover.cover_url.isValid() && result.album_cover.cover_url.isLocalFile() && QFile(result.album_cover.cover_url.toLocalFile()).exists()) {
cover_url = result.album_cover.cover_url;
}
else if (result.temp_cover_url.isValid() && result.temp_cover_url.isLocalFile()) {
cover_url = result.temp_cover_url;

View File

@ -183,7 +183,7 @@ class Mpris2 : public QObject {
// Methods
void ActivatePlaylist(const QDBusObjectPath &playlist_id);
QList<MprisPlaylist> GetPlaylists(quint32 index, quint32 max_count, const QString &order, bool reverse_order);
MprisPlaylistList GetPlaylists(quint32 index, quint32 max_count, const QString &order, bool reverse_order);
signals:
// Player

View File

@ -341,10 +341,22 @@ bool Song::compilation_on() const { return d->compilation_on_; }
const QUrl &Song::art_automatic() const { return d->art_automatic_; }
const QUrl &Song::art_manual() const { return d->art_manual_; }
bool Song::has_manually_unset_cover() const { return d->art_manual_.path() == kManuallyUnsetCover; }
void Song::manually_unset_cover() { d->art_manual_ = QUrl::fromLocalFile(kManuallyUnsetCover); }
void Song::set_manually_unset_cover() { d->art_manual_ = QUrl::fromLocalFile(kManuallyUnsetCover); }
bool Song::has_embedded_cover() const { return d->art_automatic_.path() == kEmbeddedCover; }
void Song::set_embedded_cover() { d->art_automatic_ = QUrl::fromLocalFile(kEmbeddedCover); }
void Song::clear_art_automatic() { d->art_automatic_.clear(); }
void Song::clear_art_manual() { d->art_manual_.clear(); }
bool Song::save_embedded_cover_supported(const FileType filetype) {
return filetype == FileType_FLAC ||
filetype == FileType_OggVorbis ||
filetype == FileType_MPEG ||
filetype == FileType_MP4;
}
const QUrl &Song::stream_url() const { return d->stream_url_; }
const QUrl &Song::effective_stream_url() const { return !d->stream_url_.isEmpty() && d->stream_url_.isValid() ? d->stream_url_ : d->url_; }
const QImage &Song::image() const { return d->image_; }
@ -382,6 +394,8 @@ bool Song::art_manual_is_valid() const {
);
}
bool Song::has_valid_art() const { return art_automatic_is_valid() || art_manual_is_valid(); }
const QString &Song::error() const { return d->error_; }
void Song::set_id(int id) { d->id_ = id; }
@ -1054,13 +1068,19 @@ void Song::InitArtManual() {
if (QFile::exists(path)) {
d->art_manual_ = QUrl::fromLocalFile(path);
}
else if (d->url_.isLocalFile()) { // Pick the first image file in the album directory.
QFileInfo file(d->url_.toLocalFile());
QDir dir(file.path());
QStringList files = dir.entryList(QStringList() << "*.jpg" << "*.png" << "*.gif" << "*.jpeg", QDir::Files|QDir::Readable, QDir::Name);
if (files.count() > 0) {
d->art_manual_ = QUrl::fromLocalFile(file.path() + QDir::separator() + files.first());
}
}
}
void Song::InitArtAutomatic() {
if (d->source_ == Source_LocalFile && d->url_.isLocalFile() && d->art_automatic_.isEmpty()) {
// Pick the first image file in the album directory.
QFileInfo file(d->url_.toLocalFile());
QDir dir(file.path());
QStringList files = dir.entryList(QStringList() << "*.jpg" << "*.png" << "*.gif" << "*.jpeg", QDir::Files|QDir::Readable, QDir::Name);
if (files.count() > 0) {
d->art_automatic_ = QUrl::fromLocalFile(file.path() + QDir::separator() + files.first());
}
}
@ -1491,11 +1511,15 @@ bool Song::IsMetadataEqual(const Song &other) const {
d->bitrate_ == other.d->bitrate_ &&
d->samplerate_ == other.d->samplerate_ &&
d->bitdepth_ == other.d->bitdepth_ &&
d->art_automatic_ == other.d->art_automatic_ &&
d->art_manual_ == other.d->art_manual_ &&
d->cue_path_ == other.d->cue_path_;
}
bool Song::IsMetadataAndArtEqual(const Song &other) const {
return IsMetadataEqual(other) && d->art_automatic_ == other.d->art_automatic_ && d->art_manual_ == other.d->art_manual_;
}
bool Song::IsEditable() const {
return d->valid_ && !d->url_.isEmpty() && !is_stream() && d->source_ != Source_Unknown && d->filetype_ != FileType_Unknown && !has_cue();
}

View File

@ -159,6 +159,7 @@ class Song {
void InitFromQuery(const SqlRow &query, bool reliable_metadata, int col = 0);
void InitFromFilePartial(const QString &filename); // Just store the filename: incomplete but fast
void InitArtManual(); // Check if there is already a art in the cache and store the filename in art_manual
void InitArtAutomatic();
bool MergeFromSimpleMetaBundle(const Engine::SimpleMetaBundle &bundle);
@ -255,6 +256,7 @@ class Song {
bool is_metadata_good() const;
bool art_automatic_is_valid() const;
bool art_manual_is_valid() const;
bool has_valid_art() const;
bool is_compilation() const;
// Playlist views are special because you don't want to fill in album artists automatically for compilations, but you do for normal albums:
@ -264,13 +266,19 @@ class Song {
// Returns true if this Song had it's cover manually unset by user.
bool has_manually_unset_cover() const;
// This method represents an explicit request to unset this song's cover.
void manually_unset_cover();
void set_manually_unset_cover();
// Returns true if this song (it's media file) has an embedded cover.
bool has_embedded_cover() const;
// Sets a flag saying that this song (it's media file) has an embedded cover.
void set_embedded_cover();
void clear_art_automatic();
void clear_art_manual();
static bool save_embedded_cover_supported(const FileType filetype);
bool save_embedded_cover_supported() const { return url().isLocalFile() && save_embedded_cover_supported(filetype()) && !has_cue(); };
const QUrl &stream_url() const;
const QUrl &effective_stream_url() const;
const QImage &image() const;
@ -355,6 +363,7 @@ class Song {
// Comparison functions
bool IsMetadataEqual(const Song &other) const;
bool IsMetadataAndArtEqual(const Song &other) const;
bool IsOnSameAlbum(const Song &other) const;
bool IsSimilar(const Song &other) const;

View File

@ -102,7 +102,7 @@ void StandardItemIconLoader::AlbumCoverLoaded(const quint64 id, const AlbumCover
QStandardItem *item = pending_covers_.take(id);
if (!item) return;
if (!result.image_scaled.isNull()) {
if (result.success && !result.image_scaled.isNull() && result.type != AlbumCoverLoaderResult::Type_ManuallyUnset) {
item->setIcon(QIcon(QPixmap::fromImage(result.image_scaled)));
}

View File

@ -172,7 +172,24 @@ bool TagReaderClient::IsMediaFileBlocking(const QString &filename) {
}
QImage TagReaderClient::LoadEmbeddedArtBlocking(const QString &filename) {
QByteArray TagReaderClient::LoadEmbeddedArtBlocking(const QString &filename) {
Q_ASSERT(QThread::currentThread() != thread());
QByteArray ret;
TagReaderReply *reply = LoadEmbeddedArt(filename);
if (reply->WaitForFinished()) {
const std::string &data_str = reply->message().load_embedded_art_response().data();
ret = QByteArray(data_str.data(), data_str.size());
}
metaObject()->invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
return ret;
}
QImage TagReaderClient::LoadEmbeddedArtAsImageBlocking(const QString &filename) {
Q_ASSERT(QThread::currentThread() != thread());

View File

@ -63,7 +63,8 @@ class TagReaderClient : public QObject {
void ReadFileBlocking(const QString &filename, Song *song);
bool SaveFileBlocking(const QString &filename, const Song &metadata);
bool IsMediaFileBlocking(const QString &filename);
QImage LoadEmbeddedArtBlocking(const QString &filename);
QByteArray LoadEmbeddedArtBlocking(const QString &filename);
QImage LoadEmbeddedArtAsImageBlocking(const QString &filename);
bool SaveEmbeddedArtBlocking(const QString &filename, const QByteArray &data);
// TODO: Make this not a singleton

View File

@ -57,7 +57,7 @@
#include <QtEvents>
#include <QMessageBox>
#include <QNetworkInterface>
#include <QImageReader>
#include <QMimeDatabase>
#include <QtDebug>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
# include <QRandomGenerator>
@ -102,9 +102,6 @@
namespace Utilities {
QStringList kSupportedImageMimeTypes;
QStringList kSupportedImageFormats;
static QString tr(const char *str) {
return QCoreApplication::translate("", str);
}
@ -945,41 +942,23 @@ bool IsColorDark(const QColor &color) {
return ((30 * color.red() + 59 * color.green() + 11 * color.blue()) / 100) <= 130;
}
QStringList SupportedImageMimeTypes() {
QByteArray ReadDataFromFile(const QString &filename) {
if (kSupportedImageMimeTypes.isEmpty()) {
for (const QByteArray &mimetype : QImageReader::supportedMimeTypes()) {
kSupportedImageMimeTypes << mimetype;
}
QFile file(filename);
QByteArray data;
if (file.open(QIODevice::ReadOnly)) {
data = file.readAll();
file.close();
}
return kSupportedImageMimeTypes;
return data;
}
QStringList SupportedImageFormats() {
QString MimeTypeFromData(const QByteArray &data) {
if (kSupportedImageFormats.isEmpty()) {
for (const QByteArray &filetype : QImageReader::supportedImageFormats()) {
kSupportedImageFormats << filetype;
}
}
if (data.isEmpty()) return QString();
return kSupportedImageFormats;
}
QList<QByteArray> ImageFormatsForMimeType(const QByteArray &mimetype) {
#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
return QImageReader::imageFormatsForMimeType(mimetype);
#else
if (mimetype == "image/bmp") return QList<QByteArray>() << "BMP";
else if (mimetype == "image/gif") return QList<QByteArray>() << "GIF";
else if (mimetype == "image/jpeg") return QList<QByteArray>() << "JPG";
else if (mimetype == "image/png") return QList<QByteArray>() << "PNG";
else return QList<QByteArray>();
#endif
return QMimeDatabase().mimeTypeForData(data).name();
}

View File

@ -143,9 +143,8 @@ QString ReplaceVariable(const QString &variable, const Song &song, const QString
bool IsColorDark(const QColor &color);
QStringList SupportedImageMimeTypes();
QStringList SupportedImageFormats();
QList<QByteArray> ImageFormatsForMimeType(const QByteArray &mimetype);
QByteArray ReadDataFromFile(const QString &filename);
QString MimeTypeFromData(const QByteArray &data);
} // namespace

View File

@ -23,6 +23,9 @@
#include <QtGlobal>
#include <QGuiApplication>
#include <QtConcurrentRun>
#include <QFuture>
#include <QFutureWatcher>
#include <QScreen>
#include <QWindow>
#include <QWidget>
@ -45,12 +48,16 @@
#include <QFileDialog>
#include <QLabel>
#include <QAction>
#include <QActionGroup>
#include <QMenu>
#include <QSettings>
#include <QtEvents>
#include "core/utilities.h"
#include "core/imageutils.h"
#include "core/application.h"
#include "core/song.h"
#include "core/iconloader.h"
#include "core/application.h"
#include "collection/collectionbackend.h"
#include "settings/collectionsettingspage.h"
@ -61,6 +68,7 @@
#include "albumcoverfetcher.h"
#include "albumcoverloader.h"
#include "albumcoversearcher.h"
#include "albumcoverimageresult.h"
#include "coverfromurldialog.h"
#include "currentalbumcoverloader.h"
@ -77,11 +85,23 @@ AlbumCoverChoiceController::AlbumCoverChoiceController(QWidget *parent) :
cover_fetcher_(nullptr),
save_file_dialog_(nullptr),
cover_from_url_dialog_(nullptr),
cover_album_dir_(false),
cover_filename_(CollectionSettingsPage::SaveCover_Hash),
cover_from_file_(nullptr),
cover_to_file_(nullptr),
cover_from_url_(nullptr),
search_for_cover_(nullptr),
separator1_(nullptr),
unset_cover_(nullptr),
delete_cover_(nullptr),
clear_cover_(nullptr),
separator2_(nullptr),
show_cover_(nullptr),
search_cover_auto_(nullptr),
save_cover_type_(CollectionSettingsPage::SaveCoverType_Cache),
save_cover_filename_(CollectionSettingsPage::SaveCoverFilename_Hash),
cover_overwrite_(false),
cover_lowercase_(true),
cover_replace_spaces_(true)
cover_replace_spaces_(true),
save_embedded_cover_override_(false)
{
cover_from_file_ = new QAction(IconLoader::Load("document-open"), tr("Load cover from disk..."), this);
@ -89,14 +109,18 @@ AlbumCoverChoiceController::AlbumCoverChoiceController(QWidget *parent) :
cover_from_url_ = new QAction(IconLoader::Load("download"), tr("Load cover from URL..."), this);
search_for_cover_ = new QAction(IconLoader::Load("search"), tr("Search for album covers..."), this);
unset_cover_ = new QAction(IconLoader::Load("list-remove"), tr("Unset cover"), this);
delete_cover_ = new QAction(IconLoader::Load("list-remove"), tr("Delete cover"), this);
clear_cover_ = new QAction(IconLoader::Load("list-remove"), tr("Clear cover"), this);
separator1_ = new QAction(this);
separator1_->setSeparator(true);
show_cover_ = new QAction(IconLoader::Load("zoom-in"), tr("Show fullsize..."), this);
search_cover_auto_ = new QAction(tr("Search automatically"), this);
search_cover_auto_->setCheckable(true);
search_cover_auto_->setChecked(false);
separator_ = new QAction(this);
separator_->setSeparator(true);
separator2_ = new QAction(this);
separator2_->setSeparator(true);
ReloadSettings();
@ -113,6 +137,7 @@ void AlbumCoverChoiceController::Init(Application *app) {
cover_searcher_->Init(cover_fetcher_);
QObject::connect(cover_fetcher_, &AlbumCoverFetcher::AlbumCoverFetched, this, &AlbumCoverChoiceController::AlbumCoverFetched);
QObject::connect(app_->album_cover_loader(), &AlbumCoverLoader::SaveEmbeddedCoverAsyncFinished, this, &AlbumCoverChoiceController::SaveEmbeddedCoverAsyncFinished);
}
@ -120,8 +145,8 @@ void AlbumCoverChoiceController::ReloadSettings() {
QSettings s;
s.beginGroup(CollectionSettingsPage::kSettingsGroup);
cover_album_dir_ = s.value("cover_album_dir", false).toBool();
cover_filename_ = CollectionSettingsPage::SaveCover(s.value("cover_filename", CollectionSettingsPage::SaveCover_Hash).toInt());
save_cover_type_ = CollectionSettingsPage::SaveCoverType(s.value("save_cover_type", CollectionSettingsPage::SaveCoverType_Cache).toInt());
save_cover_filename_ = CollectionSettingsPage::SaveCoverFilename(s.value("save_cover_filename", CollectionSettingsPage::SaveCoverFilename_Hash).toInt());
cover_pattern_ = s.value("cover_pattern", "%albumartist-%album").toString();
cover_overwrite_ = s.value("cover_overwrite", false).toBool();
cover_lowercase_ = s.value("cover_lowercase", false).toBool();
@ -131,30 +156,74 @@ void AlbumCoverChoiceController::ReloadSettings() {
}
QList<QAction*> AlbumCoverChoiceController::GetAllActions() {
return QList<QAction*>() << cover_from_file_ << cover_to_file_ << separator_ << cover_from_url_ << search_for_cover_ << unset_cover_ << separator_ << show_cover_;
return QList<QAction*>() << show_cover_
<< cover_to_file_
<< separator1_
<< cover_from_file_
<< cover_from_url_
<< search_for_cover_
<< separator2_
<< unset_cover_
<< clear_cover_
<< delete_cover_;
}
AlbumCoverImageResult AlbumCoverChoiceController::LoadImageFromFile(Song *song) {
if (!song->url().isLocalFile()) return AlbumCoverImageResult();
QString cover_file = QFileDialog::getOpenFileName(this, tr("Load cover from disk"), GetInitialPathForFileDialog(*song, QString()), tr(kLoadImageFileFilter) + ";;" + tr(kAllFilesFilter));
if (cover_file.isEmpty()) return AlbumCoverImageResult();
AlbumCoverImageResult result;
QFile file(cover_file);
if (file.open(QIODevice::ReadOnly)) {
result.image_data = file.readAll();
file.close();
if (!result.image_data.isEmpty()) {
result.mime_type = Utilities::MimeTypeFromData(result.image_data);
result.image.loadFromData(result.image_data);
}
}
return result;
}
QUrl AlbumCoverChoiceController::LoadCoverFromFile(Song *song) {
if (!song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return QUrl();
QString cover_file = QFileDialog::getOpenFileName(this, tr("Load cover from disk"), GetInitialPathForFileDialog(*song, QString()), tr(kLoadImageFileFilter) + ";;" + tr(kAllFilesFilter));
if (cover_file.isNull()) return QUrl();
if (cover_file.isEmpty()) return QUrl();
// Can we load the image?
QImage image(cover_file);
if (QImage(cover_file).isNull()) return QUrl();
if (image.isNull()) {
return QUrl();
}
else {
QUrl cover_url(QUrl::fromLocalFile(cover_file));
SaveCoverToSong(song, cover_url);
return cover_url;
switch(get_save_album_cover_type()) {
case CollectionSettingsPage::SaveCoverType_Embedded:
if (song->save_embedded_cover_supported()) {
SaveCoverEmbeddedAutomatic(*song, cover_file);
return QUrl::fromLocalFile(Song::kEmbeddedCover);
}
// fallthrough
case CollectionSettingsPage::SaveCoverType_Cache:
case CollectionSettingsPage::SaveCoverType_Album:{
QUrl cover_url = QUrl::fromLocalFile(cover_file);
SaveArtManualToSong(song, cover_url);
return cover_url;
break;
}
}
return QUrl();
}
void AlbumCoverChoiceController::SaveCoverToFileManual(const Song &song, const QImage &image) {
void AlbumCoverChoiceController::SaveCoverToFileManual(const Song &song, const AlbumCoverImageResult &result) {
QString initial_file_name = "/";
@ -168,14 +237,29 @@ void AlbumCoverChoiceController::SaveCoverToFileManual(const Song &song, const Q
QString save_filename = QFileDialog::getSaveFileName(this, tr("Save album cover"), GetInitialPathForFileDialog(song, initial_file_name), tr(kSaveImageFileFilter) + ";;" + tr(kAllFilesFilter));
if (save_filename.isNull()) return;
if (save_filename.isEmpty()) return;
QString extension = save_filename.right(4);
if (!extension.startsWith('.') || !QImageWriter::supportedImageFormats().contains(extension.right(3).toUtf8())) {
QFileInfo fileinfo(save_filename);
if (fileinfo.suffix().isEmpty()) {
save_filename.append(".jpg");
fileinfo.setFile(save_filename);
}
image.save(save_filename);
if (!QImageWriter::supportedImageFormats().contains(fileinfo.completeSuffix())) {
save_filename = Utilities::PathWithoutFilenameExtension(save_filename) + ".jpg";
fileinfo.setFile(save_filename);
}
if (result.is_jpeg() && fileinfo.completeSuffix().toLower() == "jpg") {
QFile file(save_filename);
if (file.open(QIODevice::WriteOnly)) {
file.write(result.image_data);
file.close();
}
}
else {
result.image.save(save_filename);
}
}
@ -203,67 +287,138 @@ QString AlbumCoverChoiceController::GetInitialPathForFileDialog(const Song &song
QUrl AlbumCoverChoiceController::LoadCoverFromURL(Song *song) {
if (!cover_from_url_dialog_) { cover_from_url_dialog_ = new CoverFromURLDialog(this); }
if (!song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return QUrl();
QImage image = cover_from_url_dialog_->Exec();
AlbumCoverImageResult result = LoadImageFromURL();
if (image.isNull()) {
if (result.image.isNull()) {
return QUrl();
}
else {
QUrl cover_url = SaveCoverToFileAutomatic(song, QUrl(), image, true);
if (cover_url.isEmpty()) return QUrl();
SaveCoverToSong(song, cover_url);
return cover_url;
return SaveCoverAutomatic(song, result);
}
}
AlbumCoverImageResult AlbumCoverChoiceController::LoadImageFromURL() {
if (!cover_from_url_dialog_) { cover_from_url_dialog_ = new CoverFromURLDialog(this); }
return cover_from_url_dialog_->Exec();
}
QUrl AlbumCoverChoiceController::SearchForCover(Song *song) {
QString album = song->effective_album();
album.remove(Song::kAlbumRemoveDisc);
album.remove(Song::kAlbumRemoveMisc);
if (!song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return QUrl();
// Get something sensible to stick in the search box
QImage image = cover_searcher_->Exec(song->effective_albumartist(), album);
if (image.isNull()) {
return QUrl();
AlbumCoverImageResult result = SearchForImage(song);
if (result.is_valid()) {
return SaveCoverAutomatic(song, result);
}
else {
QUrl cover_url = SaveCoverToFileAutomatic(song, QUrl(), image, true);
if (cover_url.isEmpty()) return QUrl();
SaveCoverToSong(song, cover_url);
return cover_url;
return QUrl();
}
}
AlbumCoverImageResult AlbumCoverChoiceController::SearchForImage(Song *song) {
if (!song->url().isLocalFile()) return AlbumCoverImageResult();
QString album = song->effective_album();
album = album.remove(Song::kAlbumRemoveDisc).remove(Song::kAlbumRemoveMisc);
// Get something sensible to stick in the search box
return cover_searcher_->Exec(song->effective_albumartist(), album);
}
QUrl AlbumCoverChoiceController::UnsetCover(Song *song) {
QUrl cover_url(QUrl::fromLocalFile(Song::kManuallyUnsetCover));
SaveCoverToSong(song, cover_url);
if (!song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return QUrl();
QUrl cover_url = QUrl::fromLocalFile(Song::kManuallyUnsetCover);
SaveArtManualToSong(song, cover_url);
return cover_url;
}
void AlbumCoverChoiceController::ShowCover(const Song &song) {
void AlbumCoverChoiceController::ClearCover(Song *song) {
QPixmap pixmap = AlbumCoverLoader::TryLoadPixmap(song.art_automatic(), song.art_manual(), song.url());
if (pixmap.isNull()) return;
ShowCover(song, pixmap);
if (!song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return;
song->clear_art_manual();
SaveArtManualToSong(song, QUrl());
}
bool AlbumCoverChoiceController::DeleteCover(Song *song) {
if (!song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return false;
if (song->has_embedded_cover() && song->save_embedded_cover_supported()) {
SaveCoverEmbeddedAutomatic(*song, AlbumCoverImageResult());
}
QString art_automatic;
QString art_manual;
if (song->art_automatic().isValid() && song->art_automatic().isLocalFile()) {
art_automatic = song->art_automatic().toLocalFile();
}
if (song->art_manual().isValid() && song->art_manual().isLocalFile()) {
art_manual = song->art_manual().toLocalFile();
}
bool success = true;
if (!art_automatic.isEmpty()) {
if (QFile::exists(art_automatic)) {
if (QFile::remove(art_automatic)) {
song->clear_art_automatic();
if (art_automatic == art_manual) song->clear_art_manual();
}
else success = false;
}
else song->clear_art_automatic();
}
else song->clear_art_automatic();
if (!art_manual.isEmpty()) {
if (QFile::exists(art_manual)) {
if (QFile::remove(art_manual)) {
song->clear_art_manual();
if (art_automatic == art_manual) song->clear_art_automatic();
}
else success = false;
}
else song->clear_art_manual();
}
else song->clear_art_manual();
if (success) UnsetCover(song);
return success;
}
void AlbumCoverChoiceController::ShowCover(const Song &song, const QImage &image) {
if (song.art_manual().isLocalFile() || song.art_automatic().isLocalFile()) {
QPixmap pixmap = AlbumCoverLoader::TryLoadPixmap(song.art_automatic(), song.art_manual(), song.url());
if (image.isNull()) {
if ((song.art_manual().isValid() && song.art_manual().isLocalFile() && QFile::exists(song.art_manual().toLocalFile())) ||
(song.art_automatic().isValid() && song.art_automatic().isLocalFile() && QFile::exists(song.art_automatic().toLocalFile())) ||
song.has_embedded_cover()
) {
QPixmap pixmap = ImageUtils::TryLoadPixmap(song.art_automatic(), song.art_manual(), song.url());
if (!pixmap.isNull()) ShowCover(song, pixmap);
}
}
else {
QPixmap pixmap = QPixmap::fromImage(image);
if (!pixmap.isNull()) ShowCover(song, pixmap);
}
else if (!image.isNull()) ShowCover(song, QPixmap::fromImage(image));
}
@ -326,7 +481,7 @@ qint64 AlbumCoverChoiceController::SearchCoverAutomatically(const Song &song) {
}
void AlbumCoverChoiceController::AlbumCoverFetched(const quint64 id, const QUrl &cover_url, const QImage &image, const CoverSearchStatistics &statistics) {
void AlbumCoverChoiceController::AlbumCoverFetched(const quint64 id, const AlbumCoverImageResult &result, const CoverSearchStatistics &statistics) {
Q_UNUSED(statistics);
@ -335,46 +490,22 @@ void AlbumCoverChoiceController::AlbumCoverFetched(const quint64 id, const QUrl
song = cover_fetching_tasks_.take(id);
}
if (!image.isNull()) {
QUrl new_cover_url = SaveCoverToFileAutomatic(&song, cover_url, image, false);
if (!new_cover_url.isEmpty()) SaveCoverToSong(&song, new_cover_url);
if (result.is_valid()) {
SaveCoverAutomatic(&song, result);
}
emit AutomaticCoverSearchDone();
}
void AlbumCoverChoiceController::SaveCoverToSong(Song *song, const QUrl &cover_url) {
void AlbumCoverChoiceController::SaveArtAutomaticToSong(Song *song, const QUrl &art_automatic) {
if (!song->is_valid()) return;
song->set_art_manual(cover_url);
if (song->id() != -1) { // Update the backends.
switch (song->source()) {
case Song::Source_Collection:
app_->collection_backend()->UpdateManualAlbumArtAsync(song->artist(), song->albumartist(), song->album(), cover_url);
break;
case Song::Source_LocalFile:
case Song::Source_CDDA:
case Song::Source_Device:
case Song::Source_Stream:
case Song::Source_Unknown:
break;
case Song::Source_Tidal:
case Song::Source_Qobuz:
case Song::Source_Subsonic:
InternetService *service = app_->internet_services()->ServiceBySource(song->source());
if (!service) break;
if (service->artists_collection_backend())
service->artists_collection_backend()->UpdateManualAlbumArtAsync(song->artist(), song->albumartist(), song->album(), cover_url);
if (service->albums_collection_backend())
service->albums_collection_backend()->UpdateManualAlbumArtAsync(song->artist(), song->albumartist(), song->album(), cover_url);
if (service->songs_collection_backend())
service->songs_collection_backend()->UpdateManualAlbumArtAsync(song->artist(), song->albumartist(), song->album(), cover_url);
break;
}
song->set_art_automatic(art_automatic);
if (song->source() == Song::Source_Collection) {
app_->collection_backend()->UpdateAutomaticAlbumArtAsync(song->effective_albumartist(), song->album(), art_automatic);
}
if (*song == app_->current_albumcover_loader()->last_song()) {
@ -383,27 +514,168 @@ void AlbumCoverChoiceController::SaveCoverToSong(Song *song, const QUrl &cover_u
}
QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song *song, const QUrl &cover_url, const QImage &image, const bool overwrite) {
void AlbumCoverChoiceController::SaveArtManualToSong(Song *song, const QUrl &art_manual, const bool clear_art_automatic) {
return SaveCoverToFileAutomatic(song->source(), song->effective_albumartist(), song->effective_album(), song->album_id(), song->url().adjusted(QUrl::RemoveFilename).path(), cover_url, image, overwrite);
if (!song->is_valid()) return;
song->set_art_manual(art_manual);
if (clear_art_automatic) song->clear_art_automatic();
// Update the backends.
switch (song->source()) {
case Song::Source_Collection:
app_->collection_backend()->UpdateManualAlbumArtAsync(song->effective_albumartist(), song->album(), art_manual, clear_art_automatic);
break;
case Song::Source_LocalFile:
case Song::Source_CDDA:
case Song::Source_Device:
case Song::Source_Stream:
case Song::Source_Unknown:
break;
case Song::Source_Tidal:
case Song::Source_Qobuz:
case Song::Source_Subsonic:
InternetService *service = app_->internet_services()->ServiceBySource(song->source());
if (!service) break;
if (service->artists_collection_backend()) {
service->artists_collection_backend()->UpdateManualAlbumArtAsync(song->effective_albumartist(), song->album(), art_manual, clear_art_automatic);
}
if (service->albums_collection_backend()) {
service->albums_collection_backend()->UpdateManualAlbumArtAsync(song->effective_albumartist(), song->album(), art_manual, clear_art_automatic);
}
if (service->songs_collection_backend()) {
service->songs_collection_backend()->UpdateManualAlbumArtAsync(song->effective_albumartist(), song->album(), art_manual, clear_art_automatic);
}
break;
}
if (*song == app_->current_albumcover_loader()->last_song()) {
app_->current_albumcover_loader()->LoadAlbumCover(*song);
}
}
QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song::Source source, const QString &artist, const QString &album, const QString &album_id, const QString &album_dir, const QUrl &cover_url, const QImage &image, const bool overwrite) {
QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song *song, const AlbumCoverImageResult &result, const bool force_overwrite) {
QString filepath = app_->album_cover_loader()->CoverFilePath(source, artist, album, album_id, album_dir, cover_url, "jpg");
return SaveCoverToFileAutomatic(song->source(),
song->effective_albumartist(),
song->effective_album(),
song->album_id(),
song->url().adjusted(QUrl::RemoveFilename).path(),
result,
force_overwrite);
}
QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song::Source source,
const QString &artist,
const QString &album,
const QString &album_id,
const QString &album_dir,
const AlbumCoverImageResult &result,
const bool force_overwrite) {
QString filepath = app_->album_cover_loader()->CoverFilePath(source, artist, album, album_id, album_dir, result.cover_url, "jpg");
if (filepath.isEmpty()) return QUrl();
QUrl new_cover_url(QUrl::fromLocalFile(filepath));
// Don't overwrite when saving in album dir if the filename is set to pattern unless the "overwrite" is set.
if (source == Song::Source_Collection && QFile::exists(filepath) && !cover_overwrite_ && !overwrite && cover_album_dir_ && cover_filename_ == CollectionSettingsPage::SaveCover_Pattern) {
return new_cover_url;
QFile file(filepath);
// Don't overwrite when saving in album dir if the filename is set to pattern unless "force_overwrite" is set.
if (source == Song::Source_Collection && !cover_overwrite_ && !force_overwrite && get_save_album_cover_type() == CollectionSettingsPage::SaveCoverType_Album && save_cover_filename_ == CollectionSettingsPage::SaveCoverFilename_Pattern && file.exists()) {
while (file.exists()) {
QFileInfo fileinfo(file.fileName());
file.setFileName(fileinfo.path() + "/0" + fileinfo.fileName());
}
filepath = file.fileName();
}
if (!image.save(filepath, "JPG") && !QFile::exists(filepath)) return QUrl();
QUrl cover_url;
if (result.is_jpeg()) {
if (file.open(QIODevice::WriteOnly)) {
if (file.write(result.image_data)) cover_url = QUrl::fromLocalFile(filepath);
file.close();
}
}
else {
if (result.image.save(filepath, "JPG")) cover_url = QUrl::fromLocalFile(filepath);
}
return new_cover_url;
return cover_url;
}
void AlbumCoverChoiceController::SaveCoverEmbeddedAutomatic(const Song &song, const AlbumCoverImageResult &result) {
if (song.source() == Song::Source_Collection) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QFuture<SongList> future = QtConcurrent::run(&CollectionBackend::GetAlbumSongs, app_->collection_backend(), song.effective_albumartist(), song.effective_album(), QueryOptions());
#else
QFuture<SongList> future = QtConcurrent::run(app_->collection_backend(), &CollectionBackend::GetAlbumSongs, song.effective_albumartist(), song.effective_album(), QueryOptions());
#endif
QFutureWatcher<SongList> *watcher = new QFutureWatcher<SongList>();
watcher->setFuture(future);
QObject::connect(watcher, &QFutureWatcher<SongList>::finished, this, [=]() {
SongList songs = watcher->result();
watcher->deleteLater();
QList<QUrl> urls;
for (const Song &s : songs) urls << s.url();
if (result.is_jpeg()) {
qint64 id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.image_data);
QMutexLocker l(&mutex_cover_save_tasks_);
cover_save_tasks_.insert(id, song);
}
else {
qint64 id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.image);
QMutexLocker l(&mutex_cover_save_tasks_);
cover_save_tasks_.insert(id, song);
}
});
}
else {
if (result.is_jpeg()) {
app_->album_cover_loader()->SaveEmbeddedCoverAsync(song.url().toLocalFile(), result.image_data);
}
else {
app_->album_cover_loader()->SaveEmbeddedCoverAsync(song.url().toLocalFile(), result.image);
}
}
}
void AlbumCoverChoiceController::SaveCoverEmbeddedAutomatic(const Song &song, const QUrl &cover_url) {
SaveCoverEmbeddedAutomatic(song, cover_url.toLocalFile());
}
void AlbumCoverChoiceController::SaveCoverEmbeddedAutomatic(const Song &song, const QString &cover_filename) {
if (song.source() == Song::Source_Collection) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QFuture<SongList> future = QtConcurrent::run(&CollectionBackend::GetAlbumSongs, app_->collection_backend(), song.effective_albumartist(), song.effective_album(), QueryOptions());
#else
QFuture<SongList> future = QtConcurrent::run(app_->collection_backend(), &CollectionBackend::GetAlbumSongs, song->effective_albumartist(), song->effective_album(), QueryOptions());
#endif
QFutureWatcher<SongList> *watcher = new QFutureWatcher<SongList>();
watcher->setFuture(future);
QObject::connect(watcher, &QFutureWatcher<SongList>::finished, this, [=]() {
SongList songs = watcher->result();
watcher->deleteLater();
QList<QUrl> urls;
for (const Song &s : songs) urls << s.url();
qint64 id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, cover_filename);
QMutexLocker l(&mutex_cover_save_tasks_);
cover_save_tasks_.insert(id, song);
});
}
else {
app_->album_cover_loader()->SaveEmbeddedCoverAsync(song.url().toLocalFile(), cover_filename);
}
}
void AlbumCoverChoiceController::SaveCoverEmbeddedAutomatic(const QList<QUrl> urls, const QImage &image) {
app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, image);
}
@ -436,7 +708,13 @@ QUrl AlbumCoverChoiceController::SaveCover(Song *song, const QDropEvent *e) {
const QString suffix = QFileInfo(filename).suffix().toLower();
if (IsKnownImageExtension(suffix)) {
SaveCoverToSong(song, url);
if (get_save_album_cover_type() == CollectionSettingsPage::SaveCoverType_Embedded && song->save_embedded_cover_supported()) {
SaveCoverEmbeddedAutomatic(*song, filename);
return QUrl::fromLocalFile(Song::kEmbeddedCover);
}
else {
SaveArtManualToSong(song, url);
}
return url;
}
}
@ -444,13 +722,43 @@ QUrl AlbumCoverChoiceController::SaveCover(Song *song, const QDropEvent *e) {
if (e->mimeData()->hasImage()) {
QImage image = qvariant_cast<QImage>(e->mimeData()->imageData());
if (!image.isNull()) {
QUrl cover_url = SaveCoverToFileAutomatic(song, QUrl(), image, true);
if (cover_url.isEmpty()) return QUrl();
SaveCoverToSong(song, cover_url);
return cover_url;
return SaveCoverAutomatic(song, AlbumCoverImageResult(image));
}
}
return QUrl();
}
QUrl AlbumCoverChoiceController::SaveCoverAutomatic(Song *song, const AlbumCoverImageResult &result) {
QUrl cover_url;
switch(get_save_album_cover_type()) {
case CollectionSettingsPage::SaveCoverType_Embedded:{
if (song->save_embedded_cover_supported()) {
SaveCoverEmbeddedAutomatic(*song, result);
cover_url = QUrl::fromLocalFile(Song::kEmbeddedCover);
break;
}
}
// fallthrough
case CollectionSettingsPage::SaveCoverType_Cache:
case CollectionSettingsPage::SaveCoverType_Album:{
cover_url = SaveCoverToFileAutomatic(song, result);
if (!cover_url.isEmpty()) SaveArtManualToSong(song, cover_url);
break;
}
}
return cover_url;
}
void AlbumCoverChoiceController::SaveEmbeddedCoverAsyncFinished(quint64 id, const bool success) {
if (!cover_save_tasks_.contains(id)) return;
Song song = cover_save_tasks_.take(id);
if (success) SaveArtAutomaticToSong(&song, QUrl::fromLocalFile(Song::kEmbeddedCover));
}

View File

@ -27,18 +27,24 @@
#include <QtGlobal>
#include <QObject>
#include <QWidget>
#include <QPair>
#include <QSet>
#include <QList>
#include <QMap>
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QImage>
#include <QMutex>
#include "core/song.h"
#include "settings/collectionsettingspage.h"
#include "albumcoverimageresult.h"
class QFileDialog;
class QAction;
class QActionGroup;
class QMenu;
class QDragEnterEvent;
class QDropEvent;
@ -63,6 +69,9 @@ class AlbumCoverChoiceController : public QWidget {
void Init(Application *app);
void ReloadSettings();
CollectionSettingsPage::SaveCoverType get_save_album_cover_type() const { return (save_embedded_cover_override_ ? CollectionSettingsPage::SaveCoverType_Embedded : save_cover_type_); }
void set_save_embedded_cover_override(const bool value) { save_embedded_cover_override_ = value; }
// Getters for all QActions implemented by this controller.
QAction *cover_from_file_action() const { return cover_from_file_; }
@ -70,6 +79,8 @@ class AlbumCoverChoiceController : public QWidget {
QAction *cover_from_url_action() const { return cover_from_url_; }
QAction *search_for_cover_action() const { return search_for_cover_; }
QAction *unset_cover_action() const { return unset_cover_; }
QAction *delete_cover_action() const { return delete_cover_; }
QAction *clear_cover_action() const { return clear_cover_; }
QAction *show_cover_action() const { return show_cover_; }
QAction *search_cover_auto_action() const { return search_cover_auto_; }
@ -87,40 +98,54 @@ class AlbumCoverChoiceController : public QWidget {
// Lets the user choose a cover from disk. If no cover will be chosen or the chosen cover will not be a proper image, this returns an empty string.
// Otherwise, the path to the chosen cover will be returned.
AlbumCoverImageResult LoadImageFromFile(Song *song);
QUrl LoadCoverFromFile(Song *song);
// Shows a dialog that allows user to save the given image on disk.
// The image is supposed to be the cover of the given song's album.
void SaveCoverToFileManual(const Song &song, const QImage &image);
void SaveCoverToFileManual(const Song &song, const AlbumCoverImageResult &result);
// Downloads the cover from an URL given by user.
// This returns the downloaded image or null image if something went wrong for example when user cancelled the dialog.
QUrl LoadCoverFromURL(Song *song);
AlbumCoverImageResult LoadImageFromURL();
// Lets the user choose a cover among all that have been found on last.fm.
// Returns the chosen cover or null cover if user didn't choose anything.
QUrl SearchForCover(Song *song);
AlbumCoverImageResult SearchForImage(Song *song);
// Returns a path which indicates that the cover has been unset manually.
QUrl UnsetCover(Song *song);
// Clears any album cover art associated with the song.
void ClearCover(Song *song);
// Physically deletes associated album covers from disk.
bool DeleteCover(Song *song);
// Shows the cover of given song in it's original size.
void ShowCover(const Song &song);
void ShowCover(const Song &song, const QImage &image);
void ShowCover(const Song &song, const QImage &image = QImage());
void ShowCover(const Song &song, const QPixmap &pixmap);
// Search for covers automatically
qint64 SearchCoverAutomatically(const Song &song);
// Saves the chosen cover as manual cover path of this song in collection.
void SaveCoverToSong(Song *song, const QUrl &cover_url);
void SaveArtAutomaticToSong(Song *song, const QUrl &art_automatic);
void SaveArtManualToSong(Song *song, const QUrl &art_manual, const bool clear_art_automatic = false);
// Saves the cover that the user picked through a drag and drop operation.
QUrl SaveCover(Song *song, const QDropEvent *e);
// Saves the given image in album directory or cache as a cover for 'album artist' - 'album'. The method returns path of the image.
QUrl SaveCoverToFileAutomatic(const Song *song, const QUrl &cover_url, const QImage &image, const bool overwrite = false);
QUrl SaveCoverToFileAutomatic(const Song::Source source, const QString &artist, const QString &album, const QString &album_id, const QString &album_dir, const QUrl &cover_url, const QImage &image, const bool overwrite = false);
QUrl SaveCoverAutomatic(Song *song, const AlbumCoverImageResult &result);
QUrl SaveCoverToFileAutomatic(const Song *song, const AlbumCoverImageResult &result, const bool force_overwrite = false);
QUrl SaveCoverToFileAutomatic(const Song::Source source, const QString &artist, const QString &album, const QString &album_id, const QString &album_dir, const AlbumCoverImageResult &result, const bool force_overwrite = false);
void SaveCoverEmbeddedAutomatic(const Song &song, const AlbumCoverImageResult &result);
void SaveCoverEmbeddedAutomatic(const Song &song, const QUrl &cover_url);
void SaveCoverEmbeddedAutomatic(const Song &song, const QString &cover_filename);
void SaveCoverEmbeddedAutomatic(const QList<QUrl> urls, const QImage &image);
static bool CanAcceptDrag(const QDragEnterEvent *e);
@ -128,9 +153,11 @@ class AlbumCoverChoiceController : public QWidget {
void AutomaticCoverSearchDone();
private slots:
void AlbumCoverFetched(const quint64 id, const QUrl &cover_url, const QImage &image, const CoverSearchStatistics &statistics);
void AlbumCoverFetched(const quint64 id, const AlbumCoverImageResult &result, const CoverSearchStatistics &statistics);
void SaveEmbeddedCoverAsyncFinished(quint64 id, const bool success);
private:
QString GetInitialPathForFileDialog(const Song &song, const QString &filename);
static bool IsKnownImageExtension(const QString &suffix);
@ -145,21 +172,27 @@ class AlbumCoverChoiceController : public QWidget {
QAction *cover_from_file_;
QAction *cover_to_file_;
QAction *separator_;
QAction *cover_from_url_;
QAction *search_for_cover_;
QAction *separator1_;
QAction *unset_cover_;
QAction *delete_cover_;
QAction *clear_cover_;
QAction *separator2_;
QAction *show_cover_;
QAction *search_cover_auto_;
QMap<quint64, Song> cover_fetching_tasks_;
QMap<qint64, Song> cover_save_tasks_;
QMutex mutex_cover_save_tasks_;
bool cover_album_dir_;
CollectionSettingsPage::SaveCover cover_filename_;
CollectionSettingsPage::SaveCoverType save_cover_type_;
CollectionSettingsPage::SaveCoverFilename save_cover_filename_;
QString cover_pattern_;
bool cover_overwrite_;
bool cover_lowercase_;
bool cover_replace_spaces_;
bool save_embedded_cover_override_;
};

View File

@ -56,7 +56,7 @@ AlbumCoverFetcher::~AlbumCoverFetcher() {
}
quint64 AlbumCoverFetcher::FetchAlbumCover(const QString &artist, const QString &album, const QString &title, bool fetchall) {
quint64 AlbumCoverFetcher::FetchAlbumCover(const QString &artist, const QString &album, const QString &title, const bool batch) {
CoverSearchRequest request;
request.id = next_id_++;
@ -66,7 +66,7 @@ quint64 AlbumCoverFetcher::FetchAlbumCover(const QString &artist, const QString
request.album = request.album.remove(Song::kAlbumRemoveMisc);
request.title = title;
request.search = false;
request.fetchall = fetchall;
request.batch = batch;
AddRequest(request);
return request.id;
@ -83,7 +83,7 @@ quint64 AlbumCoverFetcher::SearchForCovers(const QString &artist, const QString
request.album = request.album.remove(Song::kAlbumRemoveMisc);
request.title = title;
request.search = true;
request.fetchall = false;
request.batch = false;
AddRequest(request);
return request.id;
@ -135,7 +135,7 @@ void AlbumCoverFetcher::StartRequests() {
}
void AlbumCoverFetcher::SingleSearchFinished(const quint64 request_id, const CoverSearchResults results) {
void AlbumCoverFetcher::SingleSearchFinished(const quint64 request_id, const CoverProviderSearchResults &results) {
if (!active_requests_.contains(request_id)) return;
AlbumCoverFetcherSearch *search = active_requests_.take(request_id);
@ -145,12 +145,12 @@ void AlbumCoverFetcher::SingleSearchFinished(const quint64 request_id, const Cov
}
void AlbumCoverFetcher::SingleCoverFetched(const quint64 request_id, const QUrl &cover_url, const QImage &image) {
void AlbumCoverFetcher::SingleCoverFetched(const quint64 request_id, const AlbumCoverImageResult &result) {
if (!active_requests_.contains(request_id)) return;
AlbumCoverFetcherSearch *search = active_requests_.take(request_id);
search->deleteLater();
emit AlbumCoverFetched(request_id, cover_url, image, search->statistics());
emit AlbumCoverFetched(request_id, result, search->statistics());
}

View File

@ -31,11 +31,13 @@
#include <QList>
#include <QHash>
#include <QQueue>
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QImage>
#include "coversearchstatistics.h"
#include "albumcoverimageresult.h"
class QTimer;
class NetworkAccessManager;
@ -44,7 +46,7 @@ class AlbumCoverFetcherSearch;
// This class represents a single search-for-cover request. It identifies and describes the request.
struct CoverSearchRequest {
explicit CoverSearchRequest() : id(-1), search(false), fetchall(false) {}
explicit CoverSearchRequest() : id(-1), search(false), batch(false) {}
// An unique (for one AlbumCoverFetcher) request identifier
quint64 id;
@ -57,13 +59,13 @@ struct CoverSearchRequest {
// Is this only a search request or should we also fetch the first cover that's found?
bool search;
// Is the request part of fetchall (fetching all missing covers)
bool fetchall;
// Is the request part of a batch (fetching all missing covers)
bool batch;
};
// This structure represents a single result of some album's cover search request.
struct CoverSearchResult {
explicit CoverSearchResult() : score_provider(0.0), score_match(0.0), score_quality(0.0), number(0) {}
struct CoverProviderSearchResult {
explicit CoverProviderSearchResult() : score_provider(0.0), score_match(0.0), score_quality(0.0), number(0) {}
// Used for grouping in the user interface.
QString provider;
@ -94,11 +96,11 @@ struct CoverSearchResult {
float score() const { return score_provider + score_match + score_quality; }
};
Q_DECLARE_METATYPE(CoverSearchResult)
Q_DECLARE_METATYPE(CoverProviderSearchResult)
// This is a complete result of a single search request (a list of results, each describing one image, actually).
typedef QList<CoverSearchResult> CoverSearchResults;
Q_DECLARE_METATYPE(QList<CoverSearchResult>)
typedef QList<CoverProviderSearchResult> CoverProviderSearchResults;
Q_DECLARE_METATYPE(QList<CoverProviderSearchResult>)
// This class searches for album covers for a given query or artist/album and returns URLs. It's NOT thread-safe.
class AlbumCoverFetcher : public QObject {
@ -111,17 +113,17 @@ class AlbumCoverFetcher : public QObject {
static const int kMaxConcurrentRequests;
quint64 SearchForCovers(const QString &artist, const QString &album, const QString &title = QString());
quint64 FetchAlbumCover(const QString &artist, const QString &album, const QString &title, const bool fetchall);
quint64 FetchAlbumCover(const QString &artist, const QString &album, const QString &title, const bool batch);
void Clear();
signals:
void AlbumCoverFetched(quint64 request_id, QUrl cover_url, QImage cover, CoverSearchStatistics statistics);
void SearchFinished(quint64 request_id, CoverSearchResults results, CoverSearchStatistics statistics);
void AlbumCoverFetched(quint64 request_id, AlbumCoverImageResult result, CoverSearchStatistics statistics);
void SearchFinished(quint64 request_id, CoverProviderSearchResults results, CoverSearchStatistics statistics);
private slots:
void SingleSearchFinished(const quint64, const CoverSearchResults results);
void SingleCoverFetched(const quint64, const QUrl &cover_url, const QImage &image);
void SingleSearchFinished(const quint64, const CoverProviderSearchResults &results);
void SingleCoverFetched(const quint64, const AlbumCoverImageResult &result);
void StartRequests();
private:

View File

@ -40,12 +40,14 @@
#include "core/logging.h"
#include "core/utilities.h"
#include "core/imageutils.h"
#include "core/networkaccessmanager.h"
#include "core/networktimeouts.h"
#include "albumcoverfetcher.h"
#include "albumcoverfetchersearch.h"
#include "coverprovider.h"
#include "coverproviders.h"
#include "albumcoverimageresult.h"
const int AlbumCoverFetcherSearch::kSearchTimeoutMs = 20000;
const int AlbumCoverFetcherSearch::kImageLoadTimeoutMs = 6000;
@ -99,8 +101,8 @@ void AlbumCoverFetcherSearch::Start(CoverProviders *cover_providers) {
continue;
}
// Skip provider if it does not have fetchall set and we are doing fetchall - "Fetch Missing Covers".
if (!provider->fetchall() && request_.fetchall) {
// Skip provider if it does not have batch set and we are doing a batch - "Fetch Missing Covers".
if (!provider->batch() && request_.batch) {
continue;
}
@ -109,7 +111,7 @@ void AlbumCoverFetcherSearch::Start(CoverProviders *cover_providers) {
continue;
}
QObject::connect(provider, &CoverProvider::SearchResults, this, QOverload<const int, const CoverSearchResults&>::of(&AlbumCoverFetcherSearch::ProviderSearchResults));
QObject::connect(provider, &CoverProvider::SearchResults, this, QOverload<const int, const CoverProviderSearchResults&>::of(&AlbumCoverFetcherSearch::ProviderSearchResults));
QObject::connect(provider, &CoverProvider::SearchFinished, this, &AlbumCoverFetcherSearch::ProviderSearchFinished);
const int id = cover_providers->NextId();
const bool success = provider->StartSearch(request_.artist, request_.album, request_.title, id);
@ -127,7 +129,7 @@ void AlbumCoverFetcherSearch::Start(CoverProviders *cover_providers) {
}
void AlbumCoverFetcherSearch::ProviderSearchResults(const int id, const CoverSearchResults &results) {
void AlbumCoverFetcherSearch::ProviderSearchResults(const int id, const CoverProviderSearchResults &results) {
if (!pending_requests_.contains(id)) return;
CoverProvider *provider = pending_requests_[id];
@ -135,9 +137,9 @@ void AlbumCoverFetcherSearch::ProviderSearchResults(const int id, const CoverSea
}
void AlbumCoverFetcherSearch::ProviderSearchResults(CoverProvider *provider, const CoverSearchResults &results) {
void AlbumCoverFetcherSearch::ProviderSearchResults(CoverProvider *provider, const CoverProviderSearchResults &results) {
CoverSearchResults results_copy(results);
CoverProviderSearchResults results_copy(results);
for (int i = 0 ; i < results_copy.count() ; ++i) {
results_copy[i].provider = provider->name();
@ -225,7 +227,7 @@ void AlbumCoverFetcherSearch::ProviderSearchResults(CoverProvider *provider, con
}
void AlbumCoverFetcherSearch::ProviderSearchFinished(const int id, const CoverSearchResults &results) {
void AlbumCoverFetcherSearch::ProviderSearchFinished(const int id, const CoverProviderSearchResults &results) {
if (!pending_requests_.contains(id)) return;
@ -256,7 +258,7 @@ void AlbumCoverFetcherSearch::AllProvidersFinished() {
// No results?
if (results_.isEmpty()) {
statistics_.missing_images_++;
emit AlbumCoverFetched(request_.id, QUrl(), QImage());
emit AlbumCoverFetched(request_.id, AlbumCoverImageResult());
return;
}
@ -264,7 +266,7 @@ void AlbumCoverFetcherSearch::AllProvidersFinished() {
// We'll sort the list of results by current score, then load the first 3 images from each category and use some heuristics for additional score.
// If no images are good enough we'll keep loading more images until we find one that is or we run out of results.
std::stable_sort(results_.begin(), results_.end(), CoverSearchResultCompareScore);
std::stable_sort(results_.begin(), results_.end(), CoverProviderSearchResultCompareScore);
FetchMoreImages();
@ -275,7 +277,7 @@ void AlbumCoverFetcherSearch::FetchMoreImages() {
int i = 0;
while (!results_.isEmpty()) {
++i;
CoverSearchResult result = results_.takeFirst();
CoverProviderSearchResult result = results_.takeFirst();
qLog(Debug) << "Loading" << result.artist << result.album << result.image_url << "from" << result.provider << "with current score" << result.score();
@ -309,13 +311,11 @@ void AlbumCoverFetcherSearch::ProviderCoverFetchFinished(QNetworkReply *reply) {
reply->deleteLater();
if (!pending_image_loads_.contains(reply)) return;
CoverSearchResult result = pending_image_loads_.take(reply);
CoverProviderSearchResult result = pending_image_loads_.take(reply);
statistics_.bytes_transferred_ += reply->bytesAvailable();
if (cancel_requested_) {
return;
}
if (cancel_requested_) return;
if (reply->error() != QNetworkReply::NoError) {
qLog(Error) << "Error requesting" << reply->url() << reply->errorString();
@ -325,15 +325,17 @@ void AlbumCoverFetcherSearch::ProviderCoverFetchFinished(QNetworkReply *reply) {
}
else {
QString mimetype = reply->header(QNetworkRequest::ContentTypeHeader).toString();
if (Utilities::SupportedImageMimeTypes().contains(mimetype, Qt::CaseInsensitive) || Utilities::SupportedImageFormats().contains(mimetype, Qt::CaseInsensitive)) {
if (ImageUtils::SupportedImageMimeTypes().contains(mimetype, Qt::CaseInsensitive) || ImageUtils::SupportedImageFormats().contains(mimetype, Qt::CaseInsensitive)) {
QByteArray image_data = reply->readAll();
QString mime_type = Utilities::MimeTypeFromData(image_data);
QImage image;
if (image.loadFromData(reply->readAll())) {
if (result.image_size != QSize(0,0) && result.image_size != image.size()) {
if (image.loadFromData(image_data)) {
if (result.image_size != QSize(0, 0) && result.image_size != image.size()) {
qLog(Debug) << "API size for image" << result.image_size << "for" << reply->url() << "from" << result.provider << "did not match retrieved size" << image.size();
}
result.image_size = image.size();
result.score_quality = ScoreImage(image.size());
candidate_images_.insert(result.score(), CandidateImage(result, image));
candidate_images_.insert(result.score(), CandidateImage(result, AlbumCoverImageResult(result.image_url, mime_type, image_data, image)));
qLog(Debug) << reply->url() << "from" << result.provider << "scored" << result.score();
}
else {
@ -380,26 +382,24 @@ float AlbumCoverFetcherSearch::ScoreImage(const QSize size) const {
void AlbumCoverFetcherSearch::SendBestImage() {
QUrl cover_url;
QImage image;
AlbumCoverImageResult result;
if (!candidate_images_.isEmpty()) {
const CandidateImage best_image = candidate_images_.values().back();
cover_url = best_image.first.image_url;
image = best_image.second;
result = best_image.album_cover;
qLog(Info) << "Using" << best_image.first.image_url << "from" << best_image.first.provider << "with score" << best_image.first.score();
qLog(Info) << "Using" << best_image.result.image_url << "from" << best_image.result.provider << "with score" << best_image.result.score();
statistics_.chosen_images_by_provider_[best_image.first.provider]++;
statistics_.chosen_images_by_provider_[best_image.result.provider]++;
statistics_.chosen_images_++;
statistics_.chosen_width_ += image.width();
statistics_.chosen_height_ += image.height();
statistics_.chosen_width_ += result.image.width();
statistics_.chosen_height_ += result.image.height();
}
else {
statistics_.missing_images_++;
}
emit AlbumCoverFetched(request_.id, cover_url, image);
emit AlbumCoverFetched(request_.id, result);
}
@ -425,10 +425,10 @@ bool AlbumCoverFetcherSearch::ProviderCompareOrder(CoverProvider *a, CoverProvid
return a->order() < b->order();
}
bool AlbumCoverFetcherSearch::CoverSearchResultCompareScore(const CoverSearchResult &a, const CoverSearchResult &b) {
bool AlbumCoverFetcherSearch::CoverProviderSearchResultCompareScore(const CoverProviderSearchResult &a, const CoverProviderSearchResult &b) {
return a.score() > b.score();
}
bool AlbumCoverFetcherSearch::CoverSearchResultCompareNumber(const CoverSearchResult &a, const CoverSearchResult &b) {
bool AlbumCoverFetcherSearch::CoverProviderSearchResultCompareNumber(const CoverProviderSearchResult &a, const CoverProviderSearchResult &b) {
return a.number < b.number;
}

View File

@ -29,12 +29,14 @@
#include <QPair>
#include <QMap>
#include <QMultiMap>
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QImage>
#include "albumcoverfetcher.h"
#include "coversearchstatistics.h"
#include "albumcoverimageresult.h"
class QNetworkReply;
class CoverProvider;
@ -59,23 +61,23 @@ class AlbumCoverFetcherSearch : public QObject {
CoverSearchStatistics statistics() const { return statistics_; }
static bool CoverSearchResultCompareNumber(const CoverSearchResult &a, const CoverSearchResult &b);
static bool CoverProviderSearchResultCompareNumber(const CoverProviderSearchResult &a, const CoverProviderSearchResult &b);
signals:
// It's the end of search (when there was no fetch-me-a-cover request).
void SearchFinished(quint64, CoverSearchResults results);
void SearchFinished(quint64, CoverProviderSearchResults results);
// It's the end of search and we've fetched a cover.
void AlbumCoverFetched(const quint64, const QUrl &cover_url, const QImage &cover);
void AlbumCoverFetched(const quint64, AlbumCoverImageResult result);
private slots:
void ProviderSearchResults(const int id, const CoverSearchResults &results);
void ProviderSearchFinished(const int id, const CoverSearchResults &results);
void ProviderSearchResults(const int id, const CoverProviderSearchResults &results);
void ProviderSearchFinished(const int id, const CoverProviderSearchResults &results);
void ProviderCoverFetchFinished(QNetworkReply *reply);
void TerminateSearch();
private:
void ProviderSearchResults(CoverProvider *provider, const CoverSearchResults &results);
void ProviderSearchResults(CoverProvider *provider, const CoverProviderSearchResults &results);
void AllProvidersFinished();
void FetchMoreImages();
@ -83,7 +85,7 @@ class AlbumCoverFetcherSearch : public QObject {
void SendBestImage();
static bool ProviderCompareOrder(CoverProvider *a, CoverProvider *b);
static bool CoverSearchResultCompareScore(const CoverSearchResult &a, const CoverSearchResult &b);
static bool CoverProviderSearchResultCompareScore(const CoverProviderSearchResult &a, const CoverProviderSearchResult &b);
private:
static const int kSearchTimeoutMs;
@ -97,14 +99,18 @@ class AlbumCoverFetcherSearch : public QObject {
CoverSearchRequest request_;
// Complete results (from all of the available providers).
CoverSearchResults results_;
CoverProviderSearchResults results_;
QMap<int, CoverProvider*> pending_requests_;
QMap<QNetworkReply*, CoverSearchResult> pending_image_loads_;
QMap<QNetworkReply*, CoverProviderSearchResult> pending_image_loads_;
NetworkTimeouts* image_load_timeout_;
// QMap is sorted by key (score). Values are (result, image)
typedef QPair<CoverSearchResult, QImage> CandidateImage;
// QMap is sorted by key (score).
struct CandidateImage {
CandidateImage(const CoverProviderSearchResult &_result, const AlbumCoverImageResult &_album_cover) : result(_result), album_cover(_album_cover) {}
CoverProviderSearchResult result;
AlbumCoverImageResult album_cover;
};
QMultiMap<float, CandidateImage> candidate_images_;
NetworkAccessManager *network_;

View File

@ -0,0 +1,53 @@
/*
* Strawberry Music Player
* Copyright 2021, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef ALBUMCOVERIMAGERESULT_H
#define ALBUMCOVERIMAGERESULT_H
#include "config.h"
#include <QMetaType>
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QImage>
struct AlbumCoverImageResult {
explicit AlbumCoverImageResult(const QUrl &_cover_url = QUrl(),
const QString &_mime_type = QString(),
const QByteArray &_image_data = QByteArray(),
const QImage &_image = QImage()) :
cover_url(_cover_url),
mime_type(_mime_type),
image_data(_image_data), image(_image) {}
explicit AlbumCoverImageResult(const QImage &_image) : image(_image) {}
QUrl cover_url;
QString mime_type;
QByteArray image_data;
QImage image;
bool is_valid() const { return !image_data.isNull() || !image.isNull(); }
bool is_jpeg() const { return mime_type == "image/jpeg" && !image_data.isEmpty(); }
};
Q_DECLARE_METATYPE(AlbumCoverImageResult)
#endif // ALBUMCOVERIMAGERESULT_H

View File

@ -27,10 +27,12 @@
#include <QDir>
#include <QThread>
#include <QMutex>
#include <QBuffer>
#include <QSet>
#include <QList>
#include <QQueue>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QRegularExpression>
#include <QUrl>
@ -40,25 +42,29 @@
#include <QSize>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QMimeDatabase>
#include <QSettings>
#include "core/networkaccessmanager.h"
#include "core/song.h"
#include "core/tagreaderclient.h"
#include "core/utilities.h"
#include "core/imageutils.h"
#include "settings/collectionsettingspage.h"
#include "organize/organizeformat.h"
#include "albumcoverloader.h"
#include "albumcoverloaderoptions.h"
#include "albumcoverloaderresult.h"
#include "albumcoverimageresult.h"
AlbumCoverLoader::AlbumCoverLoader(QObject *parent)
: QObject(parent),
stop_requested_(false),
next_id_(1),
load_image_async_id_(1),
save_image_async_id_(1),
network_(new NetworkAccessManager(this)),
cover_album_dir_(false),
cover_filename_(CollectionSettingsPage::SaveCover_Hash),
save_cover_type_(CollectionSettingsPage::SaveCoverType_Cache),
save_cover_filename_(CollectionSettingsPage::SaveCoverFilename_Hash),
cover_overwrite_(false),
cover_lowercase_(true),
cover_replace_spaces_(true),
@ -89,8 +95,8 @@ void AlbumCoverLoader::ReloadSettings() {
QSettings s;
s.beginGroup(CollectionSettingsPage::kSettingsGroup);
cover_album_dir_ = s.value("cover_album_dir", false).toBool();
cover_filename_ = CollectionSettingsPage::SaveCover(s.value("cover_filename", CollectionSettingsPage::SaveCover_Hash).toInt());
save_cover_type_ = CollectionSettingsPage::SaveCoverType(s.value("save_cover_type", CollectionSettingsPage::SaveCoverType_Cache).toInt());
save_cover_filename_ = CollectionSettingsPage::SaveCoverFilename(s.value("save_cover_filename", CollectionSettingsPage::SaveCoverFilename_Hash).toInt());
cover_pattern_ = s.value("cover_pattern", "%albumartist-%album").toString();
cover_overwrite_ = s.value("cover_overwrite", false).toBool();
cover_lowercase_ = s.value("cover_lowercase", false).toBool();
@ -106,10 +112,10 @@ QString AlbumCoverLoader::AlbumCoverFilename(QString artist, QString album, cons
QString filename = artist + "-" + album;
filename = Utilities::UnicodeToAscii(filename.toLower());
filename = filename.replace(' ', '-');
filename = filename.replace("--", "-");
filename = filename.remove(OrganizeFormat::kInvalidFatCharacters);
filename = filename.simplified();
filename = filename.replace(' ', '-')
.replace("--", "-")
.remove(OrganizeFormat::kInvalidFatCharacters)
.simplified();
if (!extension.isEmpty()) {
filename.append('.');
@ -129,7 +135,7 @@ QString AlbumCoverLoader::CoverFilePath(const Song::Source source, const QString
album.remove(Song::kAlbumRemoveDisc);
QString path;
if (source == Song::Source_Collection && cover_album_dir_ && !album_dir.isEmpty()) {
if (source == Song::Source_Collection && save_cover_type_ == CollectionSettingsPage::SaveCoverType_Album && !album_dir.isEmpty()) {
path = album_dir;
}
else {
@ -147,7 +153,10 @@ QString AlbumCoverLoader::CoverFilePath(const Song::Source source, const QString
}
QString filename;
if (source == Song::Source_Collection && cover_album_dir_ && cover_filename_ == CollectionSettingsPage::SaveCover_Pattern && !cover_pattern_.isEmpty()) {
if (source == Song::Source_Collection &&
save_cover_type_ == CollectionSettingsPage::SaveCoverType_Album &&
save_cover_filename_ == CollectionSettingsPage::SaveCoverFilename_Pattern &&
!cover_pattern_.isEmpty()) {
filename = CoverFilenameFromVariable(artist, album);
filename.remove(OrganizeFormat::kInvalidFatCharacters);
if (cover_lowercase_) filename = filename.toLower();
@ -157,7 +166,8 @@ QString AlbumCoverLoader::CoverFilePath(const Song::Source source, const QString
filename.append(extension);
}
}
else {
if (filename.isEmpty()) {
filename = CoverFilenameFromSource(source, cover_url, artist, album, album_id, extension);
}
@ -220,7 +230,7 @@ QString AlbumCoverLoader::CoverFilenameFromVariable(const QString &artist, const
void AlbumCoverLoader::CancelTask(const quint64 id) {
QMutexLocker l(&mutex_);
QMutexLocker l(&mutex_load_image_async_);
for (QQueue<Task>::iterator it = tasks_.begin(); it != tasks_.end(); ++it) {
if (it->id == id) {
tasks_.erase(it);
@ -232,7 +242,7 @@ void AlbumCoverLoader::CancelTask(const quint64 id) {
void AlbumCoverLoader::CancelTasks(const QSet<quint64> &ids) {
QMutexLocker l(&mutex_);
QMutexLocker l(&mutex_load_image_async_);
for (QQueue<Task>::iterator it = tasks_.begin(); it != tasks_.end();) {
if (ids.contains(it->id)) {
it = tasks_.erase(it);
@ -244,26 +254,58 @@ void AlbumCoverLoader::CancelTasks(const QSet<quint64> &ids) {
}
quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions& options, const Song &song) {
return LoadImageAsync(options, song.art_automatic(), song.art_manual(), song.url(), song, song.image());
}
quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, const QUrl &art_automatic, const QUrl &art_manual, const QUrl &song_url, const Song song, const QImage &embedded_image) {
quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, const Song &song) {
Task task;
task.options = options;
task.song = song;
task.song_url = song_url;
task.art_manual = art_manual;
task.art_automatic = art_automatic;
task.art_updated = false;
task.embedded_image = embedded_image;
task.type = AlbumCoverLoaderResult::Type_None;
task.state = State_Manual;
return EnqueueTask(task);
}
quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, const QUrl &art_automatic, const QUrl &art_manual, const QUrl &song_url, const Song::Source song_source) {
Song song(song_source);
song.set_url(song_url);
song.set_art_automatic(art_automatic);
song.set_art_manual(art_manual);
Task task;
task.options = options;
task.song = song;
task.state = State_Manual;
return EnqueueTask(task);
}
quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, const AlbumCoverImageResult &album_cover) {
Task task;
task.options = options;
task.album_cover = album_cover;
return EnqueueTask(task);
}
quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, const QImage &image) {
Task task;
task.options = options;
task.album_cover.image = image;
return EnqueueTask(task);
}
quint64 AlbumCoverLoader::EnqueueTask(Task &task) {
{
QMutexLocker l(&mutex_);
task.id = next_id_++;
QMutexLocker l(&mutex_load_image_async_);
task.id = load_image_async_id_++;
tasks_.enqueue(task);
}
@ -279,7 +321,7 @@ void AlbumCoverLoader::ProcessTasks() {
// Get the next task
Task task;
{
QMutexLocker l(&mutex_);
QMutexLocker l(&mutex_load_image_async_);
if (tasks_.isEmpty()) return;
task = tasks_.dequeue();
}
@ -298,8 +340,16 @@ void AlbumCoverLoader::ProcessTask(Task *task) {
}
if (result.loaded_success) {
QPair<QImage, QImage> images = ScaleAndPad(task->options, result.image);
emit AlbumCoverLoaded(task->id, AlbumCoverLoaderResult(result.type, result.cover_url, result.image, images.first, images.second, task->art_updated));
result.album_cover.mime_type = Utilities::MimeTypeFromData(result.album_cover.image_data);
QImage image_scaled;
QImage image_thumbnail;
if (task->options.get_image_ && task->options.scale_output_image_) {
image_scaled = ImageUtils::ScaleAndPad(result.album_cover.image, task->options.scale_output_image_, task->options.pad_output_image_, task->options.desired_height_);
}
if (task->options.get_image_ && task->options.create_thumbnail_) {
image_thumbnail = ImageUtils::CreateThumbnail(result.album_cover.image, task->options.pad_thumbnail_image_, task->options.thumbnail_size_);
}
emit AlbumCoverLoaded(task->id, AlbumCoverLoaderResult(result.loaded_success, result.type, result.album_cover, image_scaled, image_thumbnail, task->art_updated));
return;
}
@ -316,30 +366,33 @@ void AlbumCoverLoader::NextState(Task *task) {
}
else {
// Give up
emit AlbumCoverLoaded(task->id, AlbumCoverLoaderResult(AlbumCoverLoaderResult::Type_None, QUrl(), task->options.default_output_image_, task->options.default_output_image_, task->options.default_thumbnail_image_, task->art_updated));
emit AlbumCoverLoaded(task->id, AlbumCoverLoaderResult(false, AlbumCoverLoaderResult::Type_None, AlbumCoverImageResult(task->options.default_output_image_), task->options.default_scaled_image_, task->options.default_thumbnail_image_, task->art_updated));
}
}
AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(Task *task) {
// An image embedded in the song itself takes priority
if (!task->embedded_image.isNull()) {
QPair<QImage, QImage> images = ScaleAndPad(task->options, task->embedded_image);
return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_Embedded, QUrl(), images.first);
// Only scale and pad.
if (task->album_cover.is_valid()) {
return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_Embedded, task->album_cover);
}
// Use cached album cover if possible.
if (task->state == State_Manual &&
!task->song.art_manual_is_valid() &&
task->art_manual.isEmpty() &&
task->song.source() != Song::Source_Collection &&
!task->options.scale_output_image_ &&
!task->options.pad_output_image_) {
task->song.InitArtManual();
if (task->song.art_manual_is_valid() && task->art_manual != task->song.art_manual()) {
task->art_manual = task->song.art_manual();
task->art_updated = true;
// For local files and streams initialize art if found.
if ((task->song.source() == Song::Source_LocalFile || task->song.source() == Song::Source_Stream) && !task->song.art_manual_is_valid() && !task->song.art_automatic_is_valid()) {
switch (task->state) {
case State_None:
break;
case State_Manual:
task->song.InitArtManual();
if (task->song.art_manual_is_valid()) task->art_updated = true;
break;
case State_Automatic:
if (task->song.url().isLocalFile()) {
task->song.InitArtAutomatic();
if (task->song.art_automatic_is_valid()) task->art_updated = true;
}
break;
}
}
@ -349,32 +402,64 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(Task *task) {
case State_None:
case State_Automatic:
type = AlbumCoverLoaderResult::Type_Automatic;
cover_url = task->art_automatic;
cover_url = task->song.art_automatic();
break;
case State_Manual:
type = AlbumCoverLoaderResult::Type_Manual;
cover_url = task->art_manual;
cover_url = task->song.art_manual();
break;
}
task->type = type;
if (!cover_url.isEmpty() && !cover_url.path().isEmpty()) {
if (cover_url.path() == Song::kManuallyUnsetCover) {
return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_ManuallyUnset, cover_url, task->options.default_output_image_);
return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_ManuallyUnset, AlbumCoverImageResult(cover_url, QString(), QByteArray(), task->options.default_output_image_));
}
else if (cover_url.path() == Song::kEmbeddedCover && task->song_url.isLocalFile()) {
const QImage taglib_image = TagReaderClient::Instance()->LoadEmbeddedArtBlocking(task->song_url.toLocalFile());
if (!taglib_image.isNull()) {
return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_Embedded, cover_url, ScaleAndPad(task->options, taglib_image).first);
else if (cover_url.path() == Song::kEmbeddedCover && task->song.url().isLocalFile()) {
QByteArray image_data = TagReaderClient::Instance()->LoadEmbeddedArtBlocking(task->song.url().toLocalFile());
if (!image_data.isEmpty()) {
QImage image;
if (task->options.get_image_ && image.loadFromData(image_data)) {
return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_Embedded, AlbumCoverImageResult(cover_url, QString(), image_data, image));
}
else {
return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_Embedded, AlbumCoverImageResult(cover_url, QString(), image_data, image));
}
}
}
else if (cover_url.isLocalFile()) {
QImage image(cover_url.toLocalFile());
return TryLoadResult(false, !image.isNull(), type, cover_url, image.isNull() ? task->options.default_output_image_ : image);
if (cover_url.isLocalFile()) {
QFile file(cover_url.toLocalFile());
if (file.exists()) {
if (file.open(QIODevice::ReadOnly)) {
QByteArray image_data = file.readAll();
file.close();
QImage image;
if (!image_data.isEmpty() && task->options.get_image_ && image.loadFromData(image_data)) {
return TryLoadResult(false, !image.isNull(), type, AlbumCoverImageResult(cover_url, QString(), image_data, image.isNull() ? task->options.default_output_image_ : image));
}
else {
return TryLoadResult(false, !image.isNull(), type, AlbumCoverImageResult(cover_url, QString(), image_data, image.isNull() ? task->options.default_output_image_ : image));
}
}
else {
qLog(Error) << "Failed to open album cover file" << cover_url;
}
}
}
else if (cover_url.scheme().isEmpty()) { // Assume a local file with no scheme.
QImage image(cover_url.path());
return TryLoadResult(false, !image.isNull(), type, cover_url, image.isNull() ? task->options.default_output_image_ : image);
QFile file(cover_url.path());
if (file.exists() && file.open(QIODevice::ReadOnly)) {
QByteArray image_data = file.readAll();
file.close();
QImage image;
if (!image_data.isEmpty() && task->options.get_image_ && image.loadFromData(image_data)) {
return TryLoadResult(false, !image.isNull(), type, AlbumCoverImageResult(cover_url, QString(), image_data, image.isNull() ? task->options.default_output_image_ : image));
}
else {
return TryLoadResult(false, !image.isNull(), type, AlbumCoverImageResult(cover_url, QString(), image_data, image.isNull() ? task->options.default_output_image_ : image));
}
}
}
else if (network_->supportedSchemes().contains(cover_url.scheme())) { // Remote URL
qLog(Debug) << "Loading remote cover from" << cover_url;
@ -388,11 +473,11 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(Task *task) {
QObject::connect(reply, &QNetworkReply::finished, [this, reply, cover_url]() { RemoteFetchFinished(reply, cover_url); });
remote_tasks_.insert(reply, *task);
return TryLoadResult(true, false, type, cover_url, QImage());
return TryLoadResult(true, false, type, AlbumCoverImageResult(cover_url));
}
}
return TryLoadResult(false, false, AlbumCoverLoaderResult::Type_None, cover_url, task->options.default_output_image_);
return TryLoadResult(false, false, AlbumCoverLoaderResult::Type_None, AlbumCoverImageResult(cover_url, QString(), QByteArray(), task->options.default_output_image_));
}
@ -425,14 +510,24 @@ void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply *reply, const QUrl &cov
if (reply->error() == QNetworkReply::NoError) {
// Try to load the image
QByteArray image_data = reply->readAll();
QString mime_type = Utilities::MimeTypeFromData(image_data);
QImage image;
if (image.load(reply, nullptr)) {
QPair<QImage, QImage> images = ScaleAndPad(task.options, image);
emit AlbumCoverLoaded(task.id, AlbumCoverLoaderResult(task.type, cover_url, image, images.first, images.second, task.art_updated));
return;
if (task.options.get_image_data_) {
if (image.loadFromData(image_data)) {
QImage image_scaled;
QImage image_thumbnail;
if (task.options.scale_output_image_) image_scaled = ImageUtils::ScaleAndPad(image, task.options.scale_output_image_, task.options.pad_output_image_, task.options.desired_height_);
if (task.options.create_thumbnail_) image_thumbnail = ImageUtils::CreateThumbnail(image, task.options.pad_thumbnail_image_, task.options.thumbnail_size_);
emit AlbumCoverLoaded(task.id, AlbumCoverLoaderResult(true, task.type, AlbumCoverImageResult(cover_url, mime_type, (task.options.get_image_data_ ? image_data : QByteArray()), image), image_scaled, image_thumbnail, task.art_updated));
return;
}
else {
qLog(Error) << "Unable to load album cover image" << cover_url;
}
}
else {
qLog(Error) << "Unable to load album cover image" << cover_url;
emit AlbumCoverLoaded(task.id, AlbumCoverLoaderResult(true, task.type, AlbumCoverImageResult(cover_url, mime_type, image_data, QImage()), QImage(), QImage(), task.art_updated));
}
}
else {
@ -443,79 +538,158 @@ void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply *reply, const QUrl &cov
}
QPair<QImage, QImage> AlbumCoverLoader::ScaleAndPad(const AlbumCoverLoaderOptions &options, const QImage &image) {
qint64 AlbumCoverLoader::SaveEmbeddedCoverAsync(const QString song_filename, const QString &cover_filename) {
if (image.isNull()) return qMakePair(image, image);
QMutexLocker l(&mutex_save_image_async_);
qint64 id = ++save_image_async_id_;
metaObject()->invokeMethod(this, "SaveEmbeddedCover", Qt::QueuedConnection, Q_ARG(qint64, id), Q_ARG(QString, song_filename), Q_ARG(QString, cover_filename));
return id;
// Scale the image down
QImage image_scaled;
if (options.scale_output_image_) {
image_scaled = image.scaled(QSize(options.desired_height_, options.desired_height_), Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
qint64 AlbumCoverLoader::SaveEmbeddedCoverAsync(const QString song_filename, const QImage &image) {
QMutexLocker l(&mutex_save_image_async_);
qint64 id = ++save_image_async_id_;
metaObject()->invokeMethod(this, "SaveEmbeddedCover", Qt::QueuedConnection, Q_ARG(qint64, id), Q_ARG(QString, song_filename), Q_ARG(QImage, image));
return id;
}
qint64 AlbumCoverLoader::SaveEmbeddedCoverAsync(const QString song_filename, const QByteArray &image_data) {
QMutexLocker l(&mutex_save_image_async_);
qint64 id = ++save_image_async_id_;
metaObject()->invokeMethod(this, "SaveEmbeddedCover", Qt::QueuedConnection, Q_ARG(qint64, id), Q_ARG(QString, song_filename), Q_ARG(QByteArray, image_data));
return id;
}
qint64 AlbumCoverLoader::SaveEmbeddedCoverAsync(const QList<QUrl> urls, const QString &cover_filename) {
QMutexLocker l(&mutex_save_image_async_);
qint64 id = ++save_image_async_id_;
metaObject()->invokeMethod(this, "SaveEmbeddedCover", Qt::QueuedConnection, Q_ARG(qint64, id), Q_ARG(QList<QUrl>, urls), Q_ARG(QString, cover_filename));
return id;
}
qint64 AlbumCoverLoader::SaveEmbeddedCoverAsync(const QList<QUrl> urls, const QImage &image) {
QMutexLocker l(&mutex_save_image_async_);
qint64 id = ++save_image_async_id_;
metaObject()->invokeMethod(this, "SaveEmbeddedCover", Qt::QueuedConnection, Q_ARG(qint64, id), Q_ARG(QList<QUrl>, urls), Q_ARG(QImage, image));
return id;
}
qint64 AlbumCoverLoader::SaveEmbeddedCoverAsync(const QList<QUrl> urls, const QByteArray &image_data) {
QMutexLocker l(&mutex_save_image_async_);
qint64 id = ++save_image_async_id_;
metaObject()->invokeMethod(this, "SaveEmbeddedCover", Qt::QueuedConnection, Q_ARG(qint64, id), Q_ARG(QList<QUrl>, urls), Q_ARG(QByteArray, image_data));
return id;
}
void AlbumCoverLoader::SaveEmbeddedCover(const qint64 id, const QString song_filename, const QByteArray &image_data) {
TagReaderReply *reply = TagReaderClient::Instance()->SaveEmbeddedArt(song_filename, image_data);
tagreader_save_embedded_art_requests_.insert(id, reply);
QObject::connect(reply, &TagReaderReply::Finished, this, [this, id, reply]() { SaveEmbeddedArtFinished(id, reply); }, Qt::QueuedConnection);
}
void AlbumCoverLoader::SaveEmbeddedCover(const qint64 id, const QString song_filename, const QImage &image) {
QByteArray image_data;
if (!image.isNull()) {
QBuffer buffer(&image_data);
if (buffer.open(QIODevice::WriteOnly)) {
image.save(&buffer, "JPEG");
buffer.close();
}
}
SaveEmbeddedCover(id, song_filename, image_data);
}
void AlbumCoverLoader::SaveEmbeddedCover(const qint64 id, const QString song_filename, const QString &cover_filename) {
QFile file(cover_filename);
if (file.size() >= 209715200 || !file.open(QIODevice::ReadOnly)) { // Max 200 MB.
emit SaveEmbeddedCoverAsyncFinished(id, false);
return;
}
QByteArray image_data = file.readAll();
file.close();
SaveEmbeddedCover(id, song_filename, image_data);
}
void AlbumCoverLoader::SaveEmbeddedCover(const qint64 id, const QList<QUrl> urls, const QImage &image) {
if (image.isNull()) {
for (const QUrl &url : urls) {
SaveEmbeddedCover(id, url.toLocalFile(), QByteArray());
}
return;
}
else {
image_scaled = image;
}
// Pad the image to height x height
if (options.pad_output_image_) {
QImage image_padded(options.desired_height_, options.desired_height_, QImage::Format_ARGB32);
image_padded.fill(0);
QPainter p(&image_padded);
p.drawImage((options.desired_height_ - image_scaled.width()) / 2, (options.desired_height_ - image_scaled.height()) / 2, image_scaled);
p.end();
image_scaled = image_padded;
}
// Create thumbnail
QImage image_thumbnail;
if (options.create_thumbnail_) {
if (options.pad_thumbnail_image_) {
image_thumbnail = image.scaled(options.thumbnail_size_, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QImage image_padded(options.thumbnail_size_, QImage::Format_ARGB32_Premultiplied);
image_padded.fill(0);
QPainter p(&image_padded);
p.drawImage((image_padded.width() - image_thumbnail.width()) / 2, (image_padded.height() - image_thumbnail.height()) / 2, image_thumbnail);
p.end();
image_thumbnail = image_padded;
}
else {
image_thumbnail = image.scaledToHeight(options.thumbnail_size_.height(), Qt::SmoothTransformation);
QByteArray image_data;
QBuffer buffer(&image_data);
if (buffer.open(QIODevice::WriteOnly)) {
if (image.save(&buffer, "JPEG")) {
SaveEmbeddedCover(id, urls, image_data);
buffer.close();
return;
}
buffer.close();
}
}
return qMakePair(image_scaled, image_thumbnail);
emit SaveEmbeddedCoverAsyncFinished(id, false);
}
QPixmap AlbumCoverLoader::TryLoadPixmap(const QUrl &art_automatic, const QUrl &art_manual, const QUrl &url) {
void AlbumCoverLoader::SaveEmbeddedCover(const qint64 id, const QList<QUrl> urls, const QString &cover_filename) {
QPixmap ret;
QFile file(cover_filename);
if (!art_manual.path().isEmpty()) {
if (art_manual.path() == Song::kManuallyUnsetCover) return ret;
else if (art_manual.isLocalFile()) {
ret.load(art_manual.toLocalFile());
}
else if (art_manual.scheme().isEmpty()) {
ret.load(art_manual.path());
}
}
if (ret.isNull() && !art_automatic.path().isEmpty()) {
if (art_automatic.path() == Song::kEmbeddedCover && !url.isEmpty() && url.isLocalFile()) {
ret = QPixmap::fromImage(TagReaderClient::Instance()->LoadEmbeddedArtBlocking(url.toLocalFile()));
}
else if (art_automatic.isLocalFile()) {
ret.load(art_automatic.toLocalFile());
}
else if (art_automatic.scheme().isEmpty()) {
ret.load(art_automatic.path());
}
if (file.size() >= 209715200 || !file.open(QIODevice::ReadOnly)) { // Max 200 MB.
emit SaveEmbeddedCoverAsyncFinished(id, false);
return;
}
return ret;
QByteArray image_data = file.readAll();
file.close();
SaveEmbeddedCover(id, urls, image_data);
}
void AlbumCoverLoader::SaveEmbeddedCover(const qint64 id, const QList<QUrl> urls, const QByteArray &image_data) {
for (const QUrl &url : urls) {
SaveEmbeddedCover(id, url.toLocalFile(), image_data);
}
}
void AlbumCoverLoader::SaveEmbeddedArtFinished(const qint64 id, TagReaderReply *reply) {
if (tagreader_save_embedded_art_requests_.contains(id)) {
tagreader_save_embedded_art_requests_.remove(id, reply);
}
if (!tagreader_save_embedded_art_requests_.contains(id)) {
emit SaveEmbeddedCoverAsyncFinished(id, reply->is_successful());
}
metaObject()->invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
}

View File

@ -30,15 +30,19 @@
#include <QPair>
#include <QSet>
#include <QMap>
#include <QMultiMap>
#include <QQueue>
#include <QByteArray>
#include <QString>
#include <QImage>
#include <QPixmap>
#include "core/song.h"
#include "core/tagreaderclient.h"
#include "settings/collectionsettingspage.h"
#include "albumcoverloaderoptions.h"
#include "albumcoverloaderresult.h"
#include "albumcoverimageresult.h"
class QThread;
class QNetworkReply;
@ -70,23 +74,39 @@ class AlbumCoverLoader : public QObject {
QString CoverFilePath(const Song::Source source, const QString &artist, QString album, const QString &album_id, const QString &album_dir, const QUrl &cover_url, const QString &extension = QString());
quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const Song &song);
virtual quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const QUrl &art_automatic, const QUrl &art_manual, const QUrl &song_url = QUrl(), const Song song = Song(), const QImage &embedded_image = QImage());
quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const QUrl &art_automatic, const QUrl &art_manual, const QUrl &song_url = QUrl(), const Song::Source song_source = Song::Source_Unknown);
quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const AlbumCoverImageResult &album_cover);
quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const QImage &image);
void CancelTask(const quint64 id);
void CancelTasks(const QSet<quint64> &ids);
static QPixmap TryLoadPixmap(const QUrl &automatic, const QUrl &manual, const QUrl &url = QUrl());
static QPair<QImage, QImage> ScaleAndPad(const AlbumCoverLoaderOptions &options, const QImage &image);
qint64 SaveEmbeddedCoverAsync(const QString song_filename, const QString &cover_filename);
qint64 SaveEmbeddedCoverAsync(const QString song_filename, const QImage &image);
qint64 SaveEmbeddedCoverAsync(const QString song_filename, const QByteArray &image_data);
qint64 SaveEmbeddedCoverAsync(const QList<QUrl> urls, const QString &cover_filename);
qint64 SaveEmbeddedCoverAsync(const QList<QUrl> urls, const QImage &image);
qint64 SaveEmbeddedCoverAsync(const QList<QUrl> urls, const QByteArray &image_data);
signals:
void ExitFinished();
void AlbumCoverLoaded(quint64 id, AlbumCoverLoaderResult result);
void SaveEmbeddedCoverAsyncFinished(quint64 id, bool success);
protected slots:
void Exit();
void ProcessTasks();
void RemoteFetchFinished(QNetworkReply *reply, const QUrl &cover_url);
void SaveEmbeddedCover(const qint64 id, const QString song_filename, const QString &cover_filename);
void SaveEmbeddedCover(const qint64 id, const QString song_filename, const QImage &image);
void SaveEmbeddedCover(const qint64 id, const QString song_filename, const QByteArray &image_data);
void SaveEmbeddedCover(const qint64 id, const QList<QUrl> urls, const QImage &image);
void SaveEmbeddedCover(const qint64 id, const QList<QUrl> urls, const QString &cover_filename);
void SaveEmbeddedCover(const qint64 id, const QList<QUrl> urls, const QByteArray &image_data);
void SaveEmbeddedArtFinished(const qint64 id, TagReaderReply *reply);
protected:
struct Task {
@ -95,11 +115,8 @@ class AlbumCoverLoader : public QObject {
AlbumCoverLoaderOptions options;
quint64 id;
QUrl art_manual;
QUrl art_automatic;
QUrl song_url;
Song song;
QImage embedded_image;
AlbumCoverImageResult album_cover;
State state;
AlbumCoverLoaderResult::Type type;
bool art_updated;
@ -107,33 +124,42 @@ class AlbumCoverLoader : public QObject {
};
struct TryLoadResult {
explicit TryLoadResult(const bool _started_async = false, const bool _loaded_success = false, const AlbumCoverLoaderResult::Type _type = AlbumCoverLoaderResult::Type_None, const QUrl &_cover_url = QUrl(), const QImage &_image = QImage()) : started_async(_started_async), loaded_success(_loaded_success), type(_type), cover_url(_cover_url), image(_image) {}
explicit TryLoadResult(const bool _started_async = false,
const bool _loaded_success = false,
const AlbumCoverLoaderResult::Type _type = AlbumCoverLoaderResult::Type_None,
const AlbumCoverImageResult &_album_cover = AlbumCoverImageResult()) :
started_async(_started_async),
loaded_success(_loaded_success),
type(_type),
album_cover(_album_cover) {}
bool started_async;
bool loaded_success;
AlbumCoverLoaderResult::Type type;
QUrl cover_url;
QImage image;
AlbumCoverImageResult album_cover;
};
quint64 EnqueueTask(Task &task);
void ProcessTask(Task *task);
void NextState(Task *task);
TryLoadResult TryLoadImage(Task *task);
bool stop_requested_;
QMutex mutex_;
QMutex mutex_load_image_async_;
QMutex mutex_save_image_async_;
QQueue<Task> tasks_;
QMap<QNetworkReply*, Task> remote_tasks_;
quint64 next_id_;
quint64 load_image_async_id_;
quint64 save_image_async_id_;
NetworkAccessManager *network_;
static const int kMaxRedirects = 3;
bool cover_album_dir_;
CollectionSettingsPage::SaveCover cover_filename_;
CollectionSettingsPage::SaveCoverType save_cover_type_;
CollectionSettingsPage::SaveCoverFilename save_cover_filename_;
QString cover_pattern_;
bool cover_overwrite_;
bool cover_lowercase_;
@ -141,6 +167,8 @@ class AlbumCoverLoader : public QObject {
QThread *original_thread_;
QMultiMap<qint64, TagReaderReply*> tagreader_save_embedded_art_requests_;
};
#endif // ALBUMCOVERLOADER_H

View File

@ -29,19 +29,25 @@
struct AlbumCoverLoaderOptions {
explicit AlbumCoverLoaderOptions()
: desired_height_(120),
: get_image_data_(true),
get_image_(true),
scale_output_image_(true),
pad_output_image_(true),
create_thumbnail_(false),
pad_thumbnail_image_(false) {}
pad_thumbnail_image_(false),
desired_height_(120),
thumbnail_size_(120, 120) {}
int desired_height_;
QSize thumbnail_size_;
bool get_image_data_;
bool get_image_;
bool scale_output_image_;
bool pad_output_image_;
bool create_thumbnail_;
bool pad_thumbnail_image_;
int desired_height_;
QSize thumbnail_size_;
QImage default_output_image_;
QImage default_scaled_image_;
QImage default_thumbnail_image_;
};

View File

@ -25,6 +25,8 @@
#include <QImage>
#include <QUrl>
#include "albumcoverimageresult.h"
struct AlbumCoverLoaderResult {
enum Type {
@ -36,11 +38,22 @@ struct AlbumCoverLoaderResult {
Type_Remote,
};
explicit AlbumCoverLoaderResult(const Type _type = Type_None, const QUrl &_cover_url = QUrl(), const QImage &_image_original = QImage(), const QImage &_image_scaled = QImage(), const QImage &_image_thumbnail = QImage(), const bool _updated = false) : type(_type), cover_url(_cover_url), image_original(_image_original), image_scaled(_image_scaled), image_thumbnail(_image_thumbnail), updated(_updated) {}
explicit AlbumCoverLoaderResult(const bool _success = false,
const Type _type = Type_None,
const AlbumCoverImageResult &_album_cover = AlbumCoverImageResult(),
const QImage &_image_scaled = QImage(),
const QImage &_image_thumbnail = QImage(),
const bool _updated = false) :
success(_success),
type(_type),
album_cover(_album_cover),
image_scaled(_image_scaled),
image_thumbnail(_image_thumbnail),
updated(_updated) {}
bool success;
Type type;
QUrl cover_url;
QImage image_original;
AlbumCoverImageResult album_cover;
QImage image_scaled;
QImage image_thumbnail;
bool updated;

View File

@ -28,6 +28,9 @@
#include <QObject>
#include <QMainWindow>
#include <QWidget>
#include <QtConcurrent>
#include <QFuture>
#include <QFutureWatcher>
#include <QScreen>
#include <QWindow>
#include <QItemSelectionModel>
@ -64,12 +67,15 @@
#include "core/application.h"
#include "core/iconloader.h"
#include "core/utilities.h"
#include "core/imageutils.h"
#include "core/tagreaderclient.h"
#include "widgets/forcescrollperpixel.h"
#include "widgets/qsearchfield.h"
#include "collection/sqlrow.h"
#include "collection/collectionbackend.h"
#include "collection/collectionquery.h"
#include "playlist/songmimedata.h"
#include "settings/collectionsettingspage.h"
#include "coverproviders.h"
#include "albumcovermanager.h"
#include "albumcoversearcher.h"
@ -83,6 +89,7 @@
#include "albumcovermanagerlist.h"
#include "coversearchstatistics.h"
#include "coversearchstatisticsdialog.h"
#include "albumcoverimageresult.h"
#include "ui_albumcovermanager.h"
@ -93,6 +100,7 @@ AlbumCoverManager::AlbumCoverManager(Application *app, CollectionBackend *collec
ui_(new Ui_CoverManager),
mainwindow_(mainwindow),
app_(app),
collection_backend_(collection_backend),
album_cover_choice_controller_(new AlbumCoverChoiceController(this)),
cover_fetcher_(new AlbumCoverFetcher(app_->cover_providers(), this)),
cover_searcher_(nullptr),
@ -100,14 +108,13 @@ AlbumCoverManager::AlbumCoverManager(Application *app, CollectionBackend *collec
cover_exporter_(new AlbumCoverExporter(this)),
artist_icon_(IconLoader::Load("folder-sound")),
all_artists_icon_(IconLoader::Load("library-music")),
no_cover_icon_(":/pictures/cdcase.png"),
no_cover_image_(GenerateNoCoverImage(no_cover_icon_)),
no_cover_item_icon_(QPixmap::fromImage(no_cover_image_)),
image_nocover_thumbnail_(ImageUtils::GenerateNoCoverImage(QSize(120, 120))),
icon_nocover_item_(QPixmap::fromImage(image_nocover_thumbnail_)),
context_menu_(new QMenu(this)),
progress_bar_(new QProgressBar(this)),
abort_progress_(new QPushButton(this)),
jobs_(0),
collection_backend_(collection_backend) {
all_artists_(nullptr) {
ui_->setupUi(this);
ui_->albums->set_cover_manager(this);
@ -122,7 +129,7 @@ AlbumCoverManager::AlbumCoverManager(Application *app, CollectionBackend *collec
album_cover_choice_controller_->Init(app_);
cover_searcher_ = new AlbumCoverSearcher(no_cover_item_icon_, app_, this);
cover_searcher_ = new AlbumCoverSearcher(icon_nocover_item_, app_, this);
cover_export_ = new AlbumCoverExport(this);
// Set up the status bar
@ -139,8 +146,15 @@ AlbumCoverManager::AlbumCoverManager(Application *app, CollectionBackend *collec
QShortcut *close = new QShortcut(QKeySequence::Close, this);
QObject::connect(close, &QShortcut::activated, this, &AlbumCoverManager::close);
cover_loader_options_.scale_output_image_ = true;
cover_loader_options_.pad_output_image_ = true;
cover_loader_options_.desired_height_ = 120;
cover_loader_options_.create_thumbnail_ = false;
EnableCoversButtons();
ReloadSettings();
}
AlbumCoverManager::~AlbumCoverManager() {
@ -151,11 +165,10 @@ AlbumCoverManager::~AlbumCoverManager() {
}
void AlbumCoverManager::ReloadSettings() {
app_->album_cover_loader()->ReloadSettings();
}
CollectionBackend *AlbumCoverManager::backend() const {
return collection_backend_;
app_->album_cover_loader()->ReloadSettings();
album_cover_choice_controller_->ReloadSettings();
}
void AlbumCoverManager::Init() {
@ -185,6 +198,8 @@ void AlbumCoverManager::Init() {
QObject::connect(album_cover_choice_controller_->cover_from_url_action(), &QAction::triggered, this, &AlbumCoverManager::LoadCoverFromURL);
QObject::connect(album_cover_choice_controller_->search_for_cover_action(), &QAction::triggered, this, &AlbumCoverManager::SearchForCover);
QObject::connect(album_cover_choice_controller_->unset_cover_action(), &QAction::triggered, this, &AlbumCoverManager::UnsetCover);
QObject::connect(album_cover_choice_controller_->clear_cover_action(), &QAction::triggered, this, &AlbumCoverManager::ClearCover);
QObject::connect(album_cover_choice_controller_->delete_cover_action(), &QAction::triggered, this, &AlbumCoverManager::DeleteCover);
QObject::connect(album_cover_choice_controller_->show_cover_action(), &QAction::triggered, this, &AlbumCoverManager::ShowCover);
QObject::connect(cover_exporter_, &AlbumCoverExporter::AlbumCoversExportUpdate, this, &AlbumCoverManager::UpdateExportStatus);
@ -213,13 +228,19 @@ void AlbumCoverManager::Init() {
QSettings s;
s.beginGroup(kSettingsGroup);
restoreGeometry(s.value("geometry").toByteArray());
if (!ui_->splitter->restoreState(s.value("splitter_state").toByteArray())) {
if (s.contains("geometry")) {
restoreGeometry(s.value("geometry").toByteArray());
}
if (!s.contains("splitter_state") || (s.contains("splitter_state") && !ui_->splitter->restoreState(s.value("splitter_state").toByteArray()))) {
// Sensible default size for the artists view
ui_->splitter->setSizes(QList<int>() << 200 << width() - 200);
}
s.endGroup();
QObject::connect(app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &AlbumCoverManager::AlbumCoverLoaded);
QObject::connect(app_->album_cover_loader(), &AlbumCoverLoader::SaveEmbeddedCoverAsyncFinished, this, &AlbumCoverManager::SaveEmbeddedCoverAsyncFinished);
cover_searcher_->Init(cover_fetcher_);
@ -227,10 +248,12 @@ void AlbumCoverManager::Init() {
}
void AlbumCoverManager::showEvent(QShowEvent *) {
void AlbumCoverManager::showEvent(QShowEvent *e) {
LoadGeometry();
Reset();
if (!e->spontaneous()) {
LoadGeometry();
Reset();
}
}
@ -246,7 +269,7 @@ void AlbumCoverManager::closeEvent(QCloseEvent *e) {
}
}
SaveGeometry();
SaveSettings();
// Cancel any outstanding requests
CancelRequests();
@ -287,12 +310,13 @@ void AlbumCoverManager::LoadGeometry() {
}
void AlbumCoverManager::SaveGeometry() {
void AlbumCoverManager::SaveSettings() {
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue("geometry", saveGeometry());
s.setValue("splitter_state", ui_->splitter->saveState());
s.setValue("save_cover_type", album_cover_choice_controller_->get_save_album_cover_type());
s.endGroup();
}
@ -305,6 +329,8 @@ void AlbumCoverManager::CancelRequests() {
app_->album_cover_loader()->CancelTasks(QSet<quint64>::fromList(cover_loading_tasks_.keys()));
#endif
cover_loading_tasks_.clear();
cover_save_tasks_.clear();
cover_save_tasks2_.clear();
cover_exporter_->Cancel();
@ -322,7 +348,7 @@ static bool CompareNocase(const QString &left, const QString &right) {
}
static bool CompareAlbumNameNocase(const CollectionBackend::Album &left, const CollectionBackend::Album &right) {
return CompareNocase(left.album_name, right.album_name);
return CompareNocase(left.album, right.album);
}
void AlbumCoverManager::Reset() {
@ -330,8 +356,8 @@ void AlbumCoverManager::Reset() {
EnableCoversButtons();
ui_->artists->clear();
new QListWidgetItem(all_artists_icon_, tr("All artists"), ui_->artists, All_Artists);
new QListWidgetItem(artist_icon_, tr("Various artists"), ui_->artists, Various_Artists);
all_artists_ = new QListWidgetItem(all_artists_icon_, tr("All artists"), ui_->artists, All_Artists);
new AlbumItem(artist_icon_, tr("Various artists"), ui_->artists, Various_Artists);
QStringList artists(collection_backend_->GetAllArtistsWithAlbums());
std::stable_sort(artists.begin(), artists.end(), CompareNocase);
@ -341,6 +367,10 @@ void AlbumCoverManager::Reset() {
new QListWidgetItem(artist_icon_, artist, ui_->artists, Specific_Artist);
}
if (ui_->artists->selectedItems().isEmpty()) {
ui_->artists->selectionModel()->setCurrentIndex(ui_->artists->indexFromItem(all_artists_), QItemSelectionModel::Clear);
}
}
void AlbumCoverManager::EnableCoversButtons() {
@ -357,9 +387,6 @@ void AlbumCoverManager::ArtistChanged(QListWidgetItem *current) {
if (!current) return;
QString artist;
if (current->type() == Specific_Artist) artist = current->text();
ui_->albums->clear();
context_menu_items_.clear();
CancelRequests();
@ -377,32 +404,42 @@ void AlbumCoverManager::ArtistChanged(QListWidgetItem *current) {
std::stable_sort(albums.begin(), albums.end(), CompareAlbumNameNocase);
for (const CollectionBackend::Album &info : albums) {
// Don't show songs without an album, obviously
if (info.album_name.isEmpty()) continue;
if (info.album.isEmpty()) continue;
QListWidgetItem *item = new QListWidgetItem(no_cover_item_icon_, info.album_name, ui_->albums);
item->setData(Role_ArtistName, info.artist);
item->setData(Role_AlbumArtistName, info.album_artist);
item->setData(Role_AlbumName, info.album_name);
item->setData(Role_FirstUrl, info.first_url);
item->setData(Qt::TextAlignmentRole, QVariant(Qt::AlignTop | Qt::AlignHCenter));
item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled);
item->setToolTip(info.artist + " - " + info.album_name);
QString display_text;
QString effective_artist = EffectiveAlbumArtistName(*item);
if (!artist.isEmpty()) {
item->setToolTip(effective_artist + " - " + info.album_name);
if (current->type() == Specific_Artist) {
display_text = info.album;
}
else {
item->setToolTip(info.album_name);
display_text = info.album_artist + " - " + info.album;
}
AlbumItem *item = new AlbumItem(icon_nocover_item_, display_text, ui_->albums);
item->setData(Role_AlbumArtist, info.album_artist);
item->setData(Role_Album, info.album);
item->setData(Role_Filetype, info.filetype);
item->setData(Role_CuePath, info.cue_path);
item->setData(Qt::TextAlignmentRole, QVariant(Qt::AlignTop | Qt::AlignHCenter));
item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled);
item->urls = info.urls;
if (info.album_artist.isEmpty()) {
item->setToolTip(info.album);
}
else {
item->setToolTip(info.album_artist + " - " + info.album);
}
if (!info.art_automatic.isEmpty() || !info.art_manual.isEmpty()) {
quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, info.art_automatic, info.art_manual, info.first_url);
item->setData(Role_PathAutomatic, info.art_automatic);
item->setData(Role_PathManual, info.art_manual);
quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, info.art_automatic, info.art_manual, info.urls.first());
cover_loading_tasks_[id] = item;
}
}
UpdateFilter();
@ -413,11 +450,18 @@ void AlbumCoverManager::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoade
if (!cover_loading_tasks_.contains(id)) return;
QListWidgetItem *item = cover_loading_tasks_.take(id);
AlbumItem *item = cover_loading_tasks_.take(id);
if (result.image_scaled.isNull()) return;
if (!result.success || result.image_scaled.isNull() || result.type == AlbumCoverLoaderResult::Type_ManuallyUnset) {
item->setIcon(icon_nocover_item_);
}
else {
item->setIcon(QPixmap::fromImage(result.image_scaled));
}
//item->setData(Role_Image, result.image_original);
//item->setData(Role_ImageData, result.image_data);
item->setIcon(QPixmap::fromImage(result.image_scaled));
UpdateFilter();
}
@ -440,14 +484,14 @@ void AlbumCoverManager::UpdateFilter() {
qint32 without_cover = 0;
for (int i = 0; i < ui_->albums->count(); ++i) {
QListWidgetItem *item = ui_->albums->item(i);
AlbumItem *item = static_cast<AlbumItem*>(ui_->albums->item(i));
bool should_hide = ShouldHide(*item, filter, hide);
item->setHidden(should_hide);
if (!should_hide) {
total_count++;
++total_count;
if (!ItemHasCover(*item)) {
without_cover++;
++without_cover;
}
}
}
@ -457,7 +501,7 @@ void AlbumCoverManager::UpdateFilter() {
}
bool AlbumCoverManager::ShouldHide(const QListWidgetItem &item, const QString &filter, HideCovers hide) const {
bool AlbumCoverManager::ShouldHide(const AlbumItem &item, const QString &filter, HideCovers hide) const {
bool has_cover = ItemHasCover(item);
if (hide == Hide_WithCovers && has_cover) {
@ -474,9 +518,8 @@ bool AlbumCoverManager::ShouldHide(const QListWidgetItem &item, const QString &f
QStringList query = filter.split(' ');
for (const QString &s : query) {
bool in_text = item.text().contains(s, Qt::CaseInsensitive);
bool in_artist = item.data(Role_ArtistName).toString().contains(s, Qt::CaseInsensitive);
bool in_albumartist = item.data(Role_AlbumArtistName).toString().contains(s, Qt::CaseInsensitive);
if (!in_text && !in_artist && !in_albumartist) {
bool in_albumartist = item.data(Role_AlbumArtist).toString().contains(s, Qt::CaseInsensitive);
if (!in_text && !in_albumartist) {
return true;
}
}
@ -488,11 +531,11 @@ bool AlbumCoverManager::ShouldHide(const QListWidgetItem &item, const QString &f
void AlbumCoverManager::FetchAlbumCovers() {
for (int i = 0; i < ui_->albums->count(); ++i) {
QListWidgetItem *item = ui_->albums->item(i);
AlbumItem *item = static_cast<AlbumItem*>(ui_->albums->item(i));
if (item->isHidden()) continue;
if (ItemHasCover(*item)) continue;
quint64 id = cover_fetcher_->FetchAlbumCover(EffectiveAlbumArtistName(*item), item->data(Role_AlbumName).toString(), QString(), true);
quint64 id = cover_fetcher_->FetchAlbumCover(item->data(Role_AlbumArtist).toString(), item->data(Role_Album).toString(), QString(), true);
cover_fetching_tasks_[id] = item;
jobs_++;
}
@ -507,14 +550,13 @@ void AlbumCoverManager::FetchAlbumCovers() {
}
void AlbumCoverManager::AlbumCoverFetched(const quint64 id, const QUrl &cover_url, const QImage &image, const CoverSearchStatistics &statistics) {
void AlbumCoverManager::AlbumCoverFetched(const quint64 id, const AlbumCoverImageResult &result, const CoverSearchStatistics &statistics) {
if (!cover_fetching_tasks_.contains(id))
return;
if (!cover_fetching_tasks_.contains(id)) return;
QListWidgetItem *item = cover_fetching_tasks_.take(id);
if (!image.isNull()) {
SaveAndSetCover(item, cover_url, image);
AlbumItem *item = cover_fetching_tasks_.take(id);
if (!result.image.isNull()) {
SaveAndSetCover(item, result);
}
if (cover_fetching_tasks_.isEmpty()) {
@ -554,29 +596,34 @@ void AlbumCoverManager::UpdateStatusText() {
}
bool AlbumCoverManager::eventFilter(QObject *obj, QEvent *event) {
bool AlbumCoverManager::eventFilter(QObject *obj, QEvent *e) {
if (obj == ui_->albums && event->type() == QEvent::ContextMenu) {
if (obj == ui_->albums && e->type() == QEvent::ContextMenu) {
context_menu_items_ = ui_->albums->selectedItems();
if (context_menu_items_.isEmpty()) return false;
bool some_with_covers = false;
for (QListWidgetItem *item : context_menu_items_) {
if (ItemHasCover(*item)) some_with_covers = true;
AlbumItem *album_item = static_cast<AlbumItem*>(item);
if (ItemHasCover(*album_item)) some_with_covers = true;
}
album_cover_choice_controller_->show_cover_action()->setEnabled(some_with_covers && context_menu_items_.size() == 1);
album_cover_choice_controller_->cover_to_file_action()->setEnabled(some_with_covers);
album_cover_choice_controller_->cover_from_file_action()->setEnabled(context_menu_items_.size() == 1);
album_cover_choice_controller_->cover_from_url_action()->setEnabled(context_menu_items_.size() == 1);
album_cover_choice_controller_->show_cover_action()->setEnabled(some_with_covers && context_menu_items_.size() == 1);
album_cover_choice_controller_->unset_cover_action()->setEnabled(some_with_covers);
album_cover_choice_controller_->search_for_cover_action()->setEnabled(app_->cover_providers()->HasAnyProviders());
album_cover_choice_controller_->unset_cover_action()->setEnabled(some_with_covers);
album_cover_choice_controller_->clear_cover_action()->setEnabled(some_with_covers);
album_cover_choice_controller_->delete_cover_action()->setEnabled(some_with_covers);
QContextMenuEvent *e = static_cast<QContextMenuEvent*>(event);
context_menu_->popup(e->globalPos());
QContextMenuEvent *context_menu_event = static_cast<QContextMenuEvent*>(e);
context_menu_->popup(context_menu_event->globalPos());
return true;
}
return QMainWindow::eventFilter(obj, event);
return QMainWindow::eventFilter(obj, e);
}
Song AlbumCoverManager::GetSingleSelectionAsSong() {
@ -587,12 +634,12 @@ Song AlbumCoverManager::GetFirstSelectedAsSong() {
return context_menu_items_.isEmpty() ? Song() : ItemAsSong(context_menu_items_[0]);
}
Song AlbumCoverManager::ItemAsSong(QListWidgetItem *item) {
Song AlbumCoverManager::ItemAsSong(AlbumItem *item) {
Song result(Song::Source_Collection);
QString title = item->data(Role_AlbumName).toString();
QString artist_name = EffectiveAlbumArtistName(*item);
QString title = item->data(Role_Album).toString();
QString artist_name = item->data(Role_AlbumArtist).toString();
if (!artist_name.isEmpty()) {
result.set_title(artist_name + " - " + title);
}
@ -600,11 +647,13 @@ Song AlbumCoverManager::ItemAsSong(QListWidgetItem *item) {
result.set_title(title);
}
result.set_artist(item->data(Role_ArtistName).toString());
result.set_albumartist(item->data(Role_AlbumArtistName).toString());
result.set_album(item->data(Role_AlbumName).toString());
result.set_artist(item->data(Role_AlbumArtist).toString());
result.set_albumartist(item->data(Role_AlbumArtist).toString());
result.set_album(item->data(Role_Album).toString());
result.set_url(item->data(Role_FirstUrl).toUrl());
result.set_filetype(Song::FileType(item->data(Role_Filetype).toInt()));
result.set_url(item->urls.first());
result.set_cue_path(item->data(Role_CuePath).toString());
result.set_art_automatic(item->data(Role_PathAutomatic).toUrl());
result.set_art_manual(item->data(Role_PathManual).toUrl());
@ -628,8 +677,9 @@ void AlbumCoverManager::ShowCover() {
void AlbumCoverManager::FetchSingleCover() {
for (QListWidgetItem *item : context_menu_items_) {
quint64 id = cover_fetcher_->FetchAlbumCover(EffectiveAlbumArtistName(*item), item->data(Role_AlbumName).toString(), QString(), false);
cover_fetching_tasks_[id] = item;
AlbumItem *album_item = static_cast<AlbumItem*>(item);
quint64 id = cover_fetcher_->FetchAlbumCover(album_item->data(Role_AlbumArtist).toString(), album_item->data(Role_Album).toString(), QString(), false);
cover_fetching_tasks_[id] = album_item;
jobs_++;
}
@ -640,7 +690,7 @@ void AlbumCoverManager::FetchSingleCover() {
}
void AlbumCoverManager::UpdateCoverInList(QListWidgetItem *item, const QUrl &cover_url) {
void AlbumCoverManager::UpdateCoverInList(AlbumItem *item, const QUrl &cover_url) {
quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, QUrl(), cover_url);
item->setData(Role_PathManual, cover_url);
@ -653,12 +703,9 @@ void AlbumCoverManager::LoadCoverFromFile() {
Song song = GetSingleSelectionAsSong();
if (!song.is_valid()) return;
QListWidgetItem *item = context_menu_items_[0];
QUrl cover_url = album_cover_choice_controller_->LoadCoverFromFile(&song);
if (!cover_url.isEmpty()) {
UpdateCoverInList(item, cover_url);
AlbumCoverImageResult result = album_cover_choice_controller_->LoadImageFromFile(&song);
if (!result.image.isNull()) {
SaveImageToAlbums(&song, result);
}
}
@ -666,33 +713,37 @@ void AlbumCoverManager::LoadCoverFromFile() {
void AlbumCoverManager::SaveCoverToFile() {
Song song = GetSingleSelectionAsSong();
if (!song.is_valid()) return;
if (!song.is_valid() || song.has_manually_unset_cover()) return;
QImage image;
AlbumCoverImageResult result;
// Load the image from disk
if (song.has_manually_unset_cover()) {
image = no_cover_image_;
if (!song.art_manual().isEmpty() && !song.has_manually_unset_cover() && song.art_manual().isLocalFile() && QFile::exists(song.art_manual().toLocalFile())) {
result.image_data = Utilities::ReadDataFromFile(song.art_manual().toLocalFile());
}
else {
if (!song.art_manual().isEmpty() && song.art_manual().isLocalFile() && QFile::exists(song.art_manual().toLocalFile())) {
image = QImage(song.art_manual().toLocalFile());
}
else if (!song.art_manual().isEmpty() && !song.art_manual().path().isEmpty() && song.art_manual().scheme().isEmpty() && QFile::exists(song.art_manual().path())) {
image = QImage(song.art_manual().path());
}
else if (!song.art_automatic().isEmpty() && song.art_automatic().isLocalFile() && QFile::exists(song.art_automatic().toLocalFile())) {
image = QImage(song.art_automatic().toLocalFile());
}
else if (!song.art_automatic().isEmpty() && !song.art_automatic().path().isEmpty() && song.art_automatic().scheme().isEmpty() && QFile::exists(song.art_automatic().path())) {
image = QImage(song.art_automatic().path());
}
else {
image = no_cover_image_;
}
else if (!song.art_manual().isEmpty() && !song.art_manual().path().isEmpty() && song.art_manual().scheme().isEmpty() && QFile::exists(song.art_manual().path())) {
result.image_data = Utilities::ReadDataFromFile(song.art_manual().path());
}
else if (song.has_embedded_cover()) {
result.image_data = TagReaderClient::Instance()->LoadEmbeddedArtBlocking(song.url().toLocalFile());
}
else if (!song.art_automatic().isEmpty() && song.art_automatic().isLocalFile() && QFile::exists(song.art_automatic().toLocalFile())) {
result.image_data = Utilities::ReadDataFromFile(song.art_automatic().toLocalFile());
}
else if (!song.art_automatic().isEmpty() && !song.art_automatic().path().isEmpty() && song.art_automatic().scheme().isEmpty() && QFile::exists(song.art_automatic().path())) {
result.image_data = Utilities::ReadDataFromFile(song.art_automatic().path());
}
album_cover_choice_controller_->SaveCoverToFileManual(song, image);
if (!result.is_valid()) return;
result.mime_type = Utilities::MimeTypeFromData(result.image_data);
if (!result.image_data.isEmpty()) {
result.image.loadFromData(result.image_data);
}
album_cover_choice_controller_->SaveCoverToFileManual(song, result);
}
@ -701,12 +752,9 @@ void AlbumCoverManager::LoadCoverFromURL() {
Song song = GetSingleSelectionAsSong();
if (!song.is_valid()) return;
QListWidgetItem *item = context_menu_items_[0];
QUrl cover_url = album_cover_choice_controller_->LoadCoverFromURL(&song);
if (!cover_url.isEmpty()) {
UpdateCoverInList(item, cover_url);
AlbumCoverImageResult result = album_cover_choice_controller_->LoadImageFromURL();
if (result.is_valid()) {
SaveImageToAlbums(&song, result);
}
}
@ -716,20 +764,58 @@ void AlbumCoverManager::SearchForCover() {
Song song = GetFirstSelectedAsSong();
if (!song.is_valid()) return;
QListWidgetItem *item = context_menu_items_[0];
AlbumCoverImageResult result = album_cover_choice_controller_->SearchForImage(&song);
if (result.is_valid()) {
SaveImageToAlbums(&song, result);
}
QUrl cover_url = album_cover_choice_controller_->SearchForCover(&song);
if (cover_url.isEmpty()) return;
}
void AlbumCoverManager::SaveImageToAlbums(Song *song, const AlbumCoverImageResult &result) {
QUrl cover_url;
switch (album_cover_choice_controller_->get_save_album_cover_type()) {
case CollectionSettingsPage::SaveCoverType_Cache:
case CollectionSettingsPage::SaveCoverType_Album:
cover_url = album_cover_choice_controller_->SaveCoverToFileAutomatic(song, result);
break;
case CollectionSettingsPage::SaveCoverType_Embedded:
cover_url = QUrl::fromLocalFile(Song::kEmbeddedCover);
break;
}
// Force the found cover on all of the selected items
for (QListWidgetItem *current : context_menu_items_) {
// Don't save the first one twice
if (current != item) {
Song current_song = ItemAsSong(current);
album_cover_choice_controller_->SaveCoverToSong(&current_song, cover_url);
QList<QUrl> urls;
QList<AlbumItem*> album_items;
for (QListWidgetItem *item : context_menu_items_) {
AlbumItem *album_item = static_cast<AlbumItem*>(item);
switch (album_cover_choice_controller_->get_save_album_cover_type()) {
case CollectionSettingsPage::SaveCoverType_Cache:
case CollectionSettingsPage::SaveCoverType_Album:{
Song current_song = ItemAsSong(album_item);
album_cover_choice_controller_->SaveArtManualToSong(&current_song, cover_url);
UpdateCoverInList(album_item, cover_url);
break;
}
case CollectionSettingsPage::SaveCoverType_Embedded:{
urls << album_item->urls;
album_items << album_item;
break;
}
}
}
UpdateCoverInList(current, cover_url);
if (album_cover_choice_controller_->get_save_album_cover_type() == CollectionSettingsPage::SaveCoverType_Embedded && !urls.isEmpty()) {
quint64 id = -1;
if (result.is_jpeg()) {
id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.image_data);
}
else {
id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.image);
}
for (AlbumItem *album_item : album_items) {
cover_save_tasks_.insert(id, album_item);
}
}
}
@ -739,19 +825,68 @@ void AlbumCoverManager::UnsetCover() {
Song song = GetFirstSelectedAsSong();
if (!song.is_valid()) return;
QListWidgetItem *item = context_menu_items_[0];
AlbumItem *first_album_item = static_cast<AlbumItem*>(context_menu_items_[0]);
QUrl cover_url = album_cover_choice_controller_->UnsetCover(&song);
// Force the 'none' cover on all of the selected items
for (QListWidgetItem *current : context_menu_items_) {
current->setIcon(no_cover_item_icon_);
current->setData(Role_PathManual, cover_url);
for (QListWidgetItem *item : context_menu_items_) {
AlbumItem *album_item = static_cast<AlbumItem*>(item);
album_item->setIcon(icon_nocover_item_);
album_item->setData(Role_PathManual, cover_url);
// Don't save the first one twice
if (current != item) {
Song current_song = ItemAsSong(current);
album_cover_choice_controller_->SaveCoverToSong(&current_song, cover_url);
if (album_item != first_album_item) {
Song current_song = ItemAsSong(album_item);
album_cover_choice_controller_->SaveArtManualToSong(&current_song, cover_url);
}
}
}
void AlbumCoverManager::ClearCover() {
Song song = GetFirstSelectedAsSong();
if (!song.is_valid()) return;
AlbumItem *first_album_item = static_cast<AlbumItem*>(context_menu_items_[0]);
album_cover_choice_controller_->ClearCover(&song);
// Force the 'none' cover on all of the selected items
for (QListWidgetItem *item : context_menu_items_) {
AlbumItem *album_item = static_cast<AlbumItem*>(item);
album_item->setIcon(icon_nocover_item_);
album_item->setData(Role_PathManual, QUrl());
// Don't save the first one twice
if (album_item != first_album_item) {
Song current_song = ItemAsSong(album_item);
album_cover_choice_controller_->SaveArtManualToSong(&current_song, QUrl(), false);
}
}
}
void AlbumCoverManager::DeleteCover() {
Song song = GetFirstSelectedAsSong();
if (!song.is_valid()) return;
AlbumItem *first_album_item = static_cast<AlbumItem*>(context_menu_items_[0]);
album_cover_choice_controller_->DeleteCover(&song);
// Force the 'none' cover on all of the selected items
for (QListWidgetItem *item : context_menu_items_) {
AlbumItem *album_item = static_cast<AlbumItem*>(item);
album_item->setIcon(icon_nocover_item_);
album_item->setData(Role_PathManual, QUrl());
// Don't save the first one twice
if (album_item != first_album_item) {
Song current_song = ItemAsSong(album_item);
album_cover_choice_controller_->SaveArtManualToSong(&current_song, QUrl(), true);
}
}
@ -763,20 +898,15 @@ SongList AlbumCoverManager::GetSongsInAlbum(const QModelIndex &idx) const {
CollectionQuery q;
q.SetColumnSpec("ROWID," + Song::kColumnSpec);
q.AddWhere("album", idx.data(Role_AlbumName).toString());
q.AddWhere("album", idx.data(Role_Album).toString());
q.SetOrderBy("disc, track, title");
QString artist = idx.data(Role_ArtistName).toString();
QString albumartist = idx.data(Role_AlbumArtistName).toString();
QString albumartist = idx.data(Role_AlbumArtist).toString();
if (!albumartist.isEmpty()) {
q.AddWhere("albumartist", albumartist);
}
else if (!artist.isEmpty()) {
q.AddWhere("artist", artist);
q.AddWhere("effective_albumartist", albumartist);
}
q.AddCompilationRequirement(artist.isEmpty() && albumartist.isEmpty());
q.AddCompilationRequirement(albumartist.isEmpty());
if (!collection_backend_->ExecQuery(&q)) return ret;
@ -813,7 +943,7 @@ SongMimeData *AlbumCoverManager::GetMimeDataForAlbums(const QModelIndexList &ind
void AlbumCoverManager::AlbumDoubleClicked(const QModelIndex &idx) {
QListWidgetItem *item = static_cast<QListWidgetItem*>(idx.internalPointer());
AlbumItem *item = static_cast<AlbumItem*>(idx.internalPointer());
if (!item) return;
album_cover_choice_controller_->ShowCover(ItemAsSong(item));
@ -833,23 +963,35 @@ void AlbumCoverManager::LoadSelectedToPlaylist() {
}
void AlbumCoverManager::SaveAndSetCover(QListWidgetItem *item, const QUrl &cover_url, const QImage &image) {
void AlbumCoverManager::SaveAndSetCover(AlbumItem *item, const AlbumCoverImageResult &result) {
const QString artist = item->data(Role_ArtistName).toString();
const QString albumartist = item->data(Role_AlbumArtistName).toString();
const QString album = item->data(Role_AlbumName).toString();
const QUrl url = item->data(Role_FirstUrl).toUrl();
const QString albumartist = item->data(Role_AlbumArtist).toString();
const QString album = item->data(Role_Album).toString();
const QList<QUrl> &urls = item->urls;
const Song::FileType filetype = Song::FileType(item->data(Role_Filetype).toInt());
const bool has_cue = !item->data(Role_CuePath).toString().isEmpty();
QUrl new_cover_url = album_cover_choice_controller_->SaveCoverToFileAutomatic(Song::Source_Collection, (!albumartist.isEmpty() ? albumartist : artist), album, QString(), url.adjusted(QUrl::RemoveFilename).path(), cover_url, image, false);
if (new_cover_url.isEmpty()) return;
if (album_cover_choice_controller_->get_save_album_cover_type() == CollectionSettingsPage::SaveCoverType_Embedded && Song::save_embedded_cover_supported(filetype) && !has_cue) {
if (result.is_jpeg()) {
quint64 id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.image_data);
cover_save_tasks_.insert(id, item);
}
else {
quint64 id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.image);
cover_save_tasks_.insert(id, item);
}
}
else {
QUrl cover_url = album_cover_choice_controller_->SaveCoverToFileAutomatic(Song::Source_Collection, albumartist, album, QString(), urls.first().adjusted(QUrl::RemoveFilename).path(), result, false);
// Save the image in the database
collection_backend_->UpdateManualAlbumArtAsync(artist, albumartist, album, new_cover_url);
if (cover_url.isEmpty()) return;
// Update the icon in our list
quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, QUrl(), new_cover_url);
item->setData(Role_PathManual, new_cover_url);
cover_loading_tasks_[id] = item;
// Save the image in the database
collection_backend_->UpdateManualAlbumArtAsync(albumartist, album, cover_url);
// Update the icon in our list
UpdateCoverInList(item, cover_url);
}
}
@ -866,7 +1008,7 @@ void AlbumCoverManager::ExportCovers() {
cover_exporter_->SetDialogResult(result);
for (int i = 0; i < ui_->albums->count(); ++i) {
QListWidgetItem *item = ui_->albums->item(i);
AlbumItem *item = static_cast<AlbumItem*>(ui_->albums->item(i));
// skip hidden and coverless albums
if (item->isHidden() || !ItemHasCover(*item)) {
@ -918,31 +1060,36 @@ void AlbumCoverManager::UpdateExportStatus(const int exported, const int skipped
}
QString AlbumCoverManager::EffectiveAlbumArtistName(const QListWidgetItem &item) const {
QString albumartist = item.data(Role_AlbumArtistName).toString();
if (!albumartist.isEmpty()) {
return albumartist;
}
return item.data(Role_ArtistName).toString();
}
QImage AlbumCoverManager::GenerateNoCoverImage(const QImage &image) const {
QImage AlbumCoverManager::GenerateNoCoverImage(const QIcon &no_cover_icon) const {
// Get a square version of the nocover image with some transparency:
QImage image_scaled = image.scaled(120, 120, Qt::KeepAspectRatio, Qt::SmoothTransformation);
// Get a square version of cdcase.png with some transparency:
QImage nocover = no_cover_icon.pixmap(no_cover_icon.availableSizes().last()).toImage();
nocover = nocover.scaled(120, 120, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QImage square_nocover(120, 120, QImage::Format_ARGB32);
square_nocover.fill(0);
QPainter p(&square_nocover);
QImage image_square(120, 120, QImage::Format_ARGB32);
image_square.fill(0);
QPainter p(&image_square);
p.setOpacity(0.4);
p.drawImage((120 - nocover.width()) / 2, (120 - nocover.height()) / 2, nocover);
p.drawImage((120 - image_scaled.width()) / 2, (120 - image_scaled.height()) / 2, image_scaled);
p.end();
return square_nocover;
return image_square;
}
bool AlbumCoverManager::ItemHasCover(const QListWidgetItem &item) const {
return item.icon().cacheKey() != no_cover_item_icon_.cacheKey();
bool AlbumCoverManager::ItemHasCover(const AlbumItem &item) const {
return item.icon().cacheKey() != icon_nocover_item_.cacheKey();
}
void AlbumCoverManager::SaveEmbeddedCoverAsyncFinished(quint64 id, const bool success) {
while (cover_save_tasks_.contains(id)) {
AlbumItem *album_item = cover_save_tasks_.take(id);
if (!success) continue;
album_item->setData(Role_PathAutomatic, QUrl::fromLocalFile(Song::kEmbeddedCover));
Song song = ItemAsSong(album_item);
album_cover_choice_controller_->SaveArtAutomaticToSong(&song, QUrl::fromLocalFile(Song::kEmbeddedCover));
quint64 cover_load_id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, album_item->data(Role_PathAutomatic).toUrl(), album_item->data(Role_PathManual).toUrl(), album_item->urls.first());
cover_loading_tasks_[cover_load_id] = album_item;
}
}

View File

@ -31,6 +31,7 @@
#include <QList>
#include <QListWidgetItem>
#include <QMap>
#include <QMultiMap>
#include <QString>
#include <QImage>
#include <QIcon>
@ -38,7 +39,9 @@
#include "core/song.h"
#include "albumcoverloaderoptions.h"
#include "albumcoverloaderresult.h"
#include "albumcoverchoicecontroller.h"
#include "coversearchstatistics.h"
#include "settings/collectionsettingspage.h"
class QWidget;
class QMimeData;
@ -53,7 +56,6 @@ class QShowEvent;
class Application;
class CollectionBackend;
class SongMimeData;
class AlbumCoverChoiceController;
class AlbumCoverExport;
class AlbumCoverExporter;
class AlbumCoverFetcher;
@ -61,17 +63,21 @@ class AlbumCoverSearcher;
class Ui_CoverManager;
class AlbumItem : public QListWidgetItem {
public:
AlbumItem(const QIcon &icon, const QString &text, QListWidget *parent = nullptr, int type = Type) : QListWidgetItem(icon, text, parent, type) {};
QList<QUrl> urls;
};
class AlbumCoverManager : public QMainWindow {
Q_OBJECT
public:
explicit AlbumCoverManager(Application *app, CollectionBackend *collection_backend, QMainWindow *mainwindow, QWidget *parent = nullptr);
~AlbumCoverManager() override;
static const char *kSettingsGroup;
CollectionBackend *backend() const;
QIcon no_cover_icon() const { return no_cover_icon_; }
void Reset();
void Init();
void ReloadSettings();
@ -80,15 +86,15 @@ class AlbumCoverManager : public QMainWindow {
void DisableCoversButtons();
SongList GetSongsInAlbum(const QModelIndex &idx) const;
SongList GetSongsInAlbums(const QModelIndexList &indexes) const;
SongMimeData *GetMimeDataForAlbums(const QModelIndexList &indexes) const;
CollectionBackend *backend() const { return collection_backend_; }
protected:
void showEvent(QShowEvent*) override;
void closeEvent(QCloseEvent*) override;
void showEvent(QShowEvent *e) override;
void closeEvent(QCloseEvent *e) override;
// For the album view context menu events
bool eventFilter(QObject *obj, QEvent *event) override;
bool eventFilter(QObject *obj, QEvent *e) override;
private:
enum ArtistItemType {
@ -98,12 +104,14 @@ class AlbumCoverManager : public QMainWindow {
};
enum Role {
Role_ArtistName = Qt::UserRole + 1,
Role_AlbumArtistName,
Role_AlbumName,
Role_AlbumArtist = Qt::UserRole + 1,
Role_Album,
Role_PathAutomatic,
Role_PathManual,
Role_FirstUrl
Role_Filetype,
Role_CuePath,
Role_ImageData,
Role_Image
};
enum HideCovers {
@ -113,21 +121,29 @@ class AlbumCoverManager : public QMainWindow {
};
void LoadGeometry();
void SaveGeometry();
void SaveSettings();
QString InitialPathForOpenCoverDialog(const QString &path_automatic, const QString &first_file_name) const;
QString EffectiveAlbumArtistName(const QListWidgetItem &item) const;
// Returns the selected element in form of a Song ready to be used by AlbumCoverChoiceController or invalid song if there's nothing or multiple elements selected.
Song GetSingleSelectionAsSong();
// Returns the first of the selected elements in form of a Song ready to be used by AlbumCoverChoiceController or invalid song if there's nothing selected.
Song GetFirstSelectedAsSong();
Song ItemAsSong(QListWidgetItem *item);
Song ItemAsSong(QListWidgetItem *item) { return ItemAsSong(static_cast<AlbumItem*>(item)); }
Song ItemAsSong(AlbumItem *item);
void UpdateStatusText();
bool ShouldHide(const QListWidgetItem &item, const QString &filter, HideCovers hide) const;
void SaveAndSetCover(QListWidgetItem *item, const QUrl &cover_url, const QImage &image);
bool ShouldHide(const AlbumItem &item, const QString &filter, HideCovers hide) const;
void SaveAndSetCover(AlbumItem *item, const AlbumCoverImageResult &result);
void SaveImageToAlbums(Song *song, const AlbumCoverImageResult &result);
SongList GetSongsInAlbums(const QModelIndexList &indexes) const;
SongMimeData *GetMimeDataForAlbums(const QModelIndexList &indexes) const;
QImage GenerateNoCoverImage(const QImage &image) const;
bool ItemHasCover(const AlbumItem &item) const;
signals:
void AddToPlaylist(QMimeData *data);
@ -138,7 +154,7 @@ class AlbumCoverManager : public QMainWindow {
void UpdateFilter();
void FetchAlbumCovers();
void ExportCovers();
void AlbumCoverFetched(const quint64 id, const QUrl &cover_url, const QImage &image, const CoverSearchStatistics &statistics);
void AlbumCoverFetched(const quint64 id, const AlbumCoverImageResult &result, const CoverSearchStatistics &statistics);
void CancelRequests();
// On the context menu
@ -149,6 +165,8 @@ class AlbumCoverManager : public QMainWindow {
void LoadCoverFromURL();
void SearchForCover();
void UnsetCover();
void ClearCover();
void DeleteCover();
void ShowCover();
// For adding albums to the playlist
@ -156,14 +174,16 @@ class AlbumCoverManager : public QMainWindow {
void AddSelectedToPlaylist();
void LoadSelectedToPlaylist();
void UpdateCoverInList(QListWidgetItem *item, const QUrl &cover);
void UpdateCoverInList(AlbumItem *item, const QUrl &cover);
void UpdateExportStatus(const int exported, const int skipped, const int max);
void SaveEmbeddedCoverAsyncFinished(quint64 id, const bool success);
private:
Ui_CoverManager *ui_;
QMainWindow *mainwindow_;
Application *app_;
CollectionBackend *collection_backend_;
AlbumCoverChoiceController *album_cover_choice_controller_;
QAction *filter_all_;
@ -171,24 +191,20 @@ class AlbumCoverManager : public QMainWindow {
QAction *filter_without_covers_;
AlbumCoverLoaderOptions cover_loader_options_;
QMap<quint64, QListWidgetItem*> cover_loading_tasks_;
QMap<quint64, AlbumItem*> cover_loading_tasks_;
AlbumCoverFetcher *cover_fetcher_;
QMap<quint64, QListWidgetItem*> cover_fetching_tasks_;
QMap<quint64, AlbumItem*> cover_fetching_tasks_;
CoverSearchStatistics fetch_statistics_;
AlbumCoverSearcher *cover_searcher_;
AlbumCoverExport *cover_export_;
AlbumCoverExporter *cover_exporter_;
QImage GenerateNoCoverImage(const QIcon &no_cover_icon) const;
bool ItemHasCover(const QListWidgetItem &item) const;
QIcon artist_icon_;
QIcon all_artists_icon_;
const QIcon no_cover_icon_;
const QImage no_cover_image_;
const QIcon no_cover_item_icon_;
const QImage image_nocover_thumbnail_;
const QIcon icon_nocover_item_;
QMenu *context_menu_;
QList<QListWidgetItem*> context_menu_items_;
@ -197,7 +213,10 @@ class AlbumCoverManager : public QMainWindow {
QPushButton *abort_progress_;
int jobs_;
CollectionBackend *collection_backend_;
QMultiMap<qint64, AlbumItem*> cover_save_tasks_;
QList<AlbumItem*> cover_save_tasks2_;
QListWidgetItem *all_artists_;
};

View File

@ -69,7 +69,9 @@ QMimeData *AlbumCoverManagerList::mimeData(const QList<QListWidgetItem*> items)
mime_data->songs = songs;
mime_data->setUrls(urls);
mime_data->setData(orig_data->formats()[0], orig_data->data(orig_data->formats()[0]));
return mime_data;
}
void AlbumCoverManagerList::dropEvent(QDropEvent *e) {

View File

@ -30,6 +30,7 @@
#include <QStandardItem>
#include <QList>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QImage>
@ -58,6 +59,7 @@
#include "albumcoverloader.h"
#include "albumcoverloaderoptions.h"
#include "albumcoverloaderresult.h"
#include "albumcoverimageresult.h"
#include "ui_albumcoversearcher.h"
const int SizeOverlayDelegate::kMargin = 4;
@ -130,6 +132,8 @@ AlbumCoverSearcher::AlbumCoverSearcher(const QIcon &no_cover_icon, Application *
ui_->covers->setItemDelegate(new SizeOverlayDelegate(this));
ui_->covers->setModel(model_);
options_.get_image_data_ = true;
options_.get_image_ = true;
options_.scale_output_image_ = false;
options_.pad_output_image_ = false;
options_.create_thumbnail_ = true;
@ -157,7 +161,7 @@ void AlbumCoverSearcher::Init(AlbumCoverFetcher *fetcher) {
}
QImage AlbumCoverSearcher::Exec(const QString &artist, const QString &album) {
AlbumCoverImageResult AlbumCoverSearcher::Exec(const QString &artist, const QString &album) {
#ifdef Q_OS_MACOS
ui_->artist->clear();
@ -175,16 +179,18 @@ QImage AlbumCoverSearcher::Exec(const QString &artist, const QString &album) {
Search();
}
if (exec() == QDialog::Rejected) return QImage();
if (exec() == QDialog::Rejected) return AlbumCoverImageResult();
QModelIndex selected = ui_->covers->currentIndex();
if (!selected.isValid() || !selected.data(Role_ImageFetchFinished).toBool())
return QImage();
return AlbumCoverImageResult();
QIcon icon = selected.data(Qt::DecorationRole).value<QIcon>();
if (icon.cacheKey() == no_cover_icon_.cacheKey()) return QImage();
AlbumCoverImageResult result;
result.image_data = selected.data(Role_ImageData).value<QByteArray>();
result.image = selected.data(Role_Image).value<QImage>();
result.mime_type = Utilities::MimeTypeFromData(result.image_data);
return icon.pixmap(icon.availableSizes()[0]).toImage();
return result;
}
@ -213,10 +219,9 @@ void AlbumCoverSearcher::Search() {
}
void AlbumCoverSearcher::SearchFinished(const quint64 id, const CoverSearchResults &results) {
void AlbumCoverSearcher::SearchFinished(const quint64 id, const CoverProviderSearchResults &results) {
if (id != id_)
return;
if (id != id_) return;
ui_->search->setEnabled(true);
ui_->artist->setEnabled(true);
@ -225,7 +230,8 @@ void AlbumCoverSearcher::SearchFinished(const quint64 id, const CoverSearchResul
ui_->search->setText(tr("Search"));
id_ = 0;
for (const CoverSearchResult &result : results) {
for (const CoverProviderSearchResult &result : results) {
if (result.image_url.isEmpty()) continue;
quint64 new_id = app_->album_cover_loader()->LoadImageAsync(options_, result.image_url, QUrl());
@ -255,19 +261,25 @@ void AlbumCoverSearcher::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoad
if (cover_loading_tasks_.isEmpty()) ui_->busy->hide();
if (result.image_original.isNull()) {
if (!result.success || result.album_cover.image_data.isNull() || result.album_cover.image.isNull() || result.image_thumbnail.isNull()) {
model_->removeRow(item->row());
return;
}
QIcon icon;
icon.addPixmap(QPixmap::fromImage(result.image_original));
icon.addPixmap(QPixmap::fromImage(result.image_thumbnail));
QPixmap pixmap = QPixmap::fromImage(result.image_thumbnail);
if (pixmap.isNull()) {
model_->removeRow(item->row());
return;
}
QIcon icon(pixmap);
item->setData(true, Role_ImageFetchFinished);
item->setData(result.image_original.width() * result.image_original.height(), Role_ImageDimensions);
item->setData(result.image_original.size(), Role_ImageSize);
item->setIcon(icon);
item->setData(result.album_cover.image_data, Role_ImageData);
item->setData(result.album_cover.image, Role_Image);
item->setData(result.album_cover.image.width() * result.album_cover.image.height(), Role_ImageDimensions);
item->setData(result.album_cover.image.size(), Role_ImageSize);
if (!icon.isNull()) item->setIcon(icon);
}

View File

@ -32,6 +32,7 @@
#include <QStyledItemDelegate>
#include <QStyleOptionViewItem>
#include <QMap>
#include <QByteArray>
#include <QString>
#include <QImage>
#include <QIcon>
@ -39,6 +40,7 @@
#include "albumcoverfetcher.h"
#include "albumcoverloaderoptions.h"
#include "albumcoverloaderresult.h"
#include "albumcoverimageresult.h"
class QWidget;
class QStandardItem;
@ -77,20 +79,22 @@ class AlbumCoverSearcher : public QDialog {
Role_ImageURL = Qt::UserRole + 1,
Role_ImageRequestId,
Role_ImageFetchFinished,
Role_ImageDimensions, // width * height
Role_ImageData,
Role_Image,
Role_ImageDimensions,
Role_ImageSize,
};
void Init(AlbumCoverFetcher *fetcher);
QImage Exec(const QString &artist, const QString &album);
AlbumCoverImageResult Exec(const QString &artist, const QString &album);
protected:
void keyPressEvent(QKeyEvent*) override;
private slots:
void Search();
void SearchFinished(const quint64 id, const CoverSearchResults &results);
void SearchFinished(const quint64 id, const CoverProviderSearchResults &results);
void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);
void CoverDoubleClicked(const QModelIndex &idx);

View File

@ -89,7 +89,7 @@ void CoverExportRunnable::ProcessAndExportCover() {
// load embedded cover if any
if (song_.has_embedded_cover()) {
embedded_cover = TagReaderClient::Instance()->LoadEmbeddedArtBlocking(song_.url().toLocalFile());
embedded_cover = TagReaderClient::Instance()->LoadEmbeddedArtAsImageBlocking(song_.url().toLocalFile());
if (embedded_cover.isNull()) {
EmitCoverSkipped();
@ -179,7 +179,7 @@ void CoverExportRunnable::ExportCover() {
if (cover_path == Song::kEmbeddedCover) {
// an embedded cover
QImage embedded = TagReaderClient::Instance()->LoadEmbeddedArtBlocking(song_.url().toLocalFile());
QImage embedded = TagReaderClient::Instance()->LoadEmbeddedArtAsImageBlocking(song_.url().toLocalFile());
if (!embedded.save(new_file)) {
EmitCoverSkipped();
return;

View File

@ -31,8 +31,10 @@
#include <QNetworkRequest>
#include <QUrl>
#include "core/utilities.h"
#include "core/networkaccessmanager.h"
#include "widgets/busyindicator.h"
#include "albumcoverimageresult.h"
#include "coverfromurldialog.h"
#include "ui_coverfromurldialog.h"
@ -47,17 +49,17 @@ CoverFromURLDialog::~CoverFromURLDialog() {
delete ui_;
}
QImage CoverFromURLDialog::Exec() {
AlbumCoverImageResult CoverFromURLDialog::Exec() {
// reset state
ui_->url->setText("");;
last_image_ = QImage();
last_album_cover_ = AlbumCoverImageResult();
QClipboard *clipboard = QApplication::clipboard();
ui_->url->setText(clipboard->text());
exec();
return last_image_;
return last_album_cover_;
}
@ -89,11 +91,13 @@ void CoverFromURLDialog::LoadCoverFromURLFinished() {
return;
}
QImage image;
image.loadFromData(reply->readAll());
AlbumCoverImageResult result;
result.image_data = reply->readAll();
result.image.loadFromData(result.image_data);
result.mime_type = Utilities::MimeTypeFromData(result.image_data);
if (!image.isNull()) {
last_image_ = image;
if (!result.image.isNull()) {
last_album_cover_ = result;
QDialog::accept();
}
else {

View File

@ -28,6 +28,8 @@
#include <QString>
#include <QImage>
#include "albumcoverimageresult.h"
class QWidget;
class NetworkAccessManager;
@ -42,7 +44,7 @@ class CoverFromURLDialog : public QDialog {
~CoverFromURLDialog() override;
// Opens the dialog. This returns an image found at the URL chosen by user or null image if the dialog got rejected.
QImage Exec();
AlbumCoverImageResult Exec();
private slots:
void accept() override;
@ -52,7 +54,7 @@ class CoverFromURLDialog : public QDialog {
Ui_CoverFromURLDialog *ui_;
NetworkAccessManager *network_;
QImage last_image_;
AlbumCoverImageResult last_album_cover_;
};
#endif // COVERFROMURLDIALOG_H

View File

@ -27,4 +27,4 @@
#include "core/application.h"
#include "coverprovider.h"
CoverProvider::CoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent) : QObject(parent), app_(app), name_(name), enabled_(enabled), order_(0), authentication_required_(authentication_required), quality_(quality), fetchall_(fetchall), allow_missing_album_(allow_missing_album) {}
CoverProvider::CoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool batch, const bool allow_missing_album, Application *app, QObject *parent) : QObject(parent), app_(app), name_(name), enabled_(enabled), order_(0), authentication_required_(authentication_required), quality_(quality), batch_(batch), allow_missing_album_(allow_missing_album) {}

View File

@ -40,14 +40,14 @@ class CoverProvider : public QObject {
Q_OBJECT
public:
explicit CoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent);
explicit CoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool batch, const bool allow_missing_album, Application *app, QObject *parent);
// A name (very short description) of this provider, like "last.fm".
QString name() const { return name_; }
bool is_enabled() const { return enabled_; }
int order() const { return order_; }
bool quality() const { return quality_; }
bool fetchall() const { return fetchall_; }
bool batch() const { return batch_; }
bool allow_missing_album() const { return allow_missing_album_; }
void set_enabled(const bool enabled) { enabled_ = enabled; }
@ -70,8 +70,8 @@ class CoverProvider : public QObject {
void AuthenticationComplete(bool, QStringList = QStringList());
void AuthenticationSuccess();
void AuthenticationFailure(QStringList);
void SearchResults(int, CoverSearchResults);
void SearchFinished(int, CoverSearchResults);
void SearchResults(int, CoverProviderSearchResults);
void SearchFinished(int, CoverProviderSearchResults);
private:
Application *app_;
@ -80,7 +80,7 @@ class CoverProvider : public QObject {
int order_;
bool authentication_required_;
float quality_;
bool fetchall_;
bool batch_;
bool allow_missing_album_;
};

View File

@ -30,6 +30,7 @@
#include <QTemporaryFile>
#include "core/application.h"
#include "core/song.h"
#include "playlist/playlistmanager.h"
#include "albumcoverloader.h"
#include "albumcoverloaderoptions.h"
@ -43,6 +44,8 @@ CurrentAlbumCoverLoader::CurrentAlbumCoverLoader(Application *app, QObject *pare
id_(0)
{
options_.get_image_data_ = true;
options_.get_image_ = true;
options_.scale_output_image_ = false;
options_.pad_output_image_ = false;
options_.create_thumbnail_ = true;
@ -74,11 +77,11 @@ void CurrentAlbumCoverLoader::TempAlbumCoverLoaded(const quint64 id, AlbumCoverL
if (id != id_) return;
id_ = 0;
if (!result.image_scaled.isNull()) {
if (!result.album_cover.image.isNull()) {
temp_cover_.reset(new QTemporaryFile(temp_file_pattern_));
temp_cover_->setAutoRemove(true);
if (temp_cover_->open()) {
if (result.image_scaled.save(temp_cover_->fileName(), "JPEG")) {
if (result.album_cover.image.save(temp_cover_->fileName(), "JPEG")) {
result.temp_cover_url = QUrl::fromLocalFile(temp_cover_->fileName());
}
else {
@ -108,7 +111,7 @@ void CurrentAlbumCoverLoader::TempAlbumCoverLoaded(const quint64 id, AlbumCoverL
}
if (result.updated) {
last_song_.set_art_manual(result.cover_url);
last_song_.set_art_manual(result.album_cover.cover_url);
}
emit AlbumCoverLoaded(last_song_, result);

View File

@ -205,23 +205,23 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
QByteArray data = GetReplyData(reply);
if (data.isEmpty()) {
emit SearchFinished(id, CoverSearchResults());
emit SearchFinished(id, CoverProviderSearchResults());
return;
}
QJsonValue value_data = ExtractData(data);
if (!value_data.isArray()) {
emit SearchFinished(id, CoverSearchResults());
emit SearchFinished(id, CoverProviderSearchResults());
return;
}
QJsonArray array_data = value_data.toArray();
if (array_data.isEmpty()) {
emit SearchFinished(id, CoverSearchResults());
emit SearchFinished(id, CoverProviderSearchResults());
return;
}
QMap<QUrl, CoverSearchResult> results;
QMap<QUrl, CoverProviderSearchResult> results;
int i = 0;
for (const QJsonValue json_value : array_data) {
@ -279,7 +279,7 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
album = album.remove(Song::kAlbumRemoveDisc);
album = album.remove(Song::kAlbumRemoveMisc);
CoverSearchResult cover_result;
CoverProviderSearchResult cover_result;
cover_result.artist = artist;
cover_result.album = album;
@ -309,11 +309,11 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
}
if (results.isEmpty()) {
emit SearchFinished(id, CoverSearchResults());
emit SearchFinished(id, CoverProviderSearchResults());
}
else {
CoverSearchResults cover_results = results.values();
std::stable_sort(cover_results.begin(), cover_results.end(), AlbumCoverFetcherSearch::CoverSearchResultCompareNumber);
CoverProviderSearchResults cover_results = results.values();
std::stable_sort(cover_results.begin(), cover_results.end(), AlbumCoverFetcherSearch::CoverProviderSearchResultCompareNumber);
emit SearchFinished(id, cover_results);
}

View File

@ -441,7 +441,7 @@ void DiscogsCoverProvider::HandleReleaseReply(QNetworkReply *reply, const int se
if (width < 300 || height < 300) continue;
const float aspect_score = 1.0 - float(std::max(width, height) - std::min(width, height)) / std::max(height, width);
if (aspect_score < 0.85) continue;
CoverSearchResult result;
CoverProviderSearchResult result;
result.artist = artist;
result.album = album;
result.image_url = QUrl(obj_image["resource_url"].toString());

View File

@ -73,7 +73,7 @@ class DiscogsCoverProvider : public JsonCoverProvider {
QString album;
DiscogsCoverType type;
QMap<quint64, DiscogsCoverReleaseContext> requests_release_;
CoverSearchResults results;
CoverProviderSearchResults results;
};
private:

View File

@ -31,7 +31,7 @@
#include "coverprovider.h"
#include "jsoncoverprovider.h"
JsonCoverProvider::JsonCoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent) : CoverProvider(name, enabled, authentication_required, quality, fetchall, allow_missing_album, app, parent) {}
JsonCoverProvider::JsonCoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool batch, const bool allow_missing_album, Application *app, QObject *parent) : CoverProvider(name, enabled, authentication_required, quality, batch, allow_missing_album, app, parent) {}
QJsonObject JsonCoverProvider::ExtractJsonObj(const QByteArray &data) {

View File

@ -35,7 +35,7 @@ class JsonCoverProvider : public CoverProvider {
Q_OBJECT
public:
explicit JsonCoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent);
explicit JsonCoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool batch, const bool allow_missing_album, Application *app, QObject *parent);
QJsonObject ExtractJsonObj(const QByteArray &data);

View File

@ -135,7 +135,7 @@ void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, const int id, cons
QObject::disconnect(reply, nullptr, this, nullptr);
reply->deleteLater();
CoverSearchResults results;
CoverProviderSearchResults results;
QByteArray data = GetReplyData(reply);
if (data.isEmpty()) {
@ -275,7 +275,7 @@ void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, const int id, cons
if (!url.isValid()) continue;
CoverSearchResult cover_result;
CoverProviderSearchResult cover_result;
cover_result.artist = artist;
cover_result.album = album;
cover_result.image_url = url;

View File

@ -128,7 +128,7 @@ void MusicbrainzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int
QObject::disconnect(reply, nullptr, this, nullptr);
reply->deleteLater();
CoverSearchResults results;
CoverProviderSearchResults results;
QByteArray data = GetReplyData(reply);
if (data.isEmpty()) {
@ -217,7 +217,7 @@ void MusicbrainzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int
QString id = obj_release["id"].toString();
QString album = obj_release["title"].toString();
CoverSearchResult cover_result;
CoverProviderSearchResult cover_result;
QUrl url(QString(kAlbumCoverUrl).arg(id));
cover_result.artist = artist;
cover_result.album = album;

View File

@ -105,7 +105,7 @@ void MusixmatchCoverProvider::HandleSearchReply(QNetworkReply *reply, const int
QObject::disconnect(reply, nullptr, this, nullptr);
reply->deleteLater();
CoverSearchResults results;
CoverProviderSearchResults results;
if (reply->error() != QNetworkReply::NoError) {
Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
@ -195,7 +195,7 @@ void MusixmatchCoverProvider::HandleSearchReply(QNetworkReply *reply, const int
return;
}
CoverSearchResult result;
CoverProviderSearchResult result;
result.artist = obj_album["artistName"].toString();
result.album = obj_album["name"].toString();

View File

@ -170,7 +170,7 @@ void QobuzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
QObject::disconnect(reply, nullptr, this, nullptr);
reply->deleteLater();
CoverSearchResults results;
CoverProviderSearchResults results;
QByteArray data = GetReplyData(reply);
if (data.isEmpty()) {
@ -274,7 +274,7 @@ void QobuzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
album = album.remove(Song::kAlbumRemoveDisc);
album = album.remove(Song::kAlbumRemoveMisc);
CoverSearchResult cover_result;
CoverProviderSearchResult cover_result;
cover_result.artist = artist;
cover_result.album = album;
cover_result.image_url = cover_url;

View File

@ -466,36 +466,36 @@ void SpotifyCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id,
QByteArray data = GetReplyData(reply);
if (data.isEmpty()) {
emit SearchFinished(id, CoverSearchResults());
emit SearchFinished(id, CoverProviderSearchResults());
return;
}
QJsonObject json_obj = ExtractJsonObj(data);
if (json_obj.isEmpty()) {
emit SearchFinished(id, CoverSearchResults());
emit SearchFinished(id, CoverProviderSearchResults());
return;
}
if (!json_obj.contains(extract) || !json_obj[extract].isObject()) {
Error(QString("Json object is missing %1 object.").arg(extract), json_obj);
emit SearchFinished(id, CoverSearchResults());
emit SearchFinished(id, CoverProviderSearchResults());
return;
}
json_obj = json_obj[extract].toObject();
if (!json_obj.contains("items") || !json_obj["items"].isArray()) {
Error(QString("%1 object is missing items array.").arg(extract), json_obj);
emit SearchFinished(id, CoverSearchResults());
emit SearchFinished(id, CoverProviderSearchResults());
return;
}
QJsonArray array_items = json_obj["items"].toArray();
if (array_items.isEmpty()) {
emit SearchFinished(id, CoverSearchResults());
emit SearchFinished(id, CoverProviderSearchResults());
return;
}
CoverSearchResults results;
CoverProviderSearchResults results;
for (const QJsonValue value_item : array_items) {
if (!value_item.isObject()) {
@ -531,7 +531,7 @@ void SpotifyCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id,
int height = obj_image["height"].toInt();
if (width < 300 || height < 300) continue;
QUrl url(obj_image["url"].toString());
CoverSearchResult result;
CoverProviderSearchResult result;
result.album = album;
result.image_url = url;
result.image_size = QSize(width, height);

View File

@ -183,34 +183,34 @@ void TidalCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
QByteArray data = GetReplyData(reply);
if (data.isEmpty()) {
emit SearchFinished(id, CoverSearchResults());
emit SearchFinished(id, CoverProviderSearchResults());
return;
}
QJsonObject json_obj = ExtractJsonObj(data);
if (json_obj.isEmpty()) {
emit SearchFinished(id, CoverSearchResults());
emit SearchFinished(id, CoverProviderSearchResults());
return;
}
if (!json_obj.contains("items")) {
Error("Json object is missing items.", json_obj);
emit SearchFinished(id, CoverSearchResults());
emit SearchFinished(id, CoverProviderSearchResults());
return;
}
QJsonValue value_items = json_obj["items"];
if (!value_items.isArray()) {
emit SearchFinished(id, CoverSearchResults());
emit SearchFinished(id, CoverProviderSearchResults());
return;
}
QJsonArray array_items = value_items.toArray();
if (array_items.isEmpty()) {
emit SearchFinished(id, CoverSearchResults());
emit SearchFinished(id, CoverProviderSearchResults());
return;
}
CoverSearchResults results;
CoverProviderSearchResults results;
int i = 0;
for (const QJsonValue value_item : array_items) {
@ -262,7 +262,7 @@ void TidalCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
album = album.remove(Song::kAlbumRemoveMisc);
cover = cover.replace("-", "/");
CoverSearchResult cover_result;
CoverProviderSearchResult cover_result;
cover_result.artist = artist;
cover_result.album = album;
cover_result.number = ++i;

View File

@ -41,8 +41,10 @@ AddStreamDialog::~AddStreamDialog() { delete ui_; }
void AddStreamDialog::showEvent(QShowEvent *e) {
ui_->url->setFocus();
ui_->url->selectAll();
if (!e->spontaneous()) {
ui_->url->setFocus();
ui_->url->selectAll();
}
QDialog::showEvent(e);

File diff suppressed because it is too large Load Diff

View File

@ -28,10 +28,11 @@
#include <QObject>
#include <QAbstractItemModel>
#include <QDialog>
#include <QList>
#include <QVariant>
#include <QString>
#include <QUrl>
#include <QList>
#include <QMap>
#include <QImage>
#include "core/song.h"
@ -39,6 +40,7 @@
#include "playlist/playlistitem.h"
#include "covermanager/albumcoverloaderoptions.h"
#include "covermanager/albumcoverloaderresult.h"
#include "covermanager/albumcoverimageresult.h"
class QWidget;
class QMenu;
@ -51,9 +53,9 @@ class QHideEvent;
class Application;
class AlbumCoverChoiceController;
class TrackSelectionDialog;
class Ui_EditTagDialog;
#if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT)
class TrackSelectionDialog;
class TagFetcher;
#endif
@ -64,8 +66,9 @@ class EditTagDialog : public QDialog {
explicit EditTagDialog(Application *app, QWidget *parent = nullptr);
~EditTagDialog() override;
static const char *kHintText;
static const char *kSettingsGroup;
static const char *kTagsDifferentHintText;
static const char *kArtDifferentHintText;
void SetSongs(const SongList &songs, const PlaylistItemList &items = PlaylistItemList());
@ -78,25 +81,30 @@ class EditTagDialog : public QDialog {
protected:
bool eventFilter(QObject *o, QEvent *e) override;
void showEvent(QShowEvent*) override;
void hideEvent(QHideEvent*) override;
void showEvent(QShowEvent *e) override;
void hideEvent(QHideEvent *e) override;
private:
enum UpdateCoverAction {
UpdateCoverAction_None = 0,
UpdateCoverAction_Clear,
UpdateCoverAction_Unset,
UpdateCoverAction_Delete,
UpdateCoverAction_New,
};
struct Data {
explicit Data(const Song &song = Song()) : original_(song), current_(song) {}
explicit Data(const Song &song = Song()) : original_(song), current_(song), cover_action_(UpdateCoverAction_None) {}
static QVariant value(const Song &song, const QString &id);
QVariant original_value(const QString &id) const {
return value(original_, id);
}
QVariant current_value(const QString &id) const {
return value(current_, id);
}
QVariant original_value(const QString &id) const { return value(original_, id); }
QVariant current_value(const QString &id) const { return value(current_, id); }
void set_value(const QString &id, const QVariant &value);
Song original_;
Song current_;
UpdateCoverAction cover_action_;
AlbumCoverImageResult cover_result_;
};
private slots:
@ -118,12 +126,15 @@ class EditTagDialog : public QDialog {
void LoadCoverFromURL();
void SearchForCover();
void UnsetCover();
void ClearCover();
void DeleteCover();
void ShowCover();
void PreviousSong();
void NextSong();
void SongSaveComplete(TagReaderReply *reply, const QString &filename, const Song &song);
void SongSaveTagsComplete(TagReaderReply *reply, const QString &filename, Song song);
void SongSaveArtComplete(TagReaderReply *reply, const QString &filename, Song song, const UpdateCoverAction cover_action);
private:
struct FieldData {
@ -136,7 +147,7 @@ class EditTagDialog : public QDialog {
};
Song *GetFirstSelected();
void UpdateCoverOf(const Song &selected, const QModelIndexList &sel, const QUrl &cover_url);
void UpdateCover(const UpdateCoverAction action, const AlbumCoverImageResult &result = AlbumCoverImageResult());
bool DoesValueVary(const QModelIndexList &sel, const QString &id) const;
bool IsValueModified(const QModelIndexList &sel, const QString &id) const;
@ -146,23 +157,34 @@ class EditTagDialog : public QDialog {
void UpdateModifiedField(const FieldData &field, const QModelIndexList &sel);
void ResetFieldValue(const FieldData &field, const QModelIndexList &sel);
void UpdateSummaryTab(const Song &song);
void UpdateSummaryTab(const Song &song, const UpdateCoverAction cover_action);
void UpdateStatisticsTab(const Song &song);
void UpdateUI(const QModelIndexList &sel);
QString GetArtSummary(const Song &song, const UpdateCoverAction cover_action);
void UpdateUI(const QModelIndexList &indexes);
bool SetLoading(const QString &message);
void SetSongListVisibility(bool visible);
// Called by QtConcurrentRun
QList<Data> LoadData(const SongList &songs) const;
void SaveData(const QList<Data> &tag_data);
void SaveData();
static void SetText(QLabel *label, const int value, const QString &suffix, const QString &def = QString());
static void SetDate(QLabel *label, const uint time);
private:
Ui_EditTagDialog *ui_;
Application *app_;
AlbumCoverChoiceController *album_cover_choice_controller_;
#if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT)
TagFetcher *tag_fetcher_;
TrackSelectionDialog *results_dialog_;
#endif
const QImage image_no_cover_thumbnail_;
bool loading_;
@ -172,25 +194,20 @@ class EditTagDialog : public QDialog {
bool ignore_edits_;
#if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT)
TagFetcher *tag_fetcher_;
#endif
AlbumCoverLoaderOptions cover_options_;
quint64 cover_art_id_;
quint64 summary_cover_art_id_;
quint64 tags_cover_art_id_;
bool cover_art_is_set_;
// A copy of the original, unscaled album cover.
QImage original_;
QMenu *cover_menu_;
QPushButton *previous_button_;
QPushButton *next_button_;
TrackSelectionDialog *results_dialog_;
int save_tag_pending_;
int save_art_pending_;
int pending_;
QMap<int, Song> collection_songs_;
};
#endif // EDITTAGDIALOG_H

File diff suppressed because it is too large Load Diff

View File

@ -849,23 +849,22 @@ void InternetSearchView::LazyLoadAlbumCover(const QModelIndex &proxy_index) {
void InternetSearchView::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &albumcover_result) {
if (!cover_loader_tasks_.contains(id)) {
return;
}
if (!cover_loader_tasks_.contains(id)) return;
QPair<QModelIndex, QString> cover_loader_task = cover_loader_tasks_.take(id);
QModelIndex idx = cover_loader_task.first;
QString key = cover_loader_task.second;
QPixmap pixmap = QPixmap::fromImage(albumcover_result.image_scaled);
if (!pixmap.isNull()) {
pixmap_cache_.insert(key, pixmap);
}
if (idx.isValid()) {
QStandardItem *item = front_model_->itemFromIndex(idx);
if (item) {
item->setData(albumcover_result.image_scaled, Qt::DecorationRole);
if (albumcover_result.success && !albumcover_result.image_scaled.isNull()) {
QPixmap pixmap = QPixmap::fromImage(albumcover_result.image_scaled);
if (!pixmap.isNull()) {
pixmap_cache_.insert(key, pixmap);
}
if (idx.isValid()) {
QStandardItem *item = front_model_->itemFromIndex(idx);
if (item) {
item->setData(albumcover_result.image_scaled, Qt::DecorationRole);
}
}
}

View File

@ -160,7 +160,7 @@ void Organize::ProcessSomeFiles() {
if (!song.is_valid()) continue;
// Get embedded album cover
QImage cover = TagReaderClient::Instance()->LoadEmbeddedArtBlocking(task.song_info_.song_.url().toLocalFile());
QImage cover = TagReaderClient::Instance()->LoadEmbeddedArtAsImageBlocking(task.song_info_.song_.url().toLocalFile());
if (!cover.isNull()) song.set_image(cover);
#ifdef HAVE_GSTREAMER
@ -212,7 +212,7 @@ void Organize::ProcessSomeFiles() {
job.remove_original_ = !copy_;
job.playlist_ = playlist_;
if (task.song_info_.song_.art_manual_is_valid() && task.song_info_.song_.art_manual().path() != Song::kManuallyUnsetCover) {
if (task.song_info_.song_.art_manual_is_valid() && !task.song_info_.song_.has_manually_unset_cover()) {
if (task.song_info_.song_.art_manual().isLocalFile() && QFile::exists(task.song_info_.song_.art_manual().toLocalFile())) {
job.cover_source_ = task.song_info_.song_.art_manual().toLocalFile();
}
@ -220,7 +220,7 @@ void Organize::ProcessSomeFiles() {
job.cover_source_ = task.song_info_.song_.art_manual().path();
}
}
else if (task.song_info_.song_.art_automatic_is_valid() && task.song_info_.song_.art_automatic().path() != Song::kEmbeddedCover) {
else if (task.song_info_.song_.art_automatic_is_valid() && !task.song_info_.song_.has_embedded_cover()) {
if (task.song_info_.song_.art_automatic().isLocalFile() && QFile::exists(task.song_info_.song_.art_automatic().toLocalFile())) {
job.cover_source_ = task.song_info_.song_.art_automatic().toLocalFile();
}

View File

@ -393,7 +393,7 @@ bool Playlist::setData(const QModelIndex &idx, const QVariant &value, int role)
TagReaderReply *reply = TagReaderClient::Instance()->SaveFile(song.url().toLocalFile(), song);
QPersistentModelIndex persistent_index = QPersistentModelIndex(idx);
QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, persistent_index]() { SongSaveComplete(reply, persistent_index); });
QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, persistent_index]() { SongSaveComplete(reply, persistent_index); }, Qt::QueuedConnection);
return true;
@ -418,7 +418,8 @@ void Playlist::SongSaveComplete(TagReaderReply *reply, const QPersistentModelInd
emit Error(tr("An error occurred writing metadata to '%1'").arg(QString::fromStdString(reply->request_message().save_file_request().filename())));
}
}
reply->deleteLater();
metaObject()->invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
}
@ -755,7 +756,7 @@ bool Playlist::dropMimeData(const QMimeData *data, Qt::DropAction action, int ro
else if (const PlaylistItemMimeData *item_data = qobject_cast<const PlaylistItemMimeData*>(data)) {
InsertItems(item_data->items_, row, play_now, enqueue_now, enqueue_next_now);
}
else if (const InternetSongMimeData* internet_song_data = qobject_cast<const InternetSongMimeData*>(data)) {
else if (const InternetSongMimeData *internet_song_data = qobject_cast<const InternetSongMimeData*>(data)) {
InsertInternetItems(internet_song_data->service, internet_song_data->songs, row, play_now, enqueue_now, enqueue_next_now);
}
else if (const PlaylistGeneratorMimeData *generator_data = qobject_cast<const PlaylistGeneratorMimeData*>(data)) {
@ -2186,11 +2187,11 @@ void Playlist::UpdateScrobblePoint(const qint64 seek_point_nanosec) {
void Playlist::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &result) {
// Update art_manual for local songs that are not in the collection.
if (((result.type == AlbumCoverLoaderResult::Type_Manual && result.cover_url.isLocalFile()) || result.type == AlbumCoverLoaderResult::Type_ManuallyUnset) && (song.source() == Song::Source_LocalFile || song.source() == Song::Source_CDDA || song.source() == Song::Source_Device)) {
if (((result.type == AlbumCoverLoaderResult::Type_Manual && result.album_cover.cover_url.isLocalFile()) || result.type == AlbumCoverLoaderResult::Type_ManuallyUnset) && (song.source() == Song::Source_LocalFile || song.source() == Song::Source_CDDA || song.source() == Song::Source_Device)) {
PlaylistItemPtr item = current_item();
if (item && item->Metadata() == song && (!item->Metadata().art_manual_is_valid() || (result.type == AlbumCoverLoaderResult::Type_ManuallyUnset && !item->Metadata().has_manually_unset_cover()))) {
qLog(Debug) << "Updating art manual for local song" << song.title() << song.album() << song.title() << "to" << result.cover_url << "in playlist.";
item->SetArtManual(result.cover_url);
qLog(Debug) << "Updating art manual for local song" << song.title() << song.album() << song.title() << "to" << result.album_cover.cover_url << "in playlist.";
item->SetArtManual(result.album_cover.cover_url);
Save();
}
}

View File

@ -600,15 +600,20 @@ void PlaylistView::StartGlowing() {
}
void PlaylistView::hideEvent(QHideEvent *) { glow_timer_.stop(); }
void PlaylistView::hideEvent(QHideEvent *e) {
glow_timer_.stop();
QTreeView::hideEvent(e);
}
void PlaylistView::showEvent(QShowEvent *) {
void PlaylistView::showEvent(QShowEvent *e) {
if (currently_glowing_ && glow_enabled_)
glow_timer_.start(1500 / kGlowIntensitySteps, this);
MaybeAutoscroll(Playlist::AutoScroll_Maybe);
QTreeView::showEvent(e);
}
namespace {
@ -1087,6 +1092,8 @@ void PlaylistView::paintEvent(QPaintEvent *event) {
p.setPen(line_pen);
p.drawLine(QPoint(0, drop_pos), QPoint(width(), drop_pos));
QTreeView::paintEvent(event);
}
void PlaylistView::dragMoveEvent(QDragMoveEvent *event) {
@ -1260,6 +1267,7 @@ void PlaylistView::StretchChanged(const bool stretch) {
void PlaylistView::resizeEvent(QResizeEvent *e) {
QTreeView::resizeEvent(e);
if (dynamic_controls_->isVisible()) {
RepositionDynamicControls();
}
@ -1379,9 +1387,9 @@ void PlaylistView::Stopped() {
void PlaylistView::AlbumCoverLoaded(const Song &song, AlbumCoverLoaderResult result) {
if ((song != Song() && song_playing_ == Song()) || result.image_original == current_song_cover_art_) return;
if ((song != Song() && song_playing_ == Song()) || result.album_cover.image == current_song_cover_art_) return;
current_song_cover_art_ = result.image_original;
current_song_cover_art_ = result.album_cover.image;
if (background_image_type_ == AppearanceSettingsPage::BackgroundImageType_Album) {
if (song.art_automatic().isEmpty() && song.art_manual().isEmpty()) {
set_background_image(QImage());

View File

@ -29,7 +29,7 @@
#include "playlistitem.h"
#include "songplaylistitem.h"
SongPlaylistItem::SongPlaylistItem(const Song::Source &source) : PlaylistItem(source) {}
SongPlaylistItem::SongPlaylistItem(const Song::Source source) : PlaylistItem(source) {}
SongPlaylistItem::SongPlaylistItem(const Song &song) : PlaylistItem(song.source()), song_(song) {}
bool SongPlaylistItem::InitFromQuery(const SqlRow &query) {

View File

@ -32,12 +32,12 @@
class SongPlaylistItem : public PlaylistItem {
public:
explicit SongPlaylistItem(const Song::Source &source);
explicit SongPlaylistItem(const Song::Source source);
explicit SongPlaylistItem(const Song &song);
// Restores a stream- or file-related playlist item using query row.
// If it's a file related playlist item, this will restore it's CUE attributes (if any) but won't parse the CUE!
bool InitFromQuery(const SqlRow& query) override;
bool InitFromQuery(const SqlRow &query) override;
void Reload() override;
Song Metadata() const override;

View File

@ -37,7 +37,7 @@
#include "core/song.h"
#include "core/timeconstants.h"
#include "core/application.h"
#include "core/utilities.h"
#include "core/imageutils.h"
#include "covermanager/albumcoverloader.h"
#include "qobuzservice.h"
#include "qobuzurlhandler.h"
@ -1154,7 +1154,7 @@ void QobuzRequest::GetAlbumCovers() {
void QobuzRequest::AddAlbumCoverRequest(Song &song) {
QUrl cover_url(song.art_automatic());
QUrl cover_url = song.art_automatic();
if (!cover_url.isValid()) return;
if (album_covers_requests_sent_.contains(cover_url)) {
@ -1233,7 +1233,7 @@ void QobuzRequest::AlbumCoverReceived(QNetworkReply *reply, const QUrl &cover_ur
}
QString mimetype = reply->header(QNetworkRequest::ContentTypeHeader).toString();
if (!Utilities::SupportedImageMimeTypes().contains(mimetype, Qt::CaseInsensitive) && !Utilities::SupportedImageFormats().contains(mimetype, Qt::CaseInsensitive)) {
if (!ImageUtils::SupportedImageMimeTypes().contains(mimetype, Qt::CaseInsensitive) && !ImageUtils::SupportedImageFormats().contains(mimetype, Qt::CaseInsensitive)) {
Error(QString("Unsupported mimetype for image reader %1 for %2").arg(mimetype).arg(cover_url.toString()));
if (album_covers_requests_sent_.contains(cover_url)) album_covers_requests_sent_.remove(cover_url);
AlbumCoverFinishCheck();
@ -1248,7 +1248,7 @@ void QobuzRequest::AlbumCoverReceived(QNetworkReply *reply, const QUrl &cover_ur
return;
}
QList<QByteArray> format_list = Utilities::ImageFormatsForMimeType(mimetype.toUtf8());
QList<QByteArray> format_list = ImageUtils::ImageFormatsForMimeType(mimetype.toUtf8());
char *format = nullptr;
if (!format_list.isEmpty()) {
format = format_list.first().data();

View File

@ -80,7 +80,7 @@ CollectionSettingsPage::CollectionSettingsPage(SettingsDialog *dialog)
QObject::connect(ui_->add, &QPushButton::clicked, this, &CollectionSettingsPage::Add);
QObject::connect(ui_->remove, &QPushButton::clicked, this, &CollectionSettingsPage::Remove);
QObject::connect(ui_->checkbox_cover_album_dir, &QCheckBox::toggled, this, &CollectionSettingsPage::CoverSaveInAlbumDirChanged);
QObject::connect(ui_->radiobutton_save_albumcover_albumdir, &QRadioButton::toggled, this, &CollectionSettingsPage::CoverSaveInAlbumDirChanged);
QObject::connect(ui_->radiobutton_cover_hash, &QRadioButton::toggled, this, &CollectionSettingsPage::CoverSaveInAlbumDirChanged);
QObject::connect(ui_->radiobutton_cover_pattern, &QRadioButton::toggled, this, &CollectionSettingsPage::CoverSaveInAlbumDirChanged);
@ -162,11 +162,17 @@ void CollectionSettingsPage::Load() {
QStringList filters = s.value("cover_art_patterns", QStringList() << "front" << "cover").toStringList();
ui_->cover_art_patterns->setText(filters.join(","));
ui_->checkbox_cover_album_dir->setChecked(s.value("cover_album_dir", false).toBool());
SaveCover save_cover = SaveCover(s.value("cover_filename", SaveCover_Hash).toInt());
switch (save_cover) {
case SaveCover_Hash: ui_->radiobutton_cover_hash->setChecked(true); break;
case SaveCover_Pattern: ui_->radiobutton_cover_pattern->setChecked(true); break;
SaveCoverType save_cover_type = SaveCoverType(s.value("save_cover_type", SaveCoverType_Cache).toInt());
switch (save_cover_type) {
case SaveCoverType_Cache: ui_->radiobutton_save_albumcover_cache->setChecked(true); break;
case SaveCoverType_Album: ui_->radiobutton_save_albumcover_albumdir->setChecked(true); break;
case SaveCoverType_Embedded: ui_->radiobutton_save_albumcover_embedded->setChecked(true); break;
}
SaveCoverFilename save_cover_filename = SaveCoverFilename(s.value("save_cover_filename", SaveCoverFilename_Hash).toInt());
switch (save_cover_filename) {
case SaveCoverFilename_Hash: ui_->radiobutton_cover_hash->setChecked(true); break;
case SaveCoverFilename_Pattern: ui_->radiobutton_cover_pattern->setChecked(true); break;
}
QString cover_pattern = s.value("cover_pattern").toString();
if (!cover_pattern.isEmpty()) ui_->lineedit_cover_pattern->setText(cover_pattern);
@ -222,11 +228,17 @@ void CollectionSettingsPage::Save() {
s.setValue("cover_art_patterns", filters);
s.setValue("cover_album_dir", ui_->checkbox_cover_album_dir->isChecked());
SaveCover save_cover = SaveCover_Hash;
if (ui_->radiobutton_cover_hash->isChecked()) save_cover = SaveCover_Hash;
if (ui_->radiobutton_cover_pattern->isChecked()) save_cover = SaveCover_Pattern;
s.setValue("cover_filename", int(save_cover));
SaveCoverType save_cover_type = SaveCoverType_Cache;
if (ui_->radiobutton_save_albumcover_cache->isChecked()) save_cover_type = SaveCoverType_Cache;
else if (ui_->radiobutton_save_albumcover_albumdir->isChecked()) save_cover_type = SaveCoverType_Album;
else if (ui_->radiobutton_save_albumcover_embedded->isChecked()) save_cover_type = SaveCoverType_Embedded;
s.setValue("save_cover_type", int(save_cover_type));
SaveCoverFilename save_cover_filename = SaveCoverFilename_Hash;
if (ui_->radiobutton_cover_hash->isChecked()) save_cover_filename = SaveCoverFilename_Hash;
else if (ui_->radiobutton_cover_pattern->isChecked()) save_cover_filename = SaveCoverFilename_Pattern;
s.setValue("save_cover_filename", int(save_cover_filename));
s.setValue("cover_pattern", ui_->lineedit_cover_pattern->text());
s.setValue("cover_overwrite", ui_->checkbox_cover_overwrite->isChecked());
s.setValue("cover_lowercase", ui_->checkbox_cover_lowercase->isChecked());
@ -246,7 +258,7 @@ void CollectionSettingsPage::Save() {
void CollectionSettingsPage::CoverSaveInAlbumDirChanged() {
if (ui_->checkbox_cover_album_dir->isChecked()) {
if (ui_->radiobutton_save_albumcover_albumdir->isChecked()) {
if (!ui_->groupbox_cover_filename->isEnabled()) {
ui_->groupbox_cover_filename->setEnabled(true);
}

View File

@ -50,6 +50,17 @@ class CollectionSettingsPage : public SettingsPage {
static const int kSettingsCacheSizeDefault;
static const int kSettingsDiskCacheSizeDefault;
enum SaveCoverType {
SaveCoverType_Cache = 1,
SaveCoverType_Album = 2,
SaveCoverType_Embedded = 3
};
enum SaveCoverFilename {
SaveCoverFilename_Hash = 1,
SaveCoverFilename_Pattern = 2
};
enum CacheSizeUnit {
CacheSizeUnit_KB,
CacheSizeUnit_MB,
@ -57,11 +68,6 @@ class CollectionSettingsPage : public SettingsPage {
CacheSizeUnit_TB,
};
enum SaveCover {
SaveCover_Hash = 1,
SaveCover_Pattern = 2
};
void Load() override;
void Save() override;

View File

@ -157,10 +157,33 @@ If there are no matches then it will use the largest image in the directory.</st
<number>0</number>
</property>
<item>
<widget class="QCheckBox" name="checkbox_cover_album_dir">
<property name="text">
<string>Save album covers in album directory</string>
<widget class="QGroupBox" name="groupbox_save_options">
<property name="title">
<string/>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QRadioButton" name="radiobutton_save_albumcover_albumdir">
<property name="text">
<string>Save album covers in album directory</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radiobutton_save_albumcover_cache">
<property name="text">
<string>Save album covers in cache directory</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radiobutton_save_albumcover_embedded">
<property name="text">
<string>Save album covers as embedded cover</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
@ -185,16 +208,16 @@ If there are no matches then it will use the largest image in the directory.</st
<number>0</number>
</property>
<item>
<widget class="QRadioButton" name="radiobutton_cover_hash">
<widget class="QRadioButton" name="radiobutton_cover_pattern">
<property name="text">
<string>Use hash</string>
<string>Pattern</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radiobutton_cover_pattern">
<widget class="QRadioButton" name="radiobutton_cover_hash">
<property name="text">
<string>Use pattern</string>
<string>Random</string>
</property>
</widget>
</item>
@ -463,9 +486,7 @@ If there are no matches then it will use the largest image in the directory.</st
<tabstop>auto_open</tabstop>
<tabstop>pretty_covers</tabstop>
<tabstop>show_dividers</tabstop>
<tabstop>checkbox_cover_album_dir</tabstop>
<tabstop>radiobutton_cover_hash</tabstop>
<tabstop>radiobutton_cover_pattern</tabstop>
<tabstop>lineedit_cover_pattern</tabstop>
<tabstop>checkbox_cover_overwrite</tabstop>
<tabstop>checkbox_cover_lowercase</tabstop>

View File

@ -189,14 +189,15 @@ SettingsDialog::~SettingsDialog() {
void SettingsDialog::showEvent(QShowEvent *e) {
LoadGeometry();
// Load settings
loading_settings_ = true;
for (const PageData &page : pages_.values()) {
page.page_->Load();
if (!e->spontaneous()) {
LoadGeometry();
// Load settings
loading_settings_ = true;
for (const PageData &page : pages_.values()) {
page.page_->Load();
}
loading_settings_ = false;
}
loading_settings_ = false;
QDialog::showEvent(e);

View File

@ -20,6 +20,8 @@
#include "config.h"
#include <QShowEvent>
#include "core/iconloader.h"
#include "settingspage.h"
#include "transcoder/transcoderoptionsflac.h"
@ -49,7 +51,7 @@ TranscoderSettingsPage::~TranscoderSettingsPage() {
void TranscoderSettingsPage::showEvent(QShowEvent *e) {
set_changed();
if (!e->spontaneous()) set_changed();
QWidget::showEvent(e);

View File

@ -28,6 +28,8 @@
#include "settingspage.h"
class QShowEvent;
class SettingsDialog;
class Ui_TranscoderSettingsPage;
@ -35,7 +37,7 @@ class TranscoderSettingsPage : public SettingsPage {
Q_OBJECT
public:
explicit TranscoderSettingsPage(SettingsDialog* dialog);
explicit TranscoderSettingsPage(SettingsDialog *dialog);
~TranscoderSettingsPage() override;
static const char *kSettingsGroup;
@ -47,7 +49,7 @@ class TranscoderSettingsPage : public SettingsPage {
void showEvent(QShowEvent *e) override;
private:
Ui_TranscoderSettingsPage* ui_;
Ui_TranscoderSettingsPage *ui_;
};
#endif // TRANSCODERSETTINGSPAGE_H

View File

@ -46,7 +46,7 @@
#include "core/logging.h"
#include "core/song.h"
#include "core/timeconstants.h"
#include "core/utilities.h"
#include "core/imageutils.h"
#include "covermanager/albumcoverloader.h"
#include "subsonicservice.h"
#include "subsonicurlhandler.h"
@ -788,7 +788,7 @@ void SubsonicRequest::AlbumCoverReceived(QNetworkReply *reply, const QUrl url, c
}
QString mimetype = reply->header(QNetworkRequest::ContentTypeHeader).toString();
if (!Utilities::SupportedImageMimeTypes().contains(mimetype, Qt::CaseInsensitive) && !Utilities::SupportedImageFormats().contains(mimetype, Qt::CaseInsensitive)) {
if (!ImageUtils::SupportedImageMimeTypes().contains(mimetype, Qt::CaseInsensitive) && !ImageUtils::SupportedImageFormats().contains(mimetype, Qt::CaseInsensitive)) {
Error(QString("Unsupported mimetype for image reader %1 for %2").arg(mimetype).arg(url.toString()));
if (album_covers_requests_sent_.contains(url)) album_covers_requests_sent_.remove(url);
AlbumCoverFinishCheck();
@ -803,7 +803,7 @@ void SubsonicRequest::AlbumCoverReceived(QNetworkReply *reply, const QUrl url, c
return;
}
QList<QByteArray> format_list = Utilities::ImageFormatsForMimeType(mimetype.toUtf8());
QList<QByteArray> format_list = ImageUtils::ImageFormatsForMimeType(mimetype.toUtf8());
char *format = nullptr;
if (!format_list.isEmpty()) {
format = format_list.first().data();

View File

@ -37,7 +37,7 @@
#include "core/song.h"
#include "core/timeconstants.h"
#include "core/application.h"
#include "core/utilities.h"
#include "core/imageutils.h"
#include "covermanager/albumcoverloader.h"
#include "tidalservice.h"
#include "tidalurlhandler.h"
@ -1116,7 +1116,7 @@ void TidalRequest::AddAlbumCoverRequest(Song &song) {
AlbumCoverRequest request;
request.album_id = song.album_id();
request.url = QUrl(song.art_automatic());
request.url = song.art_automatic();
request.filename = app_->album_cover_loader()->CoverFilePath(song.source(), song.effective_albumartist(), song.effective_album(), song.album_id(), QString(), request.url);
if (request.filename.isEmpty()) return;
@ -1187,7 +1187,7 @@ void TidalRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &album
}
QString mimetype = reply->header(QNetworkRequest::ContentTypeHeader).toString();
if (!Utilities::SupportedImageMimeTypes().contains(mimetype, Qt::CaseInsensitive) && !Utilities::SupportedImageFormats().contains(mimetype, Qt::CaseInsensitive)) {
if (!ImageUtils::SupportedImageMimeTypes().contains(mimetype, Qt::CaseInsensitive) && !ImageUtils::SupportedImageFormats().contains(mimetype, Qt::CaseInsensitive)) {
Error(QString("Unsupported mimetype for image reader %1 for %2").arg(mimetype).arg(url.toString()));
if (album_covers_requests_sent_.contains(album_id)) album_covers_requests_sent_.remove(album_id);
AlbumCoverFinishCheck();
@ -1202,7 +1202,7 @@ void TidalRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &album
return;
}
QList<QByteArray> format_list = Utilities::ImageFormatsForMimeType(mimetype.toUtf8());
QList<QByteArray> format_list = ImageUtils::ImageFormatsForMimeType(mimetype.toUtf8());
char *format = nullptr;
if (!format_list.isEmpty()) {
format = format_list.first().data();

View File

@ -154,16 +154,20 @@ TranscodeDialog::~TranscodeDialog() {
delete ui_;
}
void TranscodeDialog::showEvent(QShowEvent*) {
void TranscodeDialog::showEvent(QShowEvent *e) {
LoadGeometry();
if (!e->spontaneous()) LoadGeometry();
QDialog::showEvent(e);
}
void TranscodeDialog::closeEvent(QCloseEvent*) {
void TranscodeDialog::closeEvent(QCloseEvent *e) {
SaveGeometry();
QDialog::closeEvent(e);
}
void TranscodeDialog::accept() {

View File

@ -54,8 +54,8 @@ class TranscodeDialog : public QDialog {
void SetFilenames(const QStringList &filenames);
protected:
void showEvent(QShowEvent*) override;
void closeEvent(QCloseEvent*) override;
void showEvent(QShowEvent *e) override;
void closeEvent(QCloseEvent *e) override;
void timerEvent(QTimerEvent *e) override;
private:

View File

@ -44,8 +44,8 @@
#include <QtEvents>
#include "core/application.h"
#include "core/imageutils.h"
#include "covermanager/albumcoverchoicecontroller.h"
#include "covermanager/albumcoverloader.h"
#include "playingwidget.h"
const char *PlayingWidget::kSettingsGroup = "PlayingWidget";
@ -133,9 +133,10 @@ void PlayingWidget::Init(Application *app, AlbumCoverChoiceController *album_cov
album_cover_choice_controller_ = album_cover_choice_controller;
album_cover_choice_controller_->Init(app_);
QList<QAction*> cover_actions = album_cover_choice_controller_->GetAllActions();
cover_actions.append(album_cover_choice_controller_->search_cover_auto_action());
menu_->addActions(cover_actions);
menu_->addSeparator();
menu_->addAction(album_cover_choice_controller_->search_cover_auto_action());
menu_->addSeparator();
above_statusbar_action_ = menu_->addAction(tr("Show above status bar"));
above_statusbar_action_->setCheckable(true);
@ -333,7 +334,7 @@ void PlayingWidget::SetImage(const QImage &image) {
}
void PlayingWidget::ScaleCover() {
pixmap_cover_ = QPixmap::fromImage(AlbumCoverLoader::ScaleAndPad(cover_loader_options_, image_original_).first);
pixmap_cover_ = QPixmap::fromImage(ImageUtils::ScaleAndPad(image_original_, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_));
update();
}

View File

@ -193,7 +193,7 @@ TEST_F(SingleSong, GetSongWithNoAlbum) {
CollectionBackend::AlbumList albums = backend_->GetAllAlbums();
//EXPECT_EQ(1, albums.size());
//EXPECT_EQ("Artist", albums[0].artist);
//EXPECT_EQ("", albums[0].album_name);
//EXPECT_EQ("", albums[0].album);
}
@ -213,8 +213,8 @@ TEST_F(SingleSong, GetAllAlbums) {
CollectionBackend::AlbumList albums = backend_->GetAllAlbums();
ASSERT_EQ(1, albums.size());
EXPECT_EQ(song_.album(), albums[0].album_name);
EXPECT_EQ(song_.artist(), albums[0].artist);
EXPECT_EQ(song_.album(), albums[0].album);
EXPECT_EQ(song_.artist(), albums[0].album_artist);
}
@ -224,8 +224,8 @@ TEST_F(SingleSong, GetAlbumsByArtist) {
CollectionBackend::AlbumList albums = backend_->GetAlbumsByArtist("Artist");
ASSERT_EQ(1, albums.size());
EXPECT_EQ(song_.album(), albums[0].album_name);
EXPECT_EQ(song_.artist(), albums[0].artist);
EXPECT_EQ(song_.album(), albums[0].album);
EXPECT_EQ(song_.artist(), albums[0].album_artist);
}
@ -233,9 +233,9 @@ TEST_F(SingleSong, GetAlbumArt) {
AddDummySong(); if (HasFatalFailure()) return;
CollectionBackend::Album album = backend_->GetAlbumArt("Artist", "AlbumArtist", "Album");
EXPECT_EQ(song_.album(), album.album_name);
EXPECT_EQ(song_.artist(), album.artist);
CollectionBackend::Album album = backend_->GetAlbumArt("Artist", "Album");
EXPECT_EQ(song_.album(), album.album);
EXPECT_EQ(song_.effective_albumartist(), album.album_artist);
}
@ -243,7 +243,7 @@ TEST_F(SingleSong, GetSongs) {
AddDummySong(); if (HasFatalFailure()) return;
SongList songs = backend_->GetSongs("Artist", "Album");
SongList songs = backend_->GetAlbumSongs("Artist", "Album");
ASSERT_EQ(1, songs.size());
EXPECT_EQ(song_.album(), songs[0].album());
EXPECT_EQ(song_.artist(), songs[0].artist());