diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e00f59d5..83ff4532 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/collection/collectionbackend.cpp b/src/collection/collectionbackend.cpp index 71267135..e084230d 100644 --- a/src/collection/collectionbackend.cpp +++ b/src/collection/collectionbackend.cpp @@ -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 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); diff --git a/src/collection/collectionbackend.h b/src/collection/collectionbackend.h index 03d6aea6..6c7ac3e3 100644 --- a/src/collection/collectionbackend.h +++ b/src/collection/collectionbackend.h @@ -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 &_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 urls; + Song::FileType filetype; + QString cue_path; }; typedef QList 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 &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 &artists, const bool on); void IncrementPlayCount(const int id); void IncrementSkipCount(const int id, const float progress); diff --git a/src/collection/collectionmodel.cpp b/src/collection/collectionmodel.cpp index 49063809..39b83fcb 100644 --- a/src/collection/collectionmodel.cpp +++ b/src/collection/collectionmodel.cpp @@ -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 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)); diff --git a/src/collection/collectionview.cpp b/src/collection/collectionview.cpp index 50f32e9e..d096923c 100644 --- a/src/collection/collectionview.cpp +++ b/src/collection/collectionview.cpp @@ -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); } diff --git a/src/collection/collectionwatcher.cpp b/src/collection/collectionwatcher.cpp index b5222b42..8e420ee1 100644 --- a/src/collection/collectionwatcher.cpp +++ b/src/collection/collectionwatcher.cpp @@ -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 diff --git a/src/context/contextalbum.cpp b/src/context/contextalbum.cpp index 1eb70f5f..ba6381c0 100644 --- a/src/context/contextalbum.cpp +++ b/src/context/contextalbum.cpp @@ -37,8 +37,8 @@ #include #include +#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 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 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(); diff --git a/src/context/contextalbumsmodel.cpp b/src/context/contextalbumsmodel.cpp index 8f3d4960..f95ccba1 100644 --- a/src/context/contextalbumsmodel.cpp +++ b/src/context/contextalbumsmodel.cpp @@ -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_); } diff --git a/src/context/contextview.cpp b/src/context/contextview.cpp index 53fb0beb..54116afa 100644 --- a/src/context/contextview.cpp +++ b/src/context/contextview.cpp @@ -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("" + tr("Albums by %1").arg( song_playing_.artist().toHtmlEscaped()) + ""); - for (CollectionBackend::Album album : albumlist) { - SongList songs = app_->collection_backend()->GetSongs(song_playing_.artist(), album.album_name, opt); + label_play_albums_->setText("" + tr("Albums by %1").arg(song_playing_.effective_albumartist().toHtmlEscaped()) + ""); + 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); diff --git a/src/core/imageutils.cpp b/src/core/imageutils.cpp new file mode 100644 index 00000000..179be0b1 --- /dev/null +++ b/src/core/imageutils.cpp @@ -0,0 +1,194 @@ +/* + * Strawberry Music Player + * Copyright 2019, Jonas Kvinge + * + * 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 . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 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() << "BMP"; + else if (mimetype == "image/gif") return QList() << "GIF"; + else if (mimetype == "image/jpeg") return QList() << "JPG"; + else if (mimetype == "image/png") return QList() << "PNG"; + else return QList(); +#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; + +} diff --git a/src/core/imageutils.h b/src/core/imageutils.h new file mode 100644 index 00000000..9904e990 --- /dev/null +++ b/src/core/imageutils.h @@ -0,0 +1,51 @@ +/* + * Strawberry Music Player + * Copyright 2019, Jonas Kvinge + * + * 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 . + * + */ + +#ifndef IMAGEUTILS_H +#define IMAGEUTILS_H + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include + +class ImageUtils { + + private: + static QStringList kSupportedImageMimeTypes; + static QStringList kSupportedImageFormats; + + public: + static QStringList SupportedImageMimeTypes(); + static QStringList SupportedImageFormats(); + static QList 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 diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 3646aa64..4d8f1614 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -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()<< 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(); diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index 5e3e88bb..dea2d505 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -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_; diff --git a/src/core/metatypes.cpp b/src/core/metatypes.cpp index 46a00ff2..eb5a4d9d 100644 --- a/src/core/metatypes.cpp +++ b/src/core/metatypes.cpp @@ -115,10 +115,10 @@ void RegisterMetaTypes() { qRegisterMetaType("PlaylistSequence::ShuffleMode"); qRegisterMetaType("AlbumCoverLoaderResult"); qRegisterMetaType("AlbumCoverLoaderResult::Type"); - qRegisterMetaType("CoverSearchResult"); + qRegisterMetaType("CoverProviderSearchResult"); qRegisterMetaType("CoverSearchStatistics"); - qRegisterMetaType >("QList"); - qRegisterMetaType("CoverSearchResults"); + qRegisterMetaType >("QList"); + qRegisterMetaType("CoverProviderSearchResults"); qRegisterMetaType("Equalizer::Params"); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) qRegisterMetaTypeStreamOperators("Equalizer::Params"); diff --git a/src/core/mpris2.cpp b/src/core/mpris2.cpp index 1a96080f..b2e5b081 100644 --- a/src/core/mpris2.cpp +++ b/src/core/mpris2.cpp @@ -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; diff --git a/src/core/mpris2.h b/src/core/mpris2.h index e90a069d..6789dafd 100644 --- a/src/core/mpris2.h +++ b/src/core/mpris2.h @@ -183,7 +183,7 @@ class Mpris2 : public QObject { // Methods void ActivatePlaylist(const QDBusObjectPath &playlist_id); - QList 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 diff --git a/src/core/song.cpp b/src/core/song.cpp index 40e42dd9..2eb36ea7 100644 --- a/src/core/song.cpp +++ b/src/core/song.cpp @@ -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(); } diff --git a/src/core/song.h b/src/core/song.h index 0044f10e..0cad743f 100644 --- a/src/core/song.h +++ b/src/core/song.h @@ -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; diff --git a/src/core/standarditemiconloader.cpp b/src/core/standarditemiconloader.cpp index 689f529d..77365fbe 100644 --- a/src/core/standarditemiconloader.cpp +++ b/src/core/standarditemiconloader.cpp @@ -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))); } diff --git a/src/core/tagreaderclient.cpp b/src/core/tagreaderclient.cpp index 51a3a531..1a3f4b09 100644 --- a/src/core/tagreaderclient.cpp +++ b/src/core/tagreaderclient.cpp @@ -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()); diff --git a/src/core/tagreaderclient.h b/src/core/tagreaderclient.h index 7feefdd5..3a807f7d 100644 --- a/src/core/tagreaderclient.h +++ b/src/core/tagreaderclient.h @@ -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 diff --git a/src/core/utilities.cpp b/src/core/utilities.cpp index 2de8ada2..c4b36463 100644 --- a/src/core/utilities.cpp +++ b/src/core/utilities.cpp @@ -57,7 +57,7 @@ #include #include #include -#include +#include #include #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) # include @@ -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 ImageFormatsForMimeType(const QByteArray &mimetype) { - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) - return QImageReader::imageFormatsForMimeType(mimetype); -#else - if (mimetype == "image/bmp") return QList() << "BMP"; - else if (mimetype == "image/gif") return QList() << "GIF"; - else if (mimetype == "image/jpeg") return QList() << "JPG"; - else if (mimetype == "image/png") return QList() << "PNG"; - else return QList(); -#endif + return QMimeDatabase().mimeTypeForData(data).name(); } diff --git a/src/core/utilities.h b/src/core/utilities.h index a8390119..b2653012 100644 --- a/src/core/utilities.h +++ b/src/core/utilities.h @@ -143,9 +143,8 @@ QString ReplaceVariable(const QString &variable, const Song &song, const QString bool IsColorDark(const QColor &color); -QStringList SupportedImageMimeTypes(); -QStringList SupportedImageFormats(); -QList ImageFormatsForMimeType(const QByteArray &mimetype); +QByteArray ReadDataFromFile(const QString &filename); +QString MimeTypeFromData(const QByteArray &data); } // namespace diff --git a/src/covermanager/albumcoverchoicecontroller.cpp b/src/covermanager/albumcoverchoicecontroller.cpp index ebca9007..b776a23c 100644 --- a/src/covermanager/albumcoverchoicecontroller.cpp +++ b/src/covermanager/albumcoverchoicecontroller.cpp @@ -23,6 +23,9 @@ #include #include +#include +#include +#include #include #include #include @@ -45,12 +48,16 @@ #include #include #include +#include +#include #include #include +#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 AlbumCoverChoiceController::GetAllActions() { - return QList() << cover_from_file_ << cover_to_file_ << separator_ << cover_from_url_ << search_for_cover_ << unset_cover_ << separator_ << show_cover_; + + return QList() << 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 future = QtConcurrent::run(&CollectionBackend::GetAlbumSongs, app_->collection_backend(), song.effective_albumartist(), song.effective_album(), QueryOptions()); +#else + QFuture future = QtConcurrent::run(app_->collection_backend(), &CollectionBackend::GetAlbumSongs, song.effective_albumartist(), song.effective_album(), QueryOptions()); +#endif + QFutureWatcher *watcher = new QFutureWatcher(); + watcher->setFuture(future); + QObject::connect(watcher, &QFutureWatcher::finished, this, [=]() { + SongList songs = watcher->result(); + watcher->deleteLater(); + QList 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 future = QtConcurrent::run(&CollectionBackend::GetAlbumSongs, app_->collection_backend(), song.effective_albumartist(), song.effective_album(), QueryOptions()); +#else + QFuture future = QtConcurrent::run(app_->collection_backend(), &CollectionBackend::GetAlbumSongs, song->effective_albumartist(), song->effective_album(), QueryOptions()); +#endif + QFutureWatcher *watcher = new QFutureWatcher(); + watcher->setFuture(future); + QObject::connect(watcher, &QFutureWatcher::finished, this, [=]() { + SongList songs = watcher->result(); + watcher->deleteLater(); + QList 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 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(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)); + +} diff --git a/src/covermanager/albumcoverchoicecontroller.h b/src/covermanager/albumcoverchoicecontroller.h index e983d8b7..ffe36a7f 100644 --- a/src/covermanager/albumcoverchoicecontroller.h +++ b/src/covermanager/albumcoverchoicecontroller.h @@ -27,18 +27,24 @@ #include #include #include +#include #include #include #include +#include #include #include #include +#include #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 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 cover_fetching_tasks_; + QMap 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_; }; diff --git a/src/covermanager/albumcoverfetcher.cpp b/src/covermanager/albumcoverfetcher.cpp index 2472fda8..c3a9bb69 100644 --- a/src/covermanager/albumcoverfetcher.cpp +++ b/src/covermanager/albumcoverfetcher.cpp @@ -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()); } diff --git a/src/covermanager/albumcoverfetcher.h b/src/covermanager/albumcoverfetcher.h index 59118d87..7a016af0 100644 --- a/src/covermanager/albumcoverfetcher.h +++ b/src/covermanager/albumcoverfetcher.h @@ -31,11 +31,13 @@ #include #include #include +#include #include #include #include #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 CoverSearchResults; -Q_DECLARE_METATYPE(QList) +typedef QList CoverProviderSearchResults; +Q_DECLARE_METATYPE(QList) // 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: diff --git a/src/covermanager/albumcoverfetchersearch.cpp b/src/covermanager/albumcoverfetchersearch.cpp index 311adf6d..3f97e69f 100644 --- a/src/covermanager/albumcoverfetchersearch.cpp +++ b/src/covermanager/albumcoverfetchersearch.cpp @@ -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::of(&AlbumCoverFetcherSearch::ProviderSearchResults)); + QObject::connect(provider, &CoverProvider::SearchResults, this, QOverload::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; } diff --git a/src/covermanager/albumcoverfetchersearch.h b/src/covermanager/albumcoverfetchersearch.h index f705983b..54fe3edb 100644 --- a/src/covermanager/albumcoverfetchersearch.h +++ b/src/covermanager/albumcoverfetchersearch.h @@ -29,12 +29,14 @@ #include #include #include +#include #include #include #include #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 pending_requests_; - QMap pending_image_loads_; + QMap pending_image_loads_; NetworkTimeouts* image_load_timeout_; - // QMap is sorted by key (score). Values are (result, image) - typedef QPair 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 candidate_images_; NetworkAccessManager *network_; diff --git a/src/covermanager/albumcoverimageresult.h b/src/covermanager/albumcoverimageresult.h new file mode 100644 index 00000000..35777aa2 --- /dev/null +++ b/src/covermanager/albumcoverimageresult.h @@ -0,0 +1,53 @@ +/* + * Strawberry Music Player + * Copyright 2021, Jonas Kvinge + * + * 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 . + * + */ + +#ifndef ALBUMCOVERIMAGERESULT_H +#define ALBUMCOVERIMAGERESULT_H + +#include "config.h" + +#include +#include +#include +#include +#include + +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 diff --git a/src/covermanager/albumcoverloader.cpp b/src/covermanager/albumcoverloader.cpp index dc950a8c..0a548236 100644 --- a/src/covermanager/albumcoverloader.cpp +++ b/src/covermanager/albumcoverloader.cpp @@ -27,10 +27,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -40,25 +42,29 @@ #include #include #include +#include #include #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::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 &ids) { - QMutexLocker l(&mutex_); + QMutexLocker l(&mutex_load_image_async_); for (QQueue::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 &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 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 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 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 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 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, urls), Q_ARG(QString, cover_filename)); + return id; + +} + +qint64 AlbumCoverLoader::SaveEmbeddedCoverAsync(const QList 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, urls), Q_ARG(QImage, image)); + return id; + +} + +qint64 AlbumCoverLoader::SaveEmbeddedCoverAsync(const QList 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, 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 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 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 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); } diff --git a/src/covermanager/albumcoverloader.h b/src/covermanager/albumcoverloader.h index 98931946..9cb60f39 100644 --- a/src/covermanager/albumcoverloader.h +++ b/src/covermanager/albumcoverloader.h @@ -30,15 +30,19 @@ #include #include #include +#include #include +#include #include #include #include #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 &ids); - static QPixmap TryLoadPixmap(const QUrl &automatic, const QUrl &manual, const QUrl &url = QUrl()); - static QPair 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 urls, const QString &cover_filename); + qint64 SaveEmbeddedCoverAsync(const QList urls, const QImage &image); + qint64 SaveEmbeddedCoverAsync(const QList 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 urls, const QImage &image); + void SaveEmbeddedCover(const qint64 id, const QList urls, const QString &cover_filename); + void SaveEmbeddedCover(const qint64 id, const QList 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 tasks_; QMap 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 tagreader_save_embedded_art_requests_; + }; #endif // ALBUMCOVERLOADER_H diff --git a/src/covermanager/albumcoverloaderoptions.h b/src/covermanager/albumcoverloaderoptions.h index 0cdf4a48..5fbfd029 100644 --- a/src/covermanager/albumcoverloaderoptions.h +++ b/src/covermanager/albumcoverloaderoptions.h @@ -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_; }; diff --git a/src/covermanager/albumcoverloaderresult.h b/src/covermanager/albumcoverloaderresult.h index a11eea2a..fab708be 100644 --- a/src/covermanager/albumcoverloaderresult.h +++ b/src/covermanager/albumcoverloaderresult.h @@ -25,6 +25,8 @@ #include #include +#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; diff --git a/src/covermanager/albumcovermanager.cpp b/src/covermanager/albumcovermanager.cpp index 68dc7a44..6d0fea62 100644 --- a/src/covermanager/albumcovermanager.cpp +++ b/src/covermanager/albumcovermanager.cpp @@ -28,6 +28,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -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() << 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::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(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(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(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(event); - context_menu_->popup(e->globalPos()); + QContextMenuEvent *context_menu_event = static_cast(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(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(¤t_song, cover_url); + QList urls; + QList album_items; + for (QListWidgetItem *item : context_menu_items_) { + AlbumItem *album_item = static_cast(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(¤t_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(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(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(¤t_song, cover_url); + if (album_item != first_album_item) { + Song current_song = ItemAsSong(album_item); + album_cover_choice_controller_->SaveArtManualToSong(¤t_song, cover_url); + } + } + +} + +void AlbumCoverManager::ClearCover() { + + Song song = GetFirstSelectedAsSong(); + if (!song.is_valid()) return; + + AlbumItem *first_album_item = static_cast(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(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(¤t_song, QUrl(), false); + } + } + +} + +void AlbumCoverManager::DeleteCover() { + + Song song = GetFirstSelectedAsSong(); + if (!song.is_valid()) return; + + AlbumItem *first_album_item = static_cast(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(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(¤t_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(idx.internalPointer()); + AlbumItem *item = static_cast(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 &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(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; + } + } diff --git a/src/covermanager/albumcovermanager.h b/src/covermanager/albumcovermanager.h index b7b45ef9..cc4fed3b 100644 --- a/src/covermanager/albumcovermanager.h +++ b/src/covermanager/albumcovermanager.h @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -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 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(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 cover_loading_tasks_; + QMap cover_loading_tasks_; AlbumCoverFetcher *cover_fetcher_; - QMap cover_fetching_tasks_; + QMap 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 context_menu_items_; @@ -197,7 +213,10 @@ class AlbumCoverManager : public QMainWindow { QPushButton *abort_progress_; int jobs_; - CollectionBackend *collection_backend_; + QMultiMap cover_save_tasks_; + QList cover_save_tasks2_; + + QListWidgetItem *all_artists_; }; diff --git a/src/covermanager/albumcovermanagerlist.cpp b/src/covermanager/albumcovermanagerlist.cpp index b133c3dc..4dd8e6dc 100644 --- a/src/covermanager/albumcovermanagerlist.cpp +++ b/src/covermanager/albumcovermanagerlist.cpp @@ -69,7 +69,9 @@ QMimeData *AlbumCoverManagerList::mimeData(const QList 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) { diff --git a/src/covermanager/albumcoversearcher.cpp b/src/covermanager/albumcoversearcher.cpp index 3db1ecc9..73837765 100644 --- a/src/covermanager/albumcoversearcher.cpp +++ b/src/covermanager/albumcoversearcher.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -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(); - if (icon.cacheKey() == no_cover_icon_.cacheKey()) return QImage(); + AlbumCoverImageResult result; + result.image_data = selected.data(Role_ImageData).value(); + result.image = selected.data(Role_Image).value(); + 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); } diff --git a/src/covermanager/albumcoversearcher.h b/src/covermanager/albumcoversearcher.h index 13d7a211..3eb89834 100644 --- a/src/covermanager/albumcoversearcher.h +++ b/src/covermanager/albumcoversearcher.h @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -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); diff --git a/src/covermanager/coverexportrunnable.cpp b/src/covermanager/coverexportrunnable.cpp index 070b1bc4..72ba9ae3 100644 --- a/src/covermanager/coverexportrunnable.cpp +++ b/src/covermanager/coverexportrunnable.cpp @@ -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; diff --git a/src/covermanager/coverfromurldialog.cpp b/src/covermanager/coverfromurldialog.cpp index f66efdfc..fcfd9b39 100644 --- a/src/covermanager/coverfromurldialog.cpp +++ b/src/covermanager/coverfromurldialog.cpp @@ -31,8 +31,10 @@ #include #include +#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 { diff --git a/src/covermanager/coverfromurldialog.h b/src/covermanager/coverfromurldialog.h index ab8772a2..77e7d420 100644 --- a/src/covermanager/coverfromurldialog.h +++ b/src/covermanager/coverfromurldialog.h @@ -28,6 +28,8 @@ #include #include +#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 diff --git a/src/covermanager/coverprovider.cpp b/src/covermanager/coverprovider.cpp index fae19440..e5234cee 100644 --- a/src/covermanager/coverprovider.cpp +++ b/src/covermanager/coverprovider.cpp @@ -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) {} diff --git a/src/covermanager/coverprovider.h b/src/covermanager/coverprovider.h index fe0c4ef6..e8a8bdbb 100644 --- a/src/covermanager/coverprovider.h +++ b/src/covermanager/coverprovider.h @@ -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_; }; diff --git a/src/covermanager/currentalbumcoverloader.cpp b/src/covermanager/currentalbumcoverloader.cpp index 999521a7..6ac84ef2 100644 --- a/src/covermanager/currentalbumcoverloader.cpp +++ b/src/covermanager/currentalbumcoverloader.cpp @@ -30,6 +30,7 @@ #include #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); diff --git a/src/covermanager/deezercoverprovider.cpp b/src/covermanager/deezercoverprovider.cpp index 17e90192..ba17d59d 100644 --- a/src/covermanager/deezercoverprovider.cpp +++ b/src/covermanager/deezercoverprovider.cpp @@ -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 results; + QMap 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); } diff --git a/src/covermanager/discogscoverprovider.cpp b/src/covermanager/discogscoverprovider.cpp index 0b6d397b..fc056810 100644 --- a/src/covermanager/discogscoverprovider.cpp +++ b/src/covermanager/discogscoverprovider.cpp @@ -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()); diff --git a/src/covermanager/discogscoverprovider.h b/src/covermanager/discogscoverprovider.h index caca8b66..47d6e3ba 100644 --- a/src/covermanager/discogscoverprovider.h +++ b/src/covermanager/discogscoverprovider.h @@ -73,7 +73,7 @@ class DiscogsCoverProvider : public JsonCoverProvider { QString album; DiscogsCoverType type; QMap requests_release_; - CoverSearchResults results; + CoverProviderSearchResults results; }; private: diff --git a/src/covermanager/jsoncoverprovider.cpp b/src/covermanager/jsoncoverprovider.cpp index c82765e1..17c95931 100644 --- a/src/covermanager/jsoncoverprovider.cpp +++ b/src/covermanager/jsoncoverprovider.cpp @@ -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) { diff --git a/src/covermanager/jsoncoverprovider.h b/src/covermanager/jsoncoverprovider.h index 368ec2b0..e63da1ed 100644 --- a/src/covermanager/jsoncoverprovider.h +++ b/src/covermanager/jsoncoverprovider.h @@ -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); diff --git a/src/covermanager/lastfmcoverprovider.cpp b/src/covermanager/lastfmcoverprovider.cpp index 5c8ca569..1acc43ec 100644 --- a/src/covermanager/lastfmcoverprovider.cpp +++ b/src/covermanager/lastfmcoverprovider.cpp @@ -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; diff --git a/src/covermanager/musicbrainzcoverprovider.cpp b/src/covermanager/musicbrainzcoverprovider.cpp index fb9e9d63..a7613969 100644 --- a/src/covermanager/musicbrainzcoverprovider.cpp +++ b/src/covermanager/musicbrainzcoverprovider.cpp @@ -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; diff --git a/src/covermanager/musixmatchcoverprovider.cpp b/src/covermanager/musixmatchcoverprovider.cpp index d3a65fd3..4e475b05 100644 --- a/src/covermanager/musixmatchcoverprovider.cpp +++ b/src/covermanager/musixmatchcoverprovider.cpp @@ -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(); diff --git a/src/covermanager/qobuzcoverprovider.cpp b/src/covermanager/qobuzcoverprovider.cpp index 8f087be7..3799b7d7 100644 --- a/src/covermanager/qobuzcoverprovider.cpp +++ b/src/covermanager/qobuzcoverprovider.cpp @@ -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; diff --git a/src/covermanager/spotifycoverprovider.cpp b/src/covermanager/spotifycoverprovider.cpp index 95474257..41f7f6a7 100644 --- a/src/covermanager/spotifycoverprovider.cpp +++ b/src/covermanager/spotifycoverprovider.cpp @@ -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); diff --git a/src/covermanager/tidalcoverprovider.cpp b/src/covermanager/tidalcoverprovider.cpp index 551e7094..66a872c4 100644 --- a/src/covermanager/tidalcoverprovider.cpp +++ b/src/covermanager/tidalcoverprovider.cpp @@ -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; diff --git a/src/dialogs/addstreamdialog.cpp b/src/dialogs/addstreamdialog.cpp index 79fb5e53..ca0156c2 100644 --- a/src/dialogs/addstreamdialog.cpp +++ b/src/dialogs/addstreamdialog.cpp @@ -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); diff --git a/src/dialogs/edittagdialog.cpp b/src/dialogs/edittagdialog.cpp index 5a1d87ac..4dd3bc7a 100644 --- a/src/dialogs/edittagdialog.cpp +++ b/src/dialogs/edittagdialog.cpp @@ -74,6 +74,7 @@ #include "core/logging.h" #include "core/tagreaderclient.h" #include "core/utilities.h" +#include "core/imageutils.h" #include "widgets/busyindicator.h" #include "widgets/lineedit.h" #include "collection/collectionbackend.h" @@ -81,18 +82,21 @@ #include "playlist/playlistdelegates.h" #if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT) # include "musicbrainz/tagfetcher.h" +# include "trackselectiondialog.h" #endif #include "covermanager/albumcoverchoicecontroller.h" #include "covermanager/albumcoverloader.h" #include "covermanager/albumcoverloaderoptions.h" #include "covermanager/albumcoverloaderresult.h" #include "covermanager/coverproviders.h" +#include "covermanager/currentalbumcoverloader.h" +#include "covermanager/albumcoverimageresult.h" #include "edittagdialog.h" -#include "trackselectiondialog.h" #include "ui_edittagdialog.h" #include "tagreadermessages.pb.h" -const char *EditTagDialog::kHintText = QT_TR_NOOP("(different across multiple songs)"); +const char *EditTagDialog::kTagsDifferentHintText = QT_TR_NOOP("(different across multiple songs)"); +const char *EditTagDialog::kArtDifferentHintText = QT_TR_NOOP("Different art across multiple songs."); const char *EditTagDialog::kSettingsGroup = "EditTagDialog"; EditTagDialog::EditTagDialog(Application *app, QWidget *parent) @@ -100,19 +104,20 @@ EditTagDialog::EditTagDialog(Application *app, QWidget *parent) ui_(new Ui_EditTagDialog), app_(app), album_cover_choice_controller_(new AlbumCoverChoiceController(this)), - loading_(false), - ignore_edits_(false), #if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT) tag_fetcher_(new TagFetcher(this)), -#endif - cover_art_id_(0), - cover_art_is_set_(false), results_dialog_(new TrackSelectionDialog(this)), - pending_(0) +#endif + image_no_cover_thumbnail_(ImageUtils::GenerateNoCoverImage(QSize(120, 120))), + loading_(false), + ignore_edits_(false), + summary_cover_art_id_(-1), + tags_cover_art_id_(-1), + cover_art_is_set_(false), + save_tag_pending_(0), + save_art_pending_(0) { - cover_options_.default_output_image_ = AlbumCoverLoader::ScaleAndPad(cover_options_, QImage(":/pictures/cdcase.png")).first; - QObject::connect(app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &EditTagDialog::AlbumCoverLoaded); #if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT) @@ -127,6 +132,7 @@ EditTagDialog::EditTagDialog(Application *app, QWidget *parent) ui_->setupUi(this); ui_->splitter->setSizes(QList() << 200 << width() - 200); ui_->loading_label->hide(); + ui_->label_lyrics->hide(); ui_->fetch_tag->setIcon(QPixmap::fromImage(QImage(":/pictures/musicbrainz.png"))); #if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT) @@ -169,15 +175,12 @@ EditTagDialog::EditTagDialog(Application *app, QWidget *parent) QPalette summary_label_palette(palette()); summary_label_palette.setColor(QPalette::WindowText, light ? color.lighter(150) : color.darker(150)); - for (QLabel *label : ui_->summary_tab->findChildren()) { + for (QLabel *label : ui_->tab_summary->findChildren()) { if (label->property("field_label").toBool()) { label->setPalette(summary_label_palette); } } - // Pretend the summary text is just a label - ui_->summary->setMaximumHeight(ui_->art->height() - ui_->summary_art_button->height() - 4); - QObject::connect(ui_->song_list->selectionModel(), &QItemSelectionModel::selectionChanged, this, &EditTagDialog::SelectionChanged); QObject::connect(ui_->button_box, &QDialogButtonBox::clicked, this, &EditTagDialog::ButtonClicked); QObject::connect(ui_->playcount_reset, &QPushButton::clicked, this, &EditTagDialog::ResetPlayCounts); @@ -195,14 +198,16 @@ EditTagDialog::EditTagDialog(Application *app, QWidget *parent) QObject::connect(album_cover_choice_controller_->cover_from_url_action(), &QAction::triggered, this, &EditTagDialog::LoadCoverFromURL); QObject::connect(album_cover_choice_controller_->search_for_cover_action(), &QAction::triggered, this, &EditTagDialog::SearchForCover); QObject::connect(album_cover_choice_controller_->unset_cover_action(), &QAction::triggered, this, &EditTagDialog::UnsetCover); + QObject::connect(album_cover_choice_controller_->clear_cover_action(), &QAction::triggered, this, &EditTagDialog::ClearCover); + QObject::connect(album_cover_choice_controller_->delete_cover_action(), &QAction::triggered, this, &EditTagDialog::DeleteCover); QObject::connect(album_cover_choice_controller_->show_cover_action(), &QAction::triggered, this, &EditTagDialog::ShowCover); cover_menu_->addActions(actions); - ui_->summary_art_button->setMenu(cover_menu_); + ui_->tags_art_button->setMenu(cover_menu_); - ui_->art->installEventFilter(this); - ui_->art->setAcceptDrops(true); + ui_->tags_art->installEventFilter(this); + ui_->tags_art->setAcceptDrops(true); // Add the next/previous buttons previous_button_ = new QPushButton(IconLoader::Load("go-previous"), tr("Previous"), this); @@ -243,6 +248,91 @@ EditTagDialog::~EditTagDialog() { delete ui_; } +void EditTagDialog::showEvent(QShowEvent *e) { + + if (!e->spontaneous()) { + + // Set the dialog's height to the smallest possible + resize(width(), sizeHint().height()); + + // Restore the tab that was current last time. + QSettings s; + s.beginGroup(kSettingsGroup); + if (s.contains("geometry")) { + restoreGeometry(s.value("geometry").toByteArray()); + } + ui_->tab_widget->setCurrentIndex(s.value("current_tab").toInt()); + s.endGroup(); + + album_cover_choice_controller_->ReloadSettings(); + + } + + QDialog::showEvent(e); + +} + +void EditTagDialog::hideEvent(QHideEvent *e) { + + // Save the current tab + QSettings s; + s.beginGroup(kSettingsGroup); + s.setValue("geometry", saveGeometry()); + s.setValue("current_tab", ui_->tab_widget->currentIndex()); + s.endGroup(); + + QDialog::hideEvent(e); + +} + +void EditTagDialog::accept() { + + // Show the loading indicator + if (!SetLoading(tr("Saving tracks") + "...")) return; + + SaveData(); + +} + +bool EditTagDialog::eventFilter(QObject *o, QEvent *e) { + + if (o == ui_->tags_art) { + switch (e->type()) { + case QEvent::MouseButtonRelease: +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + cover_menu_->popup(static_cast(e)->globalPosition().toPoint()); +#else + cover_menu_->popup(static_cast(e)->globalPos()); +#endif + break; + + case QEvent::DragEnter: { + QDragEnterEvent *event = static_cast(e); + if (AlbumCoverChoiceController::CanAcceptDrag(event)) { + event->acceptProposedAction(); + } + break; + } + + case QEvent::Drop: { + const QDropEvent *event = static_cast(e); + if (event->mimeData()->hasImage()) { + QImage image = qvariant_cast(event->mimeData()->imageData()); + if (!image.isNull()) { + UpdateCover(UpdateCoverAction_New, AlbumCoverImageResult(image)); + } + } + break; + } + + default: + break; + } + } + return false; + +} + bool EditTagDialog::SetLoading(const QString &message) { const bool loading = !message.isEmpty(); @@ -257,6 +347,7 @@ bool EditTagDialog::SetLoading(const QString &message) { #endif ui_->loading_label->setVisible(loading); ui_->loading_label->set_text(message); + return true; } @@ -270,7 +361,6 @@ QList EditTagDialog::LoadData(const SongList &songs) const // Try reloading the tags from file Song copy(song); TagReaderClient::Instance()->ReadFileBlocking(copy.url().toLocalFile(), ©); - if (copy.is_valid()) { copy.MergeUserSetData(song); ret << Data(copy); @@ -290,6 +380,7 @@ void EditTagDialog::SetSongs(const SongList &s, const PlaylistItemList &items) { data_.clear(); playlist_items_ = items; ui_->song_list->clear(); + collection_songs_.clear(); // Reload tags in the background #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) @@ -309,9 +400,7 @@ void EditTagDialog::SetSongsFinished() { QList result_data = watcher->result(); watcher->deleteLater(); - if (!SetLoading(QString())) { - return; - } + if (!SetLoading(QString())) return; data_ = result_data; @@ -321,8 +410,8 @@ void EditTagDialog::SetSongsFinished() { ui_->tab_widget->setEnabled(false); // Show a summary with empty information - UpdateSummaryTab(Song()); - ui_->tab_widget->setCurrentWidget(ui_->summary_tab); + UpdateSummaryTab(Song(), UpdateCoverAction_None); + ui_->tab_widget->setCurrentWidget(ui_->tab_summary); SetSongListVisibility(false); return; @@ -419,7 +508,7 @@ void EditTagDialog::InitFieldValue(const FieldData &field, const QModelIndexList editor->clear(); editor->clear_hint(); if (varies) { - editor->set_hint(tr(EditTagDialog::kHintText)); + editor->set_hint(tr(kTagsDifferentHintText)); editor->set_partially(); } else { @@ -487,40 +576,116 @@ void EditTagDialog::ResetFieldValue(const FieldData &field, const QModelIndexLis void EditTagDialog::SelectionChanged() { - const QModelIndexList sel = ui_->song_list->selectionModel()->selectedIndexes(); - if (sel.isEmpty()) - return; + const QModelIndexList indexes = ui_->song_list->selectionModel()->selectedIndexes(); + if (indexes.isEmpty()) return; // Set the editable fields - UpdateUI(sel); + UpdateUI(indexes); // If we're editing multiple songs then we have to disable certain tabs - const bool multiple = sel.count() > 1; - ui_->tab_widget->setTabEnabled(ui_->tab_widget->indexOf(ui_->summary_tab), !multiple); + const bool multiple = indexes.count() > 1; + ui_->tab_widget->setTabEnabled(ui_->tab_widget->indexOf(ui_->tab_summary), !multiple); + ui_->tab_widget->setTabEnabled(ui_->tab_widget->indexOf(ui_->tab_lyrics), !multiple); - if (!multiple) { - const Song &song = data_[sel.first().row()].original_; - UpdateSummaryTab(song); - UpdateStatisticsTab(song); + if (multiple) { + UpdateSummaryTab(Song(), UpdateCoverAction_None); + UpdateStatisticsTab(Song()); } + else { + UpdateSummaryTab(data_[indexes.first().row()].original_, data_[indexes.first().row()].cover_action_); + UpdateStatisticsTab(data_[indexes.first().row()].original_); + } + + const Song &first_song = data_[indexes.first().row()].original_; + UpdateCoverAction first_cover_action = data_[indexes.first().row()].cover_action_; + bool art_different = false; + bool action_different = false; + for (const QModelIndex &idx : indexes) { + if (data_[idx.row()].cover_action_ == UpdateCoverAction_None) { + data_[idx.row()].cover_result_ = AlbumCoverImageResult(); + } + const Song &song = data_[idx.row()].original_; + if (data_[idx.row()].cover_action_ != first_cover_action) action_different = true; + if (data_[idx.row()].cover_action_ != first_cover_action || + song.art_manual() != first_song.art_manual() || + song.has_embedded_cover() != first_song.has_embedded_cover() || + (song.art_manual().isEmpty() && song.art_automatic() != first_song.art_automatic()) || + (song.has_embedded_cover() && first_song.has_embedded_cover() && (first_song.effective_albumartist() != song.effective_albumartist() || first_song.album() != song.album())) + ) { + art_different = true; + break; + } + } + + QString summary; + + if (indexes.count() == 1) { + summary += "

" + first_song.PrettyTitleWithArtist().toHtmlEscaped() + "

"; + } + else { + summary += "

"; + summary += tr("%1 songs selected.").arg(indexes.count()); + summary += "

"; + } + + if (art_different && action_different) { + tags_cover_art_id_ = -1; // Cancels any pending art load. + ui_->tags_art->clear(); + ui_->tags_art->setText(kArtDifferentHintText); + album_cover_choice_controller_->show_cover_action()->setEnabled(false); + album_cover_choice_controller_->cover_to_file_action()->setEnabled(false); + album_cover_choice_controller_->cover_from_file_action()->setEnabled(true); + album_cover_choice_controller_->cover_from_url_action()->setEnabled(true); + album_cover_choice_controller_->search_cover_auto_action()->setEnabled(true); + album_cover_choice_controller_->unset_cover_action()->setEnabled(true); + album_cover_choice_controller_->clear_cover_action()->setEnabled(true); + album_cover_choice_controller_->delete_cover_action()->setEnabled(true); + } + else { + ui_->tags_art->clear(); + album_cover_choice_controller_->show_cover_action()->setEnabled(first_song.has_valid_art() && !first_song.has_manually_unset_cover()); + album_cover_choice_controller_->cover_to_file_action()->setEnabled(first_song.has_valid_art() && !first_song.has_manually_unset_cover()); + album_cover_choice_controller_->cover_from_file_action()->setEnabled(true); + album_cover_choice_controller_->cover_from_url_action()->setEnabled(true); + album_cover_choice_controller_->search_cover_auto_action()->setEnabled(true); + album_cover_choice_controller_->unset_cover_action()->setEnabled(!first_song.has_manually_unset_cover()); + album_cover_choice_controller_->clear_cover_action()->setEnabled(!first_song.art_manual().isEmpty()); + album_cover_choice_controller_->delete_cover_action()->setEnabled(!first_song.art_manual().isEmpty()); + if (data_[indexes.first().row()].cover_action_ == UpdateCoverAction_None) { + tags_cover_art_id_ = app_->album_cover_loader()->LoadImageAsync(cover_options_, first_song); + } + else { + tags_cover_art_id_ = app_->album_cover_loader()->LoadImageAsync(cover_options_, data_[indexes.first().row()].cover_result_); + } + summary += GetArtSummary(first_song, first_cover_action); + } + + ui_->tags_summary->setText(summary); + ui_->tags_art_button->setEnabled(first_song.id() != -1); + + album_cover_choice_controller_->search_for_cover_action()->setEnabled(app_->cover_providers()->HasAnyProviders()); + + const bool embedded_cover = (first_song.save_embedded_cover_supported() && (first_song.has_embedded_cover() || album_cover_choice_controller_->get_save_album_cover_type() == CollectionSettingsPage::SaveCoverType_Embedded)); + ui_->checkbox_embedded_cover->setChecked(embedded_cover); + album_cover_choice_controller_->set_save_embedded_cover_override(embedded_cover); } -void EditTagDialog::UpdateUI(const QModelIndexList &sel){ +void EditTagDialog::UpdateUI(const QModelIndexList &indexes) { ignore_edits_ = true; for (const FieldData &field : fields_) { - InitFieldValue(field, sel); + InitFieldValue(field, indexes); } ignore_edits_ = false; } -static void SetText(QLabel *label, int value, const QString &suffix, const QString &def = QString()) { +void EditTagDialog::SetText(QLabel *label, const int value, const QString &suffix, const QString &def) { label->setText(value <= 0 ? def : (QString::number(value) + " " + suffix)); } -static void SetDate(QLabel *label, uint time) { +void EditTagDialog::SetDate(QLabel *label, const uint time) { if (time == std::numeric_limits::max()) { // -1 label->setText(QObject::tr("Unknown")); @@ -531,37 +696,16 @@ static void SetDate(QLabel *label, uint time) { } -void EditTagDialog::UpdateSummaryTab(const Song &song) { +void EditTagDialog::UpdateSummaryTab(const Song &song, const UpdateCoverAction cover_action) { - cover_art_id_ = app_->album_cover_loader()->LoadImageAsync(cover_options_, song); + summary_cover_art_id_ = app_->album_cover_loader()->LoadImageAsync(cover_options_, song); - QString summary = "" + song.PrettyTitleWithArtist().toHtmlEscaped() + "
"; + QString summary = "

" + song.PrettyTitleWithArtist().toHtmlEscaped() + "

"; - bool art_is_set = true; - if (song.has_manually_unset_cover()) { - summary += tr("Cover art manually unset").toHtmlEscaped(); - art_is_set = false; - } - else if (!song.art_manual().isEmpty()) { - summary += tr("Cover art set from %1").arg(song.art_manual().toString()).toHtmlEscaped(); - } - else if (song.has_embedded_cover()) { - summary += tr("Cover art from embedded image"); - } - else if (!song.art_automatic().isEmpty()) { - summary += tr("Cover art loaded automatically from %1").arg(song.art_automatic().toString()).toHtmlEscaped(); - } - else { - summary += tr("Cover art not set").toHtmlEscaped(); - art_is_set = false; - } + summary += GetArtSummary(song, cover_action); ui_->summary->setText(summary); - album_cover_choice_controller_->unset_cover_action()->setEnabled(art_is_set); - album_cover_choice_controller_->show_cover_action()->setEnabled(art_is_set); - ui_->summary_art_button->setEnabled(song.id() != -1); - ui_->length->setText(Utilities::PrettyTimeNanosec(song.length_nanosec())); SetText(ui_->samplerate, song.samplerate(), "Hz"); @@ -579,12 +723,82 @@ void EditTagDialog::UpdateSummaryTab(const Song &song) { ui_->filetype->setText(song.TextForFiletype()); - if (song.url().isLocalFile()) - ui_->filename->setText(QDir::toNativeSeparators(song.url().toLocalFile())); - else + if (song.url().isLocalFile()) { + ui_->filename->setText(song.url().fileName()); + ui_->path->setText(QFileInfo(QDir::toNativeSeparators(song.url().toLocalFile())).path()); + } + else { ui_->filename->setText(song.url().toString()); + ui_->path->clear(); + } - album_cover_choice_controller_->search_for_cover_action()->setEnabled(app_->cover_providers()->HasAnyProviders()); + if (song.art_manual().isEmpty()) { + ui_->art_manual->setText(tr("None")); + } + else if (song.has_manually_unset_cover()) { + ui_->art_manual->setText(tr("Unset")); + } + else { + ui_->art_manual->setText(song.art_manual().toString()); + } + + if (song.art_automatic().isEmpty()) { + ui_->art_automatic->setText(tr("None")); + } + else if (song.has_embedded_cover()) { + ui_->art_automatic->setText(tr("Embedded")); + } + else { + ui_->art_automatic->setText(song.art_automatic().toString()); + } + +} + +QString EditTagDialog::GetArtSummary(const Song &song, const UpdateCoverAction cover_action) { + + QString summary; + + if (cover_action != UpdateCoverAction_None) { + switch (cover_action) { + case UpdateCoverAction_Clear: + summary = tr("Cover changed: Will be cleared when saved.").toHtmlEscaped(); + break; + case UpdateCoverAction_Unset: + summary = tr("Cover changed: Will be unset when saved.").toHtmlEscaped(); + break; + case UpdateCoverAction_Delete: + summary = tr("Cover changed: Will be deleted when saved.").toHtmlEscaped(); + break; + case UpdateCoverAction_New: + summary = tr("Cover changed: Will set new when saved.").toHtmlEscaped(); + break; + case UpdateCoverAction_None: + break; + } + } + else if (song.art_manual().isEmpty() && song.art_automatic().isEmpty()) { + summary = tr("Cover art not set").toHtmlEscaped(); + } + else if (song.has_manually_unset_cover()) { + summary = tr("Cover art manually unset").toHtmlEscaped(); + } + else if (song.art_manual_is_valid()) { + summary = tr("Manually set cover art from %1").arg(song.art_manual().toString()).toHtmlEscaped(); + } + else if (song.has_embedded_cover()) { + summary = tr("Cover art from embedded image"); + } + else if (song.art_automatic_is_valid()) { + summary = tr("Cover art automatically loaded from %1").arg(song.art_automatic().toString()).toHtmlEscaped(); + } + else if (!song.art_manual().isEmpty()) { + summary = tr("Manually cover art from %1 is missing").arg(song.art_manual().toString()).toHtmlEscaped(); + } + else if (!song.art_automatic().isEmpty()) { + summary = tr("Automatically cover art from %1 is missing").arg(song.art_automatic().toString()).toHtmlEscaped(); + } + + return summary; } @@ -599,9 +813,30 @@ void EditTagDialog::UpdateStatisticsTab(const Song &song) { void EditTagDialog::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result) { - if (id == cover_art_id_) { - ui_->art->setPixmap(QPixmap::fromImage(result.image_scaled)); - original_ = result.image_original; + if (id == tags_cover_art_id_) { + ui_->tags_art->clear(); + if (result.success && !result.image_scaled.isNull() && result.type != AlbumCoverLoaderResult::Type_ManuallyUnset) { + ui_->tags_art->setPixmap(QPixmap::fromImage(result.image_scaled)); + for (const QModelIndex idx : ui_->song_list->selectionModel()->selectedIndexes()) { + data_[idx.row()].cover_result_ = result.album_cover; + } + } + else { + ui_->tags_art->setPixmap(QPixmap::fromImage(image_no_cover_thumbnail_)); + } + tags_cover_art_id_ = -1; + album_cover_choice_controller_->show_cover_action()->setEnabled(result.success && result.type != AlbumCoverLoaderResult::Type_ManuallyUnset); + album_cover_choice_controller_->cover_to_file_action()->setEnabled(result.success && result.type != AlbumCoverLoaderResult::Type_ManuallyUnset); + album_cover_choice_controller_->delete_cover_action()->setEnabled(result.success && result.type != AlbumCoverLoaderResult::Type_ManuallyUnset); + } + else if (id == summary_cover_art_id_) { + if (result.success && !result.image_scaled.isNull() && result.type != AlbumCoverLoaderResult::Type_ManuallyUnset) { + ui_->summary_art->setPixmap(QPixmap::fromImage(result.image_scaled)); + } + else { + ui_->summary_art->setPixmap(QPixmap::fromImage(image_no_cover_thumbnail_)); + } + summary_cover_art_id_ = -1; } } @@ -650,7 +885,7 @@ Song *EditTagDialog::GetFirstSelected() { const QModelIndexList sel = ui_->song_list->selectionModel()->selectedIndexes(); if (sel.isEmpty()) return nullptr; - return &data_[sel.first().row()].original_; + return &data_[sel.first().row()].current_; } @@ -659,33 +894,26 @@ void EditTagDialog::LoadCoverFromFile() { Song *song = GetFirstSelected(); if (!song) return; - const QModelIndexList sel = ui_->song_list->selectionModel()->selectedIndexes(); - - QUrl cover_url = album_cover_choice_controller_->LoadCoverFromFile(song); - - if (!cover_url.isEmpty()) UpdateCoverOf(*song, sel, cover_url); + AlbumCoverImageResult result = album_cover_choice_controller_->LoadImageFromFile(song); + if (result.is_valid()) UpdateCover(UpdateCoverAction_New, result); } void EditTagDialog::SaveCoverToFile() { - Song *song = GetFirstSelected(); - if (!song) return; + if (ui_->song_list->selectionModel()->selectedIndexes().isEmpty()) return; - album_cover_choice_controller_->SaveCoverToFileManual(*song, original_); + const Data &first_data = data_[ui_->song_list->selectionModel()->selectedIndexes().first().row()]; + album_cover_choice_controller_->SaveCoverToFileManual(first_data.current_, first_data.cover_result_); } void EditTagDialog::LoadCoverFromURL() { - Song *song = GetFirstSelected(); - if (!song) return; + if (ui_->song_list->selectionModel()->selectedIndexes().isEmpty()) return; - const QModelIndexList sel = ui_->song_list->selectionModel()->selectedIndexes(); - - QUrl cover_url = album_cover_choice_controller_->LoadCoverFromURL(song); - - if (!cover_url.isEmpty()) UpdateCoverOf(*song, sel, cover_url); + AlbumCoverImageResult result = album_cover_choice_controller_->LoadImageFromURL(); + if (result.is_valid()) UpdateCover(UpdateCoverAction_New, result); } @@ -694,11 +922,8 @@ void EditTagDialog::SearchForCover() { Song *song = GetFirstSelected(); if (!song) return; - const QModelIndexList sel = ui_->song_list->selectionModel()->selectedIndexes(); - - QUrl cover_url = album_cover_choice_controller_->SearchForCover(song); - - if (!cover_url.isEmpty()) UpdateCoverOf(*song, sel, cover_url); + AlbumCoverImageResult result = album_cover_choice_controller_->SearchForImage(song); + if (result.is_valid()) UpdateCover(UpdateCoverAction_New, result); } @@ -707,44 +932,88 @@ void EditTagDialog::UnsetCover() { Song *song = GetFirstSelected(); if (!song) return; - const QModelIndexList sel = ui_->song_list->selectionModel()->selectedIndexes(); + song->set_manually_unset_cover(); - QUrl cover_url = album_cover_choice_controller_->UnsetCover(song); - UpdateCoverOf(*song, sel, cover_url); + UpdateCover(UpdateCoverAction_Unset); + +} + +void EditTagDialog::ClearCover() { + + Song *song = GetFirstSelected(); + if (!song) return; + + song->clear_art_automatic(); + song->clear_art_manual(); + + UpdateCover(UpdateCoverAction_Clear); + +} + +void EditTagDialog::DeleteCover() { + + UpdateCover(UpdateCoverAction_Delete); } void EditTagDialog::ShowCover() { - Song *song = GetFirstSelected(); - if (!song) { - return; - } - - album_cover_choice_controller_->ShowCover(*song); + if (ui_->song_list->selectionModel()->selectedIndexes().isEmpty()) return; + const Data &first_data = data_[ui_->song_list->selectionModel()->selectedIndexes().first().row()]; + album_cover_choice_controller_->ShowCover(first_data.current_, first_data.cover_result_.image); } -void EditTagDialog::UpdateCoverOf(const Song &selected, const QModelIndexList &sel, const QUrl &cover_url) { +void EditTagDialog::UpdateCover(const UpdateCoverAction action, const AlbumCoverImageResult &result) { - if (!selected.is_valid() || selected.id() == -1) return; + const QModelIndexList indexes = ui_->song_list->selectionModel()->selectedIndexes(); + if (indexes.isEmpty()) return; - UpdateSummaryTab(selected); + QString artist = data_[indexes.first().row()].current_.effective_albumartist(); + QString album = data_[indexes.first().row()].current_.album(); + + for (const QModelIndex &idx : indexes) { + data_[idx.row()].cover_action_ = action; + data_[idx.row()].cover_result_ = result; + if (action == UpdateCoverAction_New) { + data_[idx.row()].current_.clear_art_manual(); + } + else if (action == UpdateCoverAction_Unset) { + data_[idx.row()].current_.set_manually_unset_cover(); + } + else if (action == UpdateCoverAction_Clear || action == UpdateCoverAction_Delete) { + data_[idx.row()].current_.clear_art_manual(); + data_[idx.row()].current_.clear_art_automatic(); + } + if (artist != data_[idx.row()].current_.effective_albumartist() || album != data_[idx.row()].current_.effective_albumartist()) { + artist.clear(); + album.clear(); + } + } // Now check if we have any other songs cached that share that artist and album (and would therefore be changed as well) - for (int i = 0; i < data_.count(); ++i) { - - if (i != sel.first().row()) { - Song *other_song = &data_[i].original_; - if (selected.effective_albumartist() == other_song->effective_albumartist() && selected.album() == other_song->album()) { - other_song->set_art_manual(cover_url); + if (!artist.isEmpty() && !album.isEmpty()) { + for (int i = 0; i < data_.count(); ++i) { + if (data_[i].current_.effective_albumartist() == artist && data_[i].current_.album() == album) { + data_[i].cover_action_ = action; + data_[i].cover_result_ = result; + if (action == UpdateCoverAction_New) { + data_[i].current_.clear_art_manual(); + } + else if (action == UpdateCoverAction_Unset) { + data_[i].current_.set_manually_unset_cover(); + } + else if (action == UpdateCoverAction_Clear || action == UpdateCoverAction_Delete) { + data_[i].current_.clear_art_manual(); + data_[i].current_.clear_art_automatic(); + } } } - - data_[i].current_.set_art_manual(data_[i].original_.art_manual()); - } + UpdateSummaryTab(data_[indexes.first().row()].current_, data_[indexes.first().row()].cover_action_); + SelectionChanged(); + } void EditTagDialog::NextSong() { @@ -777,113 +1046,125 @@ void EditTagDialog::ButtonClicked(QAbstractButton *button) { } -void EditTagDialog::SaveData(const QList &tag_data) { +void EditTagDialog::SaveData() { - for (int i = 0; i < tag_data.count(); ++i) { - const Data &ref = tag_data[i]; - if (ref.current_.IsMetadataEqual(ref.original_)) continue; + QUrl new_cover_url; - ++pending_; - TagReaderReply *reply = TagReaderClient::Instance()->SaveFile(ref.current_.url().toLocalFile(), ref.current_); - QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, ref]() { SongSaveComplete(reply, ref.current_.url().toLocalFile(), ref.current_); }); + for (int i = 0; i < data_.count(); ++i) { + Data &ref = data_[i]; + if (!ref.current_.IsMetadataEqual(ref.original_)) { + ++save_tag_pending_; + TagReaderReply *reply = TagReaderClient::Instance()->SaveFile(ref.current_.url().toLocalFile(), ref.current_); + QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, ref]() { SongSaveTagsComplete(reply, ref.current_.url().toLocalFile(), ref.current_); }, Qt::QueuedConnection); + } + + // If embedded album cover is selected and it isn't saved to the tags, then save it even if no action was done. + if (ui_->checkbox_embedded_cover->isChecked() && ref.cover_action_ == UpdateCoverAction_None && !ref.original_.has_embedded_cover() && ref.original_.save_embedded_cover_supported() && + ( + (ref.original_.art_automatic().isValid() && + ref.original_.art_automatic().isLocalFile() && + QFile::exists(ref.original_.art_automatic().toLocalFile())) + || + (ref.original_.art_manual().isValid() && + ref.original_.art_manual().isLocalFile() && + QFile::exists(ref.original_.art_manual().toLocalFile())) + )) { + ref.cover_action_ = UpdateCoverAction_New; + ref.current_.clear_art_automatic(); + ref.current_.clear_art_manual(); + } + + if (ref.cover_action_ != UpdateCoverAction_None) { + switch (ref.cover_action_) { + case UpdateCoverAction_None: + break; + case UpdateCoverAction_New:{ + if ((!ui_->checkbox_embedded_cover->isChecked() || !ref.original_.save_embedded_cover_supported()) && new_cover_url.isEmpty()) { + new_cover_url = album_cover_choice_controller_->SaveCoverToFileAutomatic(&ref.current_, ref.cover_result_); + } + ref.current_.set_art_manual(new_cover_url); + break; + } + case UpdateCoverAction_Unset: + ref.current_.set_manually_unset_cover(); + break; + case UpdateCoverAction_Clear: + ref.current_.clear_art_manual(); + break; + case UpdateCoverAction_Delete:{ + if (ref.original_.art_automatic().isValid() && + ref.original_.art_automatic().isLocalFile() && + QFile::exists(ref.original_.art_automatic().toLocalFile())) { + QFile::remove(ref.original_.art_automatic().toLocalFile()); + ref.current_.clear_art_manual(); + } + if (ref.original_.art_manual().isValid() && + ref.original_.art_manual().isLocalFile() && + QFile::exists(ref.original_.art_manual().toLocalFile())) { + QFile::remove(ref.original_.art_manual().toLocalFile()); + ref.current_.clear_art_manual(); + } + break; + } + } + if (ui_->checkbox_embedded_cover->isChecked() && ref.original_.save_embedded_cover_supported()) { + if (ref.cover_action_ != UpdateCoverAction_Clear && ref.cover_action_ != UpdateCoverAction_Unset) { + ++save_art_pending_; + if (ref.cover_action_ == UpdateCoverAction_Delete && ref.cover_result_.is_valid()) ref.cover_result_ = AlbumCoverImageResult(); + if (ref.cover_result_.is_jpeg() || ref.cover_result_.image_data.isNull()) { + TagReaderReply *reply = TagReaderClient::Instance()->SaveEmbeddedArt(ref.current_.url().toLocalFile(), ref.cover_result_.image_data); + QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, ref]() { + SongSaveArtComplete(reply, ref.current_.url().toLocalFile(), ref.current_, ref.cover_action_); + }, Qt::QueuedConnection); + } + else { + QFuture future = QtConcurrent::run(&ImageUtils::SaveImageToJpegData, ref.cover_result_.image); + QFutureWatcher *watcher = new QFutureWatcher(); + watcher->setFuture(future); + QObject::connect(watcher, &QFutureWatcher::finished, this, [=]() { + TagReaderReply *reply = TagReaderClient::Instance()->SaveEmbeddedArt(ref.current_.url().toLocalFile(), watcher->result()); + QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, ref]() { + SongSaveArtComplete(reply, ref.current_.url().toLocalFile(), ref.current_, ref.cover_action_); + }, Qt::QueuedConnection); + watcher->deleteLater(); + }); + } + } + } + else { + if (ref.current_.is_collection_song()) { + collection_songs_.insert(ref.current_.id(), ref.current_); + } + if (ref.current_ == app_->current_albumcover_loader()->last_song()) { + app_->current_albumcover_loader()->LoadAlbumCover(ref.current_); + } + } + } } - if (pending_ <= 0) AcceptFinished(); - -} - -void EditTagDialog::accept() { - - // Show the loading indicator - if (!SetLoading(tr("Saving tracks") + "...")) return; - - SaveData(data_); + if (save_tag_pending_ <= 0 && save_art_pending_ <= 0) AcceptFinished(); } void EditTagDialog::AcceptFinished() { + + if (!collection_songs_.isEmpty()) { + app_->collection_backend()->AddOrUpdateSongsAsync(collection_songs_.values()); + collection_songs_.clear(); + } + if (!SetLoading(QString())) return; + QDialog::accept(); -} - -bool EditTagDialog::eventFilter(QObject *o, QEvent *e) { - - if (o == ui_->art) { - switch (e->type()) { - case QEvent::MouseButtonRelease: -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) - cover_menu_->popup(static_cast(e)->globalPosition().toPoint()); -#else - cover_menu_->popup(static_cast(e)->globalPos()); -#endif - break; - - case QEvent::DragEnter: { - QDragEnterEvent *event = static_cast(e); - if (AlbumCoverChoiceController::CanAcceptDrag(event)) { - event->acceptProposedAction(); - } - break; - } - - case QEvent::Drop: { - const QDropEvent *event = static_cast(e); - const QModelIndexList sel = ui_->song_list->selectionModel()->selectedIndexes(); - Song *song = GetFirstSelected(); - - const QUrl cover_url = album_cover_choice_controller_->SaveCover(song, event); - if (!cover_url.isEmpty()) { - UpdateCoverOf(*song, sel, cover_url); - } - - break; - } - - default: - break; - } - } - return false; - -} - -void EditTagDialog::showEvent(QShowEvent *e) { - - // Set the dialog's height to the smallest possible - resize(width(), sizeHint().height()); - - // Restore the tab that was current last time. - QSettings s; - s.beginGroup(kSettingsGroup); - if (s.contains("geometry")) { - restoreGeometry(s.value("geometry").toByteArray()); - } - ui_->tab_widget->setCurrentIndex(s.value("current_tab").toInt()); - s.endGroup(); - - QDialog::showEvent(e); - -} - -void EditTagDialog::hideEvent(QHideEvent *e) { - - // Save the current tab - QSettings s; - s.beginGroup(kSettingsGroup); - s.setValue("geometry", saveGeometry()); - s.setValue("current_tab", ui_->tab_widget->currentIndex()); - s.endGroup(); - - QDialog::hideEvent(e); } void EditTagDialog::ResetPlayCounts() { const QModelIndexList sel = ui_->song_list->selectionModel()->selectedIndexes(); - if (sel.isEmpty()) - return; + if (sel.isEmpty()) return; + Song *song = &data_[sel.first().row()].original_; if (!song->is_valid() || song->id() == -1) return; @@ -895,8 +1176,9 @@ void EditTagDialog::ResetPlayCounts() { song->set_skipcount(0); song->set_lastplayed(-1); - if (song->is_collection_song()) + if (song->is_collection_song()) { app_->collection_backend()->ResetStatisticsAsync(song->id()); + } UpdateStatisticsTab(*song); @@ -915,7 +1197,6 @@ void EditTagDialog::FetchTag() { if (!song.is_valid()) { continue; } - songs << song; } @@ -966,20 +1247,67 @@ void EditTagDialog::FetchTagSongChosen(const Song &original_song, const Song &ne } -void EditTagDialog::SongSaveComplete(TagReaderReply *reply, const QString &filename, const Song &song) { +void EditTagDialog::SongSaveTagsComplete(TagReaderReply *reply, const QString &filename, Song song) { - --pending_; + --save_tag_pending_; if (!reply->message().save_file_response().success()) { QString message = tr("An error occurred writing metadata to '%1'").arg(filename); emit Error(message); } else if (song.is_collection_song()) { - app_->collection_backend()->AddOrUpdateSongs(SongList() << song); + if (collection_songs_.contains(song.id())) { + Song old_song = collection_songs_.take(song.id()); + song.set_art_automatic(old_song.art_automatic()); + song.set_art_manual(old_song.art_manual()); + } + collection_songs_.insert(song.id(), song); } - if (pending_ <= 0) AcceptFinished(); + metaObject()->invokeMethod(reply, "deleteLater", Qt::QueuedConnection); - reply->deleteLater(); + if (save_tag_pending_ <= 0 && save_art_pending_ <= 0) AcceptFinished(); + +} + +void EditTagDialog::SongSaveArtComplete(TagReaderReply *reply, const QString &filename, Song song, const UpdateCoverAction cover_action) { + + --save_art_pending_; + + if (!reply->message().save_embedded_art_response().success()) { + QString message = tr("An error occurred writing cover art to '%1'").arg(filename); + emit Error(message); + } + else if (song.is_collection_song()) { + if (collection_songs_.contains(song.id())) { + song = collection_songs_.take(song.id()); + } + switch (cover_action) { + case UpdateCoverAction_None: + break; + case UpdateCoverAction_New: + song.clear_art_manual(); + song.set_embedded_cover(); + break; + case UpdateCoverAction_Clear: + case UpdateCoverAction_Delete: + song.clear_art_automatic(); + song.clear_art_manual(); + break; + case UpdateCoverAction_Unset: + song.clear_art_automatic(); + song.set_manually_unset_cover(); + break; + } + collection_songs_.insert(song.id(), song); + } + + if (song == app_->current_albumcover_loader()->last_song()) { + app_->current_albumcover_loader()->LoadAlbumCover(song); + } + + metaObject()->invokeMethod(reply, "deleteLater", Qt::QueuedConnection); + + if (save_tag_pending_ <= 0 && save_art_pending_ <= 0) AcceptFinished(); } diff --git a/src/dialogs/edittagdialog.h b/src/dialogs/edittagdialog.h index d3e66c64..daca8f81 100644 --- a/src/dialogs/edittagdialog.h +++ b/src/dialogs/edittagdialog.h @@ -28,10 +28,11 @@ #include #include #include -#include #include #include #include +#include +#include #include #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 LoadData(const SongList &songs) const; - void SaveData(const QList &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 collection_songs_; }; #endif // EDITTAGDIALOG_H diff --git a/src/dialogs/edittagdialog.ui b/src/dialogs/edittagdialog.ui index a2835936..550e8955 100644 --- a/src/dialogs/edittagdialog.ui +++ b/src/dialogs/edittagdialog.ui @@ -6,8 +6,8 @@ 0 0 - 863 - 671 + 800 + 843 @@ -32,47 +32,85 @@ 1 - + + + + 0 + 0 + + Summary + + QLayout::SetMinimumSize + - + + + QLayout::SetMinimumSize + - - - - 0 - 0 - + + + QLayout::SetMinimumSize - - - 124 - 124 - - - - QFrame::StyledPanel - - - - - - Qt::AlignCenter - - - 2 - - + + + + + 0 + 0 + + + + + 128 + 128 + + + + QFrame::StyledPanel + + + + + + Qt::AlignHCenter|Qt::AlignTop + + + 2 + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 1 + + + + + - + + + QLayout::SetMinimumSize + - + 0 0 @@ -85,54 +123,18 @@ QFrame::NoFrame + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Noto Sans'; font-size:10pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - 0 - 0 - - - - Change cover art - - - - - - - - 0 - 0 - - - - Reset play counts - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - @@ -145,18 +147,22 @@ - - - 18 + + + QLayout::SetMinimumSize - - - - - 0 - 0 - + + + + true + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + Length @@ -165,260 +171,8 @@ - - - - - 0 - 0 - - - - - 150 - 0 - - - - - 150 - 0 - - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - 0 - 0 - - - - Play count - - - true - - - - - - - - 0 - 0 - - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - 0 - 0 - - - - Skip count - - - true - - - - - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - 0 - 0 - - - - Bit rate - - - true - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - 0 - 0 - - - - Last played - - - true - - - - - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - 0 - 0 - - - - Sample rate - - - true - - - - - - - - 0 - 0 - - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - 0 - 0 - - - - Bit depth - - - true - - - - - - - - 0 - 0 - - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - 0 - 0 - - - - File size - - - true - - - - - - - - 0 - 0 - - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - 0 - 0 - - File type @@ -427,14 +181,61 @@ - - - - - 0 - 0 - + + + + Bit depth + + true + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Filename + + + true + + + true + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + @@ -446,14 +247,28 @@ + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Skip count + + + true + + + - - - 0 - 0 - - Date modified @@ -462,7 +277,17 @@ - + + + + Play count + + + true + + + + true @@ -472,14 +297,8 @@ - + - - - 0 - 0 - - Date created @@ -488,8 +307,15 @@ - - + + + + Path + + + + + true @@ -498,17 +324,17 @@ - - + + - File name + Sample rate true - + QLineEdit { @@ -523,374 +349,671 @@ + + + + Last played + + + true + + + + + + + + 0 + 0 + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + QLineEdit { + background: transparent; +} + + + false + + + true + + + + + + + + 150 + 0 + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Art Manual + + + + + + + Bit rate + + + true + + + + + + + File size + + + true + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Art Automatic + + + + + + + QLineEdit { + background: transparent; +} + + + false + + + + + + + QLineEdit { + background: transparent; +} + + + false + + + - + + + + 0 + 0 + + + + Reset play counts + + + + + Qt::Vertical + + QSizePolicy::MinimumExpanding + 20 - 40 + 1 - + - Edit tags + Tags - - - - - Complete tags automatically + + + QLayout::SetMinimumSize + + + + + QLayout::SetMinimumSize - - - :/pictures/musicbrainz.png:/pictures/musicbrainz.png + + + + QLayout::SetMinimumSize + + + + + + 0 + 0 + + + + + 128 + 128 + + + + QFrame::StyledPanel + + + + + + Qt::AlignHCenter|Qt::AlignTop + + + true + + + 2 + + + + + + + Change art + + + + + + + Embedded cover + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + + QLayout::SetMinimumSize + + + + + + 0 + 0 + + + + QTextEdit { + background: transparent; +} + + + true + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Noto Sans'; font-size:10pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> + + + + + + + + + + + QLayout::SetMinimumSize - + + + + true + + + false + + + + + + + + 80 + 0 + + + + Title + + + title + + + + + + + false + + + false + + + + + + + + 80 + 0 + + + + Genre + + + genre + + + + + + + + 0 + 0 + + + + true + + + false + + + + + + + Disc + + + disc + + + + + + + + 80 + 0 + + + + Album artist + + + albumartist + + + + + + + + 80 + 0 + + + + Compilation + + + compilation + + + + + + + true + + + false + + + + + + + + 80 + 0 + + + + Album + + + album + + + + + + + + 80 + 0 + + + + Artist + + + artist + + + + + + + + 80 + 0 + + + + Comment + + + comment + + + + + + + + 80 + 0 + + + + Composer + + + composer + + + + + + + Complete tags automatically + + + + :/pictures/musicbrainz.png:/pictures/musicbrainz.png + + + + 38 + 22 + + + + + + + + + 80 + 0 + + + + Performer + + + performer + + + + + + + true + + + false + + + + + + + true + + + false + + + + + + + + 80 + 0 + + + + Grouping + + + grouping + + + + + + + QAbstractSpinBox::CorrectToNearestValue + + + 9999 + + + false + + + true + + + + + + + true + + + false + + + + + + + true + + + false + + + + + + + QAbstractSpinBox::CorrectToNearestValue + + + 9999 + + + false + + + true + + + + + + + true + + + false + + + + + + + true + + + false + + + + + + + QAbstractSpinBox::CorrectToNearestValue + + + 9999 + + + false + + + true + + + + + + + Track + + + track + + + + + + + Year + + + year + + + + + + + + + Qt::Vertical + + + QSizePolicy::MinimumExpanding + + - 38 - 22 + 20 + 1 - + - - - - QAbstractSpinBox::CorrectToNearestValue - - - 9999 - - - false - - - true - - - - - - - - 80 - 0 - - - - Genre - - - genre - - - - - - - true - - - false - - - - - - - true - - - false - - - - - - - - 80 - 0 - - - - Title - - - title - - - - - - - - 80 - 0 - - - - Grouping - - - grouping - - - - - - - - 80 - 0 - - - - Album - - - album - - - - - - - true - - - false - - - - - - - Disc - - - disc - - - - - - - true - - - false - - - - - - - - 80 - 0 - - - - Album artist - - - albumartist - - - - - - - true - - - false - - - - - - - - 80 - 0 - - - - Composer - - - composer - - - - - - - - 80 - 0 - - - - Comment - - - comment - - - - - - - true - - - false - - - - - - - Track - - - track - - - - - - - true - - - false - - - - - - - QAbstractSpinBox::CorrectToNearestValue - - - 9999 - - - false - - - true - - - - - - - - 80 - 0 - - - - Artist - - - artist - - - - - - - QAbstractSpinBox::CorrectToNearestValue - - - 9999 - - - false - - - true - - - - - - - true - - - false - - - - - - - Year - - - year - - - - - - - true - - - false - - - - - - - - 80 - 0 - - - - Performer - - - performer - - - - - - - - 80 - 0 - - - - Lyrics - - - lyrics - - - - + + + + + Lyrics + + + + QLayout::SetMaximumSize + + true @@ -900,29 +1023,22 @@ - - - + + + false - - false - - - - - - + - 80 - 0 + 1 + 1 - Compilation + - compilation + lyrics @@ -973,24 +1089,30 @@ song_list tab_widget - summary - summary_art_button - playcount_reset - filename + tags_summary + tags_art_button + checkbox_embedded_cover title - track artist - disc album - year albumartist composer performer grouping genre + compilation fetch_tag - lyrics comment + track + disc + year + path + art_manual + art_automatic + summary + filename + playcount_reset + lyrics diff --git a/src/internet/internetsearchview.cpp b/src/internet/internetsearchview.cpp index 46df65ef..e7b4ec39 100644 --- a/src/internet/internetsearchview.cpp +++ b/src/internet/internetsearchview.cpp @@ -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 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); + } } } diff --git a/src/organize/organize.cpp b/src/organize/organize.cpp index bdccfabe..f35c0f3f 100644 --- a/src/organize/organize.cpp +++ b/src/organize/organize.cpp @@ -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(); } diff --git a/src/playlist/playlist.cpp b/src/playlist/playlist.cpp index 5f2cfe2c..b90cc05b 100644 --- a/src/playlist/playlist.cpp +++ b/src/playlist/playlist.cpp @@ -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(data)) { InsertItems(item_data->items_, row, play_now, enqueue_now, enqueue_next_now); } - else if (const InternetSongMimeData* internet_song_data = qobject_cast(data)) { + else if (const InternetSongMimeData *internet_song_data = qobject_cast(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(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(); } } diff --git a/src/playlist/playlistview.cpp b/src/playlist/playlistview.cpp index 70c16814..a260f13d 100644 --- a/src/playlist/playlistview.cpp +++ b/src/playlist/playlistview.cpp @@ -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()); diff --git a/src/playlist/songplaylistitem.cpp b/src/playlist/songplaylistitem.cpp index 73a6cee2..3b93c3d6 100644 --- a/src/playlist/songplaylistitem.cpp +++ b/src/playlist/songplaylistitem.cpp @@ -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) { diff --git a/src/playlist/songplaylistitem.h b/src/playlist/songplaylistitem.h index 8b0ebebd..797c3483 100644 --- a/src/playlist/songplaylistitem.h +++ b/src/playlist/songplaylistitem.h @@ -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; diff --git a/src/qobuz/qobuzrequest.cpp b/src/qobuz/qobuzrequest.cpp index 2f694d68..66477497 100644 --- a/src/qobuz/qobuzrequest.cpp +++ b/src/qobuz/qobuzrequest.cpp @@ -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 format_list = Utilities::ImageFormatsForMimeType(mimetype.toUtf8()); + QList format_list = ImageUtils::ImageFormatsForMimeType(mimetype.toUtf8()); char *format = nullptr; if (!format_list.isEmpty()) { format = format_list.first().data(); diff --git a/src/settings/collectionsettingspage.cpp b/src/settings/collectionsettingspage.cpp index 5bcc3e68..b084c7da 100644 --- a/src/settings/collectionsettingspage.cpp +++ b/src/settings/collectionsettingspage.cpp @@ -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); } diff --git a/src/settings/collectionsettingspage.h b/src/settings/collectionsettingspage.h index 8b8b9d06..206586b1 100644 --- a/src/settings/collectionsettingspage.h +++ b/src/settings/collectionsettingspage.h @@ -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; diff --git a/src/settings/collectionsettingspage.ui b/src/settings/collectionsettingspage.ui index f04095c6..0bb81767 100644 --- a/src/settings/collectionsettingspage.ui +++ b/src/settings/collectionsettingspage.ui @@ -157,10 +157,33 @@ If there are no matches then it will use the largest image in the directory.0 - - - Save album covers in album directory + + + + + + + + Save album covers in album directory + + + + + + + Save album covers in cache directory + + + + + + + Save album covers as embedded cover + + + + @@ -185,16 +208,16 @@ If there are no matches then it will use the largest image in the directory.0 - + - Use hash + Pattern - + - Use pattern + Random @@ -463,9 +486,7 @@ If there are no matches then it will use the largest image in the directory.auto_open pretty_covers show_dividers - checkbox_cover_album_dir radiobutton_cover_hash - radiobutton_cover_pattern lineedit_cover_pattern checkbox_cover_overwrite checkbox_cover_lowercase diff --git a/src/settings/settingsdialog.cpp b/src/settings/settingsdialog.cpp index c4ea807e..b13de0e2 100644 --- a/src/settings/settingsdialog.cpp +++ b/src/settings/settingsdialog.cpp @@ -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); diff --git a/src/settings/transcodersettingspage.cpp b/src/settings/transcodersettingspage.cpp index 19530c5e..4d5ffd59 100644 --- a/src/settings/transcodersettingspage.cpp +++ b/src/settings/transcodersettingspage.cpp @@ -20,6 +20,8 @@ #include "config.h" +#include + #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); diff --git a/src/settings/transcodersettingspage.h b/src/settings/transcodersettingspage.h index 9d974124..c773b905 100644 --- a/src/settings/transcodersettingspage.h +++ b/src/settings/transcodersettingspage.h @@ -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 diff --git a/src/subsonic/subsonicrequest.cpp b/src/subsonic/subsonicrequest.cpp index 589b65b7..b0be4d8f 100644 --- a/src/subsonic/subsonicrequest.cpp +++ b/src/subsonic/subsonicrequest.cpp @@ -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 format_list = Utilities::ImageFormatsForMimeType(mimetype.toUtf8()); + QList format_list = ImageUtils::ImageFormatsForMimeType(mimetype.toUtf8()); char *format = nullptr; if (!format_list.isEmpty()) { format = format_list.first().data(); diff --git a/src/tidal/tidalrequest.cpp b/src/tidal/tidalrequest.cpp index 4ece3b13..5e05927e 100644 --- a/src/tidal/tidalrequest.cpp +++ b/src/tidal/tidalrequest.cpp @@ -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 format_list = Utilities::ImageFormatsForMimeType(mimetype.toUtf8()); + QList format_list = ImageUtils::ImageFormatsForMimeType(mimetype.toUtf8()); char *format = nullptr; if (!format_list.isEmpty()) { format = format_list.first().data(); diff --git a/src/transcoder/transcodedialog.cpp b/src/transcoder/transcodedialog.cpp index 404612ba..16b7ad73 100644 --- a/src/transcoder/transcodedialog.cpp +++ b/src/transcoder/transcodedialog.cpp @@ -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() { diff --git a/src/transcoder/transcodedialog.h b/src/transcoder/transcodedialog.h index e0b7241f..88c0b9a1 100644 --- a/src/transcoder/transcodedialog.h +++ b/src/transcoder/transcodedialog.h @@ -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: diff --git a/src/widgets/playingwidget.cpp b/src/widgets/playingwidget.cpp index 48927412..c850400e 100644 --- a/src/widgets/playingwidget.cpp +++ b/src/widgets/playingwidget.cpp @@ -44,8 +44,8 @@ #include #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 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(); } diff --git a/tests/src/collectionbackend_test.cpp b/tests/src/collectionbackend_test.cpp index 2826e7a4..bc3a0809 100644 --- a/tests/src/collectionbackend_test.cpp +++ b/tests/src/collectionbackend_test.cpp @@ -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());