From 331aa382f93d4e9c34cb8c871b7c30e5a40023d1 Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Sun, 14 May 2023 11:34:55 +0200 Subject: [PATCH] Rewrite album cover loader --- data/data.qrc | 1 + data/schema/device-schema.sql | 2 + data/schema/schema-17.sql | 37 + data/schema/schema.sql | 20 +- ext/libstrawberry-tagreader/tagreaderbase.cpp | 50 +- ext/libstrawberry-tagreader/tagreaderbase.h | 19 +- .../tagreadermessages.proto | 13 +- .../tagreadertaglib.cpp | 86 +- ext/libstrawberry-tagreader/tagreadertaglib.h | 10 +- .../tagreadertagparser.cpp | 2 +- src/CMakeLists.txt | 1 + src/collection/collectionbackend.cpp | 206 +++- src/collection/collectionbackend.h | 29 +- src/collection/collectionmodel.cpp | 19 +- src/collection/collectionmodel.h | 6 +- src/collection/collectionwatcher.cpp | 67 +- src/collection/collectionwatcher.h | 14 +- src/context/contextalbum.cpp | 16 +- src/context/contextalbum.h | 4 +- src/core/database.cpp | 2 +- src/core/mainwindow.cpp | 20 +- src/core/mpris2.cpp | 8 +- src/core/song.cpp | 1052 +++++++++-------- src/core/song.h | 270 ++--- src/core/standarditemiconloader.cpp | 21 +- src/core/standarditemiconloader.h | 8 - src/core/tagreaderclient.cpp | 33 +- src/core/tagreaderclient.h | 35 +- .../albumcoverchoicecontroller.cpp | 431 +++---- src/covermanager/albumcoverchoicecontroller.h | 37 +- src/covermanager/albumcoverexporter.cpp | 9 +- src/covermanager/albumcoverexporter.h | 4 + src/covermanager/albumcoverimageresult.h | 8 +- src/covermanager/albumcoverloader.cpp | 596 ++++------ src/covermanager/albumcoverloader.h | 125 +- src/covermanager/albumcoverloaderoptions.cpp | 59 + src/covermanager/albumcoverloaderoptions.h | 54 +- src/covermanager/albumcoverloaderresult.h | 12 +- src/covermanager/albumcovermanager.cpp | 358 +++--- src/covermanager/albumcovermanager.h | 35 +- src/covermanager/albumcoversearcher.cpp | 16 +- src/covermanager/albumcoversearcher.h | 1 - src/covermanager/coverexportrunnable.cpp | 169 +-- src/covermanager/coverexportrunnable.h | 6 +- src/covermanager/coverproviders.cpp | 2 +- src/covermanager/currentalbumcoverloader.cpp | 31 +- src/covermanager/currentalbumcoverloader.h | 3 + src/dialogs/edittagdialog.cpp | 326 ++--- src/dialogs/edittagdialog.h | 23 +- src/dialogs/edittagdialog.ui | 340 +++--- src/internet/internetsearchview.cpp | 8 +- src/internet/internetsearchview.h | 2 - src/organize/organize.cpp | 4 +- src/playlist/playlist.cpp | 4 +- src/playlist/playlistview.cpp | 7 +- src/playlistparsers/xspfparser.cpp | 9 +- src/radios/radiomodel.cpp | 10 +- src/radios/radiomodel.h | 2 - src/settings/collectionsettingspage.cpp | 76 -- src/settings/collectionsettingspage.h | 1 - src/settings/collectionsettingspage.ui | 135 +-- src/settings/coverssettingspage.cpp | 233 +++- src/settings/coverssettingspage.h | 28 +- src/settings/coverssettingspage.ui | 176 ++- src/utilities/imageutils.cpp | 95 +- src/utilities/imageutils.h | 7 +- src/widgets/playingwidget.cpp | 17 +- src/widgets/playingwidget.h | 3 +- 68 files changed, 2948 insertions(+), 2565 deletions(-) create mode 100644 data/schema/schema-17.sql create mode 100644 src/covermanager/albumcoverloaderoptions.cpp diff --git a/data/data.qrc b/data/data.qrc index 2ac1879a..e95311c6 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -8,6 +8,7 @@ schema/schema-14.sql schema/schema-15.sql schema/schema-16.sql + schema/schema-17.sql schema/device-schema.sql style/strawberry.css style/smartplaylistsearchterm.css diff --git a/data/schema/device-schema.sql b/data/schema/device-schema.sql index a998b1bd..76a20b3f 100644 --- a/data/schema/device-schema.sql +++ b/data/schema/device-schema.sql @@ -59,8 +59,10 @@ CREATE TABLE device_%deviceid_songs ( compilation_off INTEGER NOT NULL DEFAULT 0, compilation_effective INTEGER NOT NULL DEFAULT 0, + art_embedded INTEGER DEFAULT 0, art_automatic TEXT, art_manual TEXT, + art_unset INTEGER DEFAULT 0, effective_albumartist TEXT, effective_originalyear INTEGER NOT NULL DEFAULT 0, diff --git a/data/schema/schema-17.sql b/data/schema/schema-17.sql new file mode 100644 index 00000000..8e7bbf6c --- /dev/null +++ b/data/schema/schema-17.sql @@ -0,0 +1,37 @@ +ALTER TABLE songs ADD COLUMN art_embedded INTEGER DEFAULT 0; + +ALTER TABLE songs ADD COLUMN art_unset INTEGER DEFAULT 0; + +ALTER TABLE subsonic_songs ADD COLUMN art_embedded INTEGER DEFAULT 0; + +ALTER TABLE subsonic_songs ADD COLUMN art_unset INTEGER DEFAULT 0; + +ALTER TABLE tidal_artists_songs ADD COLUMN art_embedded INTEGER DEFAULT 0; + +ALTER TABLE tidal_artists_songs ADD COLUMN art_unset INTEGER DEFAULT 0; + +ALTER TABLE tidal_albums_songs ADD COLUMN art_embedded INTEGER DEFAULT 0; + +ALTER TABLE tidal_albums_songs ADD COLUMN art_unset INTEGER DEFAULT 0; + +ALTER TABLE tidal_songs ADD COLUMN art_embedded INTEGER DEFAULT 0; + +ALTER TABLE tidal_songs ADD COLUMN art_unset INTEGER DEFAULT 0; + +ALTER TABLE qobuz_artists_songs ADD COLUMN art_embedded INTEGER DEFAULT 0; + +ALTER TABLE qobuz_artists_songs ADD COLUMN art_unset INTEGER DEFAULT 0; + +ALTER TABLE qobuz_albums_songs ADD COLUMN art_embedded INTEGER DEFAULT 0; + +ALTER TABLE qobuz_albums_songs ADD COLUMN art_unset INTEGER DEFAULT 0; + +ALTER TABLE qobuz_songs ADD COLUMN art_embedded INTEGER DEFAULT 0; + +ALTER TABLE qobuz_songs ADD COLUMN art_unset INTEGER DEFAULT 0; + +ALTER TABLE playlist_items ADD COLUMN art_embedded INTEGER DEFAULT 0; + +ALTER TABLE playlist_items ADD COLUMN art_unset INTEGER DEFAULT 0; + +UPDATE schema_version SET version=17; diff --git a/data/schema/schema.sql b/data/schema/schema.sql index 4e0b445e..793e2123 100644 --- a/data/schema/schema.sql +++ b/data/schema/schema.sql @@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version ( DELETE FROM schema_version; -INSERT INTO schema_version (version) VALUES (16); +INSERT INTO schema_version (version) VALUES (17); CREATE TABLE IF NOT EXISTS directories ( path TEXT NOT NULL, @@ -67,8 +67,10 @@ CREATE TABLE IF NOT EXISTS songs ( compilation_off INTEGER NOT NULL DEFAULT 0, compilation_effective INTEGER NOT NULL DEFAULT 0, + art_embedded INTEGER DEFAULT 0, art_automatic TEXT, art_manual TEXT, + art_unset INTEGER DEFAULT 0, effective_albumartist TEXT, effective_originalyear INTEGER NOT NULL DEFAULT 0, @@ -143,8 +145,10 @@ CREATE TABLE IF NOT EXISTS subsonic_songs ( compilation_off INTEGER NOT NULL DEFAULT 0, compilation_effective INTEGER NOT NULL DEFAULT 0, + art_embedded INTEGER DEFAULT 0, art_automatic TEXT, art_manual TEXT, + art_unset INTEGER DEFAULT 0, effective_albumartist TEXT, effective_originalyear INTEGER NOT NULL DEFAULT 0, @@ -219,8 +223,10 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs ( compilation_off INTEGER NOT NULL DEFAULT 0, compilation_effective INTEGER NOT NULL DEFAULT 0, + art_embedded INTEGER DEFAULT 0, art_automatic TEXT, art_manual TEXT, + art_unset INTEGER DEFAULT 0, effective_albumartist TEXT, effective_originalyear INTEGER NOT NULL DEFAULT 0, @@ -295,8 +301,10 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs ( compilation_off INTEGER NOT NULL DEFAULT 0, compilation_effective INTEGER NOT NULL DEFAULT 0, + art_embedded INTEGER DEFAULT 0, art_automatic TEXT, art_manual TEXT, + art_unset INTEGER DEFAULT 0, effective_albumartist TEXT, effective_originalyear INTEGER NOT NULL DEFAULT 0, @@ -371,8 +379,10 @@ CREATE TABLE IF NOT EXISTS tidal_songs ( compilation_off INTEGER NOT NULL DEFAULT 0, compilation_effective INTEGER NOT NULL DEFAULT 0, + art_embedded INTEGER DEFAULT 0, art_automatic TEXT, art_manual TEXT, + art_unset INTEGER DEFAULT 0, effective_albumartist TEXT, effective_originalyear INTEGER NOT NULL DEFAULT 0, @@ -447,8 +457,10 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs ( compilation_off INTEGER NOT NULL DEFAULT 0, compilation_effective INTEGER NOT NULL DEFAULT 0, + art_embedded INTEGER DEFAULT 0, art_automatic TEXT, art_manual TEXT, + art_unset INTEGER DEFAULT 0, effective_albumartist TEXT, effective_originalyear INTEGER NOT NULL DEFAULT 0, @@ -523,8 +535,10 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs ( compilation_off INTEGER NOT NULL DEFAULT 0, compilation_effective INTEGER NOT NULL DEFAULT 0, + art_embedded INTEGER DEFAULT 0, art_automatic TEXT, art_manual TEXT, + art_unset INTEGER DEFAULT 0, effective_albumartist TEXT, effective_originalyear INTEGER NOT NULL DEFAULT 0, @@ -599,8 +613,10 @@ CREATE TABLE IF NOT EXISTS qobuz_songs ( compilation_off INTEGER NOT NULL DEFAULT 0, compilation_effective INTEGER NOT NULL DEFAULT 0, + art_embedded INTEGER DEFAULT 0, art_automatic TEXT, art_manual TEXT, + art_unset INTEGER DEFAULT 0, effective_albumartist TEXT, effective_originalyear INTEGER NOT NULL DEFAULT 0, @@ -695,8 +711,10 @@ CREATE TABLE IF NOT EXISTS playlist_items ( compilation_off INTEGER DEFAULT 0, compilation_effective INTEGER DEFAULT 0, + art_embedded INTEGER DEFAULT 0, art_automatic TEXT, art_manual TEXT, + art_unset INTEGER DEFAULT 0, effective_albumartist TEXT, effective_originalyear INTEGER, diff --git a/ext/libstrawberry-tagreader/tagreaderbase.cpp b/ext/libstrawberry-tagreader/tagreaderbase.cpp index e98382be..dcbd44b1 100644 --- a/ext/libstrawberry-tagreader/tagreaderbase.cpp +++ b/ext/libstrawberry-tagreader/tagreaderbase.cpp @@ -1,5 +1,5 @@ /* This file is part of Strawberry. - Copyright 2018-2021, Jonas Kvinge + Copyright 2018-2023, 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 @@ -30,8 +30,6 @@ #include "core/logging.h" #include "tagreaderbase.h" -const std::string TagReaderBase::kEmbeddedCover = "(embedded)"; - TagReaderBase::TagReaderBase() = default; TagReaderBase::~TagReaderBase() = default; @@ -59,10 +57,10 @@ int TagReaderBase::ConvertToPOPMRating(const float rating) { } -QByteArray TagReaderBase::LoadCoverDataFromRequest(const spb::tagreader::SaveFileRequest &request) { +TagReaderBase::Cover TagReaderBase::LoadCoverFromRequest(const spb::tagreader::SaveFileRequest &request) { if (!request.has_save_cover() || !request.save_cover()) { - return QByteArray(); + return Cover(); } const QString song_filename = QString::fromUtf8(request.filename().data(), request.filename().size()); @@ -74,16 +72,16 @@ QByteArray TagReaderBase::LoadCoverDataFromRequest(const spb::tagreader::SaveFil if (request.has_cover_data()) { cover_data = QByteArray(request.cover_data().data(), request.cover_data().size()); } - bool cover_is_jpeg = false; - if (request.has_cover_is_jpeg()) { - cover_is_jpeg = request.cover_is_jpeg(); + QString cover_mime_type; + if (request.has_cover_mime_type()) { + cover_mime_type = QByteArray(request.cover_mime_type().data(), request.cover_mime_type().size()); } - return LoadCoverDataFromRequest(song_filename, cover_filename, cover_data, cover_is_jpeg); + return LoadCoverFromRequest(song_filename, cover_filename, cover_data, cover_mime_type); } -QByteArray TagReaderBase::LoadCoverDataFromRequest(const spb::tagreader::SaveEmbeddedArtRequest &request) { +TagReaderBase::Cover TagReaderBase::LoadCoverFromRequest(const spb::tagreader::SaveEmbeddedArtRequest &request) { const QString song_filename = QString::fromUtf8(request.filename().data(), request.filename().size()); QString cover_filename; @@ -94,37 +92,39 @@ QByteArray TagReaderBase::LoadCoverDataFromRequest(const spb::tagreader::SaveEmb if (request.has_cover_data()) { cover_data = QByteArray(request.cover_data().data(), request.cover_data().size()); } - bool cover_is_jpeg = false; - if (request.has_cover_is_jpeg()) { - cover_is_jpeg = request.cover_is_jpeg(); + QString cover_mime_type; + if (request.has_cover_mime_type()) { + cover_mime_type = QByteArray(request.cover_mime_type().data(), request.cover_mime_type().size()); } - return LoadCoverDataFromRequest(song_filename, cover_filename, cover_data, cover_is_jpeg); + return LoadCoverFromRequest(song_filename, cover_filename, cover_data, cover_mime_type); } -QByteArray TagReaderBase::LoadCoverDataFromRequest(const QString &song_filename, const QString &cover_filename, QByteArray cover_data, const bool cover_is_jpeg) { - - if (!cover_data.isEmpty() && cover_is_jpeg) { - qLog(Debug) << "Using cover from JPEG data for" << song_filename; - return cover_data; - } +TagReaderBase::Cover TagReaderBase::LoadCoverFromRequest(const QString &song_filename, const QString &cover_filename, QByteArray cover_data, QString cover_mime_type) { if (cover_data.isEmpty() && !cover_filename.isEmpty()) { qLog(Debug) << "Loading cover from" << cover_filename << "for" << song_filename; QFile file(cover_filename); if (!file.open(QIODevice::ReadOnly)) { qLog(Error) << "Failed to open file" << cover_filename << "for reading:" << file.errorString(); - return QByteArray(); + return Cover(); } cover_data = file.readAll(); file.close(); } if (!cover_data.isEmpty()) { - if (QMimeDatabase().mimeTypeForData(cover_data).name() == "image/jpeg") { + if (cover_mime_type.isEmpty()) { + cover_mime_type = QMimeDatabase().mimeTypeForData(cover_data).name(); + } + if (cover_mime_type == "image/jpeg") { qLog(Debug) << "Using cover from JPEG data for" << song_filename; - return cover_data; + return Cover(cover_data, cover_mime_type); + } + if (cover_mime_type == "image/png") { + qLog(Debug) << "Using cover from PNG data for" << song_filename; + return Cover(cover_data, cover_mime_type); } // Convert image to JPEG. qLog(Debug) << "Converting cover to JPEG data for" << song_filename; @@ -135,9 +135,9 @@ QByteArray TagReaderBase::LoadCoverDataFromRequest(const QString &song_filename, cover_image.save(&buffer, "JPEG"); buffer.close(); } - return cover_data; + return Cover(cover_data, "image/jpeg"); } - return QByteArray(); + return Cover(); } diff --git a/ext/libstrawberry-tagreader/tagreaderbase.h b/ext/libstrawberry-tagreader/tagreaderbase.h index 49aa7348..52fc1cbe 100644 --- a/ext/libstrawberry-tagreader/tagreaderbase.h +++ b/ext/libstrawberry-tagreader/tagreaderbase.h @@ -1,5 +1,5 @@ /* This file is part of Strawberry. - Copyright 2018-2021, Jonas Kvinge + Copyright 2018-2023, 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 @@ -36,6 +36,14 @@ class TagReaderBase { explicit TagReaderBase(); ~TagReaderBase(); + class Cover { + public: + explicit Cover(const QByteArray &_data = QByteArray(), const QString &_mime_type = QString()) : data(_data), mime_type(_mime_type) {} + QByteArray data; + QString mime_type; + QString error; + }; + virtual bool IsMediaFile(const QString &filename) const = 0; virtual bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const = 0; @@ -50,14 +58,11 @@ class TagReaderBase { static float ConvertPOPMRating(const int POPM_rating); static int ConvertToPOPMRating(const float rating); - static QByteArray LoadCoverDataFromRequest(const spb::tagreader::SaveFileRequest &request); - static QByteArray LoadCoverDataFromRequest(const spb::tagreader::SaveEmbeddedArtRequest &request); + static Cover LoadCoverFromRequest(const spb::tagreader::SaveFileRequest &request); + static Cover LoadCoverFromRequest(const spb::tagreader::SaveEmbeddedArtRequest &request); private: - static QByteArray LoadCoverDataFromRequest(const QString &song_filename, const QString &cover_filename, QByteArray cover_data, const bool cover_is_jpeg); - - protected: - static const std::string kEmbeddedCover; + static Cover LoadCoverFromRequest(const QString &song_filename, const QString &cover_filename, QByteArray cover_data, QString cover_mime_type); Q_DISABLE_COPY(TagReaderBase) }; diff --git a/ext/libstrawberry-tagreader/tagreadermessages.proto b/ext/libstrawberry-tagreader/tagreadermessages.proto index 7b5cf819..80c03835 100644 --- a/ext/libstrawberry-tagreader/tagreadermessages.proto +++ b/ext/libstrawberry-tagreader/tagreadermessages.proto @@ -69,7 +69,7 @@ message SongMetadata { optional int64 lastplayed = 29; optional int64 lastseen = 30; - optional string art_automatic = 31; + optional bool art_embedded = 31; optional float rating = 32; @@ -97,6 +97,7 @@ message IsMediaFileRequest { message IsMediaFileResponse { optional bool success = 1; + optional string error = 2; } message ReadFileRequest { @@ -105,6 +106,7 @@ message ReadFileRequest { message ReadFileResponse { optional SongMetadata metadata = 1; + optional string error = 2; } message SaveFileRequest { @@ -116,11 +118,12 @@ message SaveFileRequest { optional SongMetadata metadata = 6; optional string cover_filename = 7; optional bytes cover_data = 8; - optional bool cover_is_jpeg = 9; + optional string cover_mime_type = 9; } message SaveFileResponse { optional bool success = 1; + optional string error = 2; } message LoadEmbeddedArtRequest { @@ -129,17 +132,19 @@ message LoadEmbeddedArtRequest { message LoadEmbeddedArtResponse { optional bytes data = 1; + optional string error = 2; } message SaveEmbeddedArtRequest { optional string filename = 1; optional string cover_filename = 2; optional bytes cover_data = 3; - optional bool cover_is_jpeg = 4; + optional string cover_mime_type = 4; } message SaveEmbeddedArtResponse { optional bool success = 1; + optional string error = 2; } message SaveSongPlaycountToFileRequest { @@ -149,6 +154,7 @@ message SaveSongPlaycountToFileRequest { message SaveSongPlaycountToFileResponse { optional bool success = 1; + optional string error = 2; } message SaveSongRatingToFileRequest { @@ -158,6 +164,7 @@ message SaveSongRatingToFileRequest { message SaveSongRatingToFileResponse { optional bool success = 1; + optional string error = 2; } message Message { diff --git a/ext/libstrawberry-tagreader/tagreadertaglib.cpp b/ext/libstrawberry-tagreader/tagreadertaglib.cpp index 54ede44a..4647e1fe 100644 --- a/ext/libstrawberry-tagreader/tagreadertaglib.cpp +++ b/ext/libstrawberry-tagreader/tagreadertaglib.cpp @@ -1,6 +1,6 @@ /* This file is part of Strawberry. Copyright 2013, David Sansome - Copyright 2018-2021, Jonas Kvinge + Copyright 2018-2023, 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 @@ -287,7 +287,7 @@ bool TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta if (!pictures.isEmpty()) { for (TagLib::FLAC::Picture *picture : pictures) { if (picture->type() == TagLib::FLAC::Picture::FrontCover && picture->data().size() > 0) { - song->set_art_automatic(kEmbeddedCover); + song->set_art_embedded(true); break; } } @@ -302,7 +302,7 @@ bool TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta if (!pictures.isEmpty()) { for (TagLib::FLAC::Picture *picture : pictures) { if (picture->type() == TagLib::FLAC::Picture::FrontCover && picture->data().size() > 0) { - song->set_art_automatic(kEmbeddedCover); + song->set_art_embedded(true); break; } } @@ -362,7 +362,7 @@ bool TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta TStringToStdString(map["SYLT"].front()->toString(), song->mutable_lyrics()); } - if (map.contains("APIC")) song->set_art_automatic(kEmbeddedCover); + if (map.contains("APIC")) song->set_art_embedded(true); // Find a suitable comment tag. For now we ignore iTunNORM comments. for (uint i = 0; i < map["COMM"].size(); ++i) { @@ -469,7 +469,7 @@ bool TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta // Find album cover art if (mp4_tag->item("covr").isValid()) { - song->set_art_automatic(kEmbeddedCover); + song->set_art_embedded(true); } if (mp4_tag->item("disk").isValid()) { @@ -715,8 +715,8 @@ void TagReaderTagLib::ParseOggTag(const TagLib::Ogg::FieldListMap &map, QString if (map.contains("DISCNUMBER")) *disc = TStringToQString(map["DISCNUMBER"].front()).trimmed(); if (map.contains("COMPILATION")) *compilation = TStringToQString(map["COMPILATION"].front()).trimmed(); - if (map.contains("COVERART")) song->set_art_automatic(kEmbeddedCover); - if (map.contains("METADATA_BLOCK_PICTURE")) song->set_art_automatic(kEmbeddedCover); + if (map.contains("COVERART")) song->set_art_embedded(true); + if (map.contains("METADATA_BLOCK_PICTURE")) song->set_art_embedded(true); if (map.contains("FMPS_PLAYCOUNT") && song->playcount() <= 0) { const int playcount = TStringToQString(map["FMPS_PLAYCOUNT"].front()).trimmed().toInt(); @@ -753,7 +753,7 @@ void TagReaderTagLib::ParseAPETag(const TagLib::APE::ItemListMap &map, QString * } } - if (map.find("COVER ART (FRONT)") != map.end()) song->set_art_automatic(kEmbeddedCover); + if (map.find("COVER ART (FRONT)") != map.end()) song->set_art_embedded(true); if (map.contains("COMPILATION")) { *compilation = TStringToQString(TagLib::String::number(map["COMPILATION"].toString().toInt())); } @@ -853,7 +853,7 @@ bool TagReaderTagLib::SaveFile(const spb::tagreader::SaveFileRequest &request) c qLog(Debug) << "Saving" << save_tags_options.join(", ") << "to" << filename; - const QByteArray cover_data = LoadCoverDataFromRequest(request); + const Cover cover = LoadCoverFromRequest(request); std::unique_ptr fileref(factory_->GetFileRef(filename)); if (!fileref || fileref->isNull()) return false; @@ -883,7 +883,7 @@ bool TagReaderTagLib::SaveFile(const spb::tagreader::SaveFileRequest &request) c SetRating(xiph_comment, song); } if (save_cover) { - SetEmbeddedArt(file_flac, xiph_comment, cover_data); + SetEmbeddedArt(file_flac, xiph_comment, cover.data, cover.mime_type); } } @@ -949,7 +949,7 @@ bool TagReaderTagLib::SaveFile(const spb::tagreader::SaveFileRequest &request) c SetRating(tag, song); } if (save_cover) { - SetEmbeddedArt(file_mpeg, tag, cover_data); + SetEmbeddedArt(file_mpeg, tag, cover.data, cover.mime_type); } } @@ -971,7 +971,7 @@ bool TagReaderTagLib::SaveFile(const spb::tagreader::SaveFileRequest &request) c SetRating(tag, song); } if (save_cover) { - SetEmbeddedArt(file_mp4, tag, cover_data); + SetEmbeddedArt(file_mp4, tag, cover.data, cover.mime_type); } } @@ -989,20 +989,20 @@ bool TagReaderTagLib::SaveFile(const spb::tagreader::SaveFileRequest &request) c SetRating(xiph_comment, song); } if (save_cover) { - SetEmbeddedArt(xiph_comment, cover_data); + SetEmbeddedArt(xiph_comment, cover.data, cover.mime_type); } } } - const bool result = fileref->save(); + const bool success = fileref->save(); #ifdef Q_OS_LINUX - if (result) { + if (success) { // Linux: inotify doesn't seem to notice the change to the file unless we change the timestamps as well. (this is what touch does) utimensat(0, QFile::encodeName(filename).constData(), nullptr, 0); } #endif // Q_OS_LINUX - return result; + return success; } @@ -1238,7 +1238,7 @@ QByteArray TagReaderTagLib::LoadEmbeddedAPEArt(const TagLib::APE::ItemListMap &m } -void TagReaderTagLib::SetEmbeddedArt(TagLib::FLAC::File *flac_file, TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data) const { +void TagReaderTagLib::SetEmbeddedArt(TagLib::FLAC::File *flac_file, TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data, const QString &mime_type) const { (void)xiph_comment; @@ -1247,28 +1247,28 @@ void TagReaderTagLib::SetEmbeddedArt(TagLib::FLAC::File *flac_file, TagLib::Ogg: if (!data.isEmpty()) { TagLib::FLAC::Picture *picture = new TagLib::FLAC::Picture(); picture->setType(TagLib::FLAC::Picture::FrontCover); - picture->setMimeType("image/jpeg"); + picture->setMimeType(QStringToTString(mime_type)); picture->setData(TagLib::ByteVector(data.constData(), data.size())); flac_file->addPicture(picture); } } -void TagReaderTagLib::SetEmbeddedArt(TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data) const { +void TagReaderTagLib::SetEmbeddedArt(TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data, const QString &mime_type) const { xiph_comment->removeAllPictures(); if (!data.isEmpty()) { TagLib::FLAC::Picture *picture = new TagLib::FLAC::Picture(); picture->setType(TagLib::FLAC::Picture::FrontCover); - picture->setMimeType("image/jpeg"); + picture->setMimeType(QStringToTString(mime_type)); picture->setData(TagLib::ByteVector(data.constData(), data.size())); xiph_comment->addPicture(picture); } } -void TagReaderTagLib::SetEmbeddedArt(TagLib::MPEG::File *file_mp3, TagLib::ID3v2::Tag *tag, const QByteArray &data) const { +void TagReaderTagLib::SetEmbeddedArt(TagLib::MPEG::File *file_mp3, TagLib::ID3v2::Tag *tag, const QByteArray &data, const QString &mime_type) const { (void)file_mp3; @@ -1284,14 +1284,14 @@ void TagReaderTagLib::SetEmbeddedArt(TagLib::MPEG::File *file_mp3, TagLib::ID3v2 TagLib::ID3v2::AttachedPictureFrame *frontcover = nullptr; frontcover = new TagLib::ID3v2::AttachedPictureFrame("APIC"); frontcover->setType(TagLib::ID3v2::AttachedPictureFrame::FrontCover); - frontcover->setMimeType("image/jpeg"); + frontcover->setMimeType(QStringToTString(mime_type)); frontcover->setPicture(TagLib::ByteVector(data.constData(), data.size())); tag->addFrame(frontcover); } } -void TagReaderTagLib::SetEmbeddedArt(TagLib::MP4::File *aac_file, TagLib::MP4::Tag *tag, const QByteArray &data) const { +void TagReaderTagLib::SetEmbeddedArt(TagLib::MP4::File *aac_file, TagLib::MP4::Tag *tag, const QByteArray &data, const QString &mime_type) const { (void)aac_file; @@ -1300,7 +1300,17 @@ void TagReaderTagLib::SetEmbeddedArt(TagLib::MP4::File *aac_file, TagLib::MP4::T if (tag->contains("covr")) tag->removeItem("covr"); } else { - covers.append(TagLib::MP4::CoverArt(TagLib::MP4::CoverArt::JPEG, TagLib::ByteVector(data.constData(), data.size()))); + TagLib::MP4::CoverArt::Format cover_format; + if (mime_type == "image/jpeg") { + cover_format = TagLib::MP4::CoverArt::Format::JPEG; + } + else if (mime_type == "image/png") { + cover_format = TagLib::MP4::CoverArt::Format::PNG; + } + else { + return; + } + covers.append(TagLib::MP4::CoverArt(cover_format, TagLib::ByteVector(data.constData(), data.size()))); tag->setItem("covr", covers); } @@ -1314,7 +1324,7 @@ bool TagReaderTagLib::SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtReque qLog(Debug) << "Saving art to" << filename; - const QByteArray cover_data = LoadCoverDataFromRequest(request); + const Cover cover = LoadCoverFromRequest(request); #ifdef Q_OS_WIN32 TagLib::FileRef fileref(filename.toStdWString().c_str()); @@ -1328,40 +1338,40 @@ bool TagReaderTagLib::SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtReque if (TagLib::FLAC::File *flac_file = dynamic_cast(fileref.file())) { TagLib::Ogg::XiphComment *xiph_comment = flac_file->xiphComment(true); if (!xiph_comment) return false; - SetEmbeddedArt(flac_file, xiph_comment, cover_data); + SetEmbeddedArt(flac_file, xiph_comment, cover.data, cover.mime_type); } // Ogg Vorbis / Opus / Speex else if (TagLib::Ogg::XiphComment *xiph_comment = dynamic_cast(fileref.file()->tag())) { - SetEmbeddedArt(xiph_comment, cover_data); + SetEmbeddedArt(xiph_comment, cover.data, cover.mime_type); } // MP3 else if (TagLib::MPEG::File *file_mp3 = dynamic_cast(fileref.file())) { TagLib::ID3v2::Tag *tag = file_mp3->ID3v2Tag(); if (!tag) return false; - SetEmbeddedArt(file_mp3, tag, cover_data); + SetEmbeddedArt(file_mp3, tag, cover.data, cover.mime_type); } // MP4/AAC else if (TagLib::MP4::File *aac_file = dynamic_cast(fileref.file())) { TagLib::MP4::Tag *tag = aac_file->tag(); if (!tag) return false; - SetEmbeddedArt(aac_file, tag, cover_data); + SetEmbeddedArt(aac_file, tag, cover.data, cover.mime_type); } // Not supported. else return false; - const bool result = fileref.file()->save(); + const bool success = fileref.file()->save(); #ifdef Q_OS_LINUX - if (result) { + if (success) { // Linux: inotify doesn't seem to notice the change to the file unless we change the timestamps as well. (this is what touch does) utimensat(0, QFile::encodeName(filename).constData(), nullptr, 0); } #endif // Q_OS_LINUX - return result; + return success; } @@ -1490,15 +1500,15 @@ bool TagReaderTagLib::SaveSongPlaycountToFile(const QString &filename, const spb return true; } - bool ret = fileref->save(); + bool success = fileref->save(); #ifdef Q_OS_LINUX - if (ret) { + if (success) { // Linux: inotify doesn't seem to notice the change to the file unless we change the timestamps as well. (this is what touch does) utimensat(0, QFile::encodeName(filename).constData(), nullptr, 0); } #endif // Q_OS_LINUX - return ret; + return success; } @@ -1602,14 +1612,14 @@ bool TagReaderTagLib::SaveSongRatingToFile(const QString &filename, const spb::t return true; } - bool ret = fileref->save(); + const bool success = fileref->save(); #ifdef Q_OS_LINUX - if (ret) { + if (success) { // Linux: inotify doesn't seem to notice the change to the file unless we change the timestamps as well. (this is what touch does) utimensat(0, QFile::encodeName(filename).constData(), nullptr, 0); } #endif // Q_OS_LINUX - return ret; + return success; } diff --git a/ext/libstrawberry-tagreader/tagreadertaglib.h b/ext/libstrawberry-tagreader/tagreadertaglib.h index 8522cc40..bde69646 100644 --- a/ext/libstrawberry-tagreader/tagreadertaglib.h +++ b/ext/libstrawberry-tagreader/tagreadertaglib.h @@ -1,6 +1,6 @@ /* This file is part of Strawberry. Copyright 2013, David Sansome - Copyright 2018-2021, Jonas Kvinge + Copyright 2018-2023, 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 @@ -96,10 +96,10 @@ class TagReaderTagLib : public TagReaderBase { void SetRating(TagLib::MP4::Tag *tag, const spb::tagreader::SongMetadata &song) const; void SetRating(TagLib::ASF::Tag *tag, const spb::tagreader::SongMetadata &song) const; - void SetEmbeddedArt(TagLib::FLAC::File *flac_file, TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data) const; - void SetEmbeddedArt(TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data) const; - void SetEmbeddedArt(TagLib::MPEG::File *file_mp3, TagLib::ID3v2::Tag *tag, const QByteArray &data) const; - void SetEmbeddedArt(TagLib::MP4::File *aac_file, TagLib::MP4::Tag *tag, const QByteArray &data) const; + void SetEmbeddedArt(TagLib::FLAC::File *flac_file, TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data, const QString &mime_type) const; + void SetEmbeddedArt(TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data, const QString &mime_type) const; + void SetEmbeddedArt(TagLib::MPEG::File *file_mp3, TagLib::ID3v2::Tag *tag, const QByteArray &data, const QString &mime_type) const; + void SetEmbeddedArt(TagLib::MP4::File *aac_file, TagLib::MP4::Tag *tag, const QByteArray &data, const QString &mime_type) const; private: FileRefFactory *factory_; diff --git a/ext/libstrawberry-tagreader/tagreadertagparser.cpp b/ext/libstrawberry-tagreader/tagreadertagparser.cpp index 9cd8ae9d..51a783f0 100644 --- a/ext/libstrawberry-tagreader/tagreadertagparser.cpp +++ b/ext/libstrawberry-tagreader/tagreadertagparser.cpp @@ -224,7 +224,7 @@ bool TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM song->set_track(tag->value(TagParser::KnownField::TrackPosition).toInteger()); song->set_disc(tag->value(TagParser::KnownField::DiskPosition).toInteger()); if (!tag->value(TagParser::KnownField::Cover).empty() && tag->value(TagParser::KnownField::Cover).dataSize() > 0) { - song->set_art_automatic(kEmbeddedCover); + song->set_art_embedded(true); } const float rating = ConvertPOPMRating(tag->value(TagParser::KnownField::Rating)); if (song->rating() <= 0 && rating > 0.0 && rating <= 1.0) { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2d4a28da..e5ac69e6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -147,6 +147,7 @@ set(SOURCES covermanager/albumcovermanager.cpp covermanager/albumcovermanagerlist.cpp covermanager/albumcoverloader.cpp + covermanager/albumcoverloaderoptions.cpp covermanager/albumcoverfetcher.cpp covermanager/albumcoverfetchersearch.cpp covermanager/albumcoversearcher.cpp diff --git a/src/collection/collectionbackend.cpp b/src/collection/collectionbackend.cpp index 4cd1b068..95dbcf05 100644 --- a/src/collection/collectionbackend.cpp +++ b/src/collection/collectionbackend.cpp @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2023, 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 @@ -1435,7 +1435,7 @@ CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist, QSqlDatabase db(db_->Connect()); CollectionQuery query(db, songs_table_, fts_table_, opt); - query.SetColumnSpec("url, effective_albumartist, album, compilation_effective, art_automatic, art_manual, filetype, cue_path"); + query.SetColumnSpec("url, filetype, cue_path, effective_albumartist, album, compilation_effective, art_embedded, art_automatic, art_manual, art_unset"); query.SetOrderBy("effective_albumartist, album, url"); if (compilation_required) { @@ -1453,42 +1453,48 @@ CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist, QMap albums; while (query.Next()) { - bool is_compilation = query.Value(3).toBool(); - Album info; + Album album_info; QUrl url = QUrl::fromEncoded(query.Value(0).toByteArray()); + + album_info.filetype = static_cast(query.Value(1).toInt()); + const QString filetype = Song::TextForFiletype(album_info.filetype); + album_info.cue_path = query.Value(2).toString(); + + const bool is_compilation = query.Value(5).toBool(); if (!is_compilation) { - info.album_artist = query.Value(1).toString(); + album_info.album_artist = query.Value(3).toString(); } - info.album = query.Value(2).toString(); - QString art_automatic = query.Value(4).toString(); + album_info.album = query.Value(4).toString(); + + album_info.art_embedded = query.Value(6).toBool(); + + const QString art_automatic = query.Value(7).toString(); if (art_automatic.contains(QRegularExpression("..+:.*"))) { - info.art_automatic = QUrl::fromEncoded(art_automatic.toUtf8()); + album_info.art_automatic = QUrl::fromEncoded(art_automatic.toUtf8()); } else { - info.art_automatic = QUrl::fromLocalFile(art_automatic); + album_info.art_automatic = QUrl::fromLocalFile(art_automatic); } - QString art_manual = query.Value(5).toString(); + const QString art_manual = query.Value(8).toString(); if (art_manual.contains(QRegularExpression("..+:.*"))) { - info.art_manual = QUrl::fromEncoded(art_manual.toUtf8()); + album_info.art_manual = QUrl::fromEncoded(art_manual.toUtf8()); } else { - info.art_manual = QUrl::fromLocalFile(art_manual); + album_info.art_manual = QUrl::fromLocalFile(art_manual); } - info.filetype = static_cast(query.Value(6).toInt()); - QString filetype = Song::TextForFiletype(info.filetype); - info.cue_path = query.Value(7).toString(); + album_info.art_unset = query.Value(9).toBool(); QString key; - if (!info.album_artist.isEmpty()) { - key.append(info.album_artist); + if (!album_info.album_artist.isEmpty()) { + key.append(album_info.album_artist); } - if (!info.album.isEmpty()) { + if (!album_info.album.isEmpty()) { if (!key.isEmpty()) key.append("-"); - key.append(info.album); + key.append(album_info.album); } if (!filetype.isEmpty()) { key.append(filetype); @@ -1500,8 +1506,8 @@ CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist, albums[key].urls.append(url); } else { - info.urls << url; - albums.insert(key, info); + album_info.urls << url; + albums.insert(key, album_info); } } @@ -1520,7 +1526,7 @@ CollectionBackend::Album CollectionBackend::GetAlbumArt(const QString &effective ret.album_artist = effective_albumartist; CollectionQuery query(db, songs_table_, fts_table_); - query.SetColumnSpec("art_automatic, art_manual, url"); + query.SetColumnSpec("url, art_embedded, art_automatic, art_manual, art_unset"); if (!effective_albumartist.isEmpty()) { query.AddWhere("effective_albumartist", effective_albumartist); } @@ -1532,22 +1538,24 @@ CollectionBackend::Album CollectionBackend::GetAlbumArt(const QString &effective } if (query.Next()) { - ret.art_automatic = QUrl::fromEncoded(query.Value(0).toByteArray()); - ret.art_manual = QUrl::fromEncoded(query.Value(1).toByteArray()); - ret.urls << QUrl::fromEncoded(query.Value(2).toByteArray()); + ret.urls << QUrl::fromEncoded(query.Value(0).toByteArray()); + ret.art_embedded = query.Value(1).toInt() == 1; + ret.art_automatic = QUrl::fromEncoded(query.Value(2).toByteArray()); + ret.art_manual = QUrl::fromEncoded(query.Value(3).toByteArray()); + ret.art_unset = query.Value(4).toInt() == 1; } return ret; } -void CollectionBackend::UpdateManualAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic) { +void CollectionBackend::UpdateEmbeddedAlbumArtAsync(const QString &effective_albumartist, const QString &album, const bool art_embedded) { - QMetaObject::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)); + QMetaObject::invokeMethod(this, "UpdateEmbeddedAlbumArt", Qt::QueuedConnection, Q_ARG(QString, effective_albumartist), Q_ARG(QString, album), Q_ARG(bool, art_embedded)); } -void CollectionBackend::UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic) { +void CollectionBackend::UpdateEmbeddedAlbumArt(const QString &effective_albumartist, const QString &album, const bool art_embedded) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); @@ -1571,15 +1579,11 @@ void CollectionBackend::UpdateManualAlbumArt(const QString &effective_albumartis } // Update the songs - QString sql = QString("UPDATE %1 SET art_manual = :cover").arg(songs_table_); - if (clear_art_automatic) { - sql += ", art_automatic = ''"; - } - sql += " WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0"; + QString sql = QString("UPDATE %1 SET art_embedded = :art_embedded, art_unset = 0 WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_); SqlQuery q(db); q.prepare(sql); - q.BindValue(":cover", cover_url.isValid() ? cover_url.toString(QUrl::FullyEncoded) : ""); + q.BindValue(":art_embedded", art_embedded ? 1 : 0); q.BindValue(":effective_albumartist", effective_albumartist); q.BindValue(":album", album); @@ -1608,18 +1612,17 @@ void CollectionBackend::UpdateManualAlbumArt(const QString &effective_albumartis } -void CollectionBackend::UpdateAutomaticAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_manual) { +void CollectionBackend::UpdateManualAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &art_manual) { - QMetaObject::invokeMethod(this, "UpdateAutomaticAlbumArt", Qt::QueuedConnection, Q_ARG(QString, effective_albumartist), Q_ARG(QString, album), Q_ARG(QUrl, cover_url), Q_ARG(bool, clear_art_manual)); + QMetaObject::invokeMethod(this, "UpdateManualAlbumArt", Qt::QueuedConnection, Q_ARG(QString, effective_albumartist), Q_ARG(QString, album), Q_ARG(QUrl, art_manual)); } -void CollectionBackend::UpdateAutomaticAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_manual) { +void CollectionBackend::UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &art_manual) { QMutexLocker l(db_->Mutex()); QSqlDatabase db(db_->Connect()); - // Get the songs before they're updated CollectionQuery query(db, songs_table_, fts_table_); query.SetColumnSpec("ROWID, " + Song::kColumnSpec); query.AddWhere("effective_albumartist", effective_albumartist); @@ -1637,16 +1640,124 @@ void CollectionBackend::UpdateAutomaticAlbumArt(const QString &effective_albumar deleted_songs << song; } - // Update the songs - QString sql = QString("UPDATE %1 SET art_automatic = :cover").arg(songs_table_); - if (clear_art_manual) { - sql += ", art_manual = ''"; - } - sql += " WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0"; - SqlQuery q(db); - q.prepare(sql); - q.BindValue(":cover", cover_url.isValid() ? cover_url.toString(QUrl::FullyEncoded) : ""); + q.prepare(QString("UPDATE %1 SET art_manual = :art_manual, art_unset = 0 WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_)); + q.BindValue(":art_manual", art_manual.isValid() ? art_manual.toString(QUrl::FullyEncoded) : ""); + q.BindValue(":effective_albumartist", effective_albumartist); + q.BindValue(":album", album); + + if (!q.Exec()) { + db_->ReportErrors(q); + return; + } + + if (!query.Exec()) { + ReportErrors(query); + return; + } + + SongList added_songs; + while (query.Next()) { + Song song(source_); + song.InitFromQuery(query, true); + added_songs << song; + } + + if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) { + emit SongsDeleted(deleted_songs); + emit SongsDiscovered(added_songs); + } + +} + +void CollectionBackend::UnsetAlbumArtAsync(const QString &effective_albumartist, const QString &album) { + + QMetaObject::invokeMethod(this, "UnsetAlbumArt", Qt::QueuedConnection, Q_ARG(QString, effective_albumartist), Q_ARG(QString, album)); + +} + +void CollectionBackend::UnsetAlbumArt(const QString &effective_albumartist, const QString &album) { + + QMutexLocker l(db_->Mutex()); + QSqlDatabase db(db_->Connect()); + + CollectionQuery query(db, songs_table_, fts_table_); + query.SetColumnSpec("ROWID, " + Song::kColumnSpec); + query.AddWhere("effective_albumartist", effective_albumartist); + query.AddWhere("album", album); + + if (!query.Exec()) { + ReportErrors(query); + return; + } + + SongList deleted_songs; + while (query.Next()) { + Song song(source_); + song.InitFromQuery(query, true); + deleted_songs << song; + } + + SqlQuery q(db); + q.prepare(QString("UPDATE %1 SET art_unset = 1, art_manual = '', art_automatic = '', art_embedded = '' WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_)); + q.BindValue(":effective_albumartist", effective_albumartist); + q.BindValue(":album", album); + + if (!q.Exec()) { + db_->ReportErrors(q); + return; + } + + if (!query.Exec()) { + ReportErrors(query); + return; + } + + SongList added_songs; + while (query.Next()) { + Song song(source_); + song.InitFromQuery(query, true); + added_songs << song; + } + + if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) { + emit SongsDeleted(deleted_songs); + emit SongsDiscovered(added_songs); + } + +} + +void CollectionBackend::ClearAlbumArtAsync(const QString &effective_albumartist, const QString &album, const bool unset) { + + QMetaObject::invokeMethod(this, "ClearAlbumArt", Qt::QueuedConnection, Q_ARG(QString, effective_albumartist), Q_ARG(QString, album), Q_ARG(bool, unset)); + +} + +void CollectionBackend::ClearAlbumArt(const QString &effective_albumartist, const QString &album, const bool art_unset) { + + QMutexLocker l(db_->Mutex()); + QSqlDatabase db(db_->Connect()); + + CollectionQuery query(db, songs_table_, fts_table_); + query.SetColumnSpec("ROWID, " + Song::kColumnSpec); + query.AddWhere("effective_albumartist", effective_albumartist); + query.AddWhere("album", album); + + if (!query.Exec()) { + ReportErrors(query); + return; + } + + SongList deleted_songs; + while (query.Next()) { + Song song(source_); + song.InitFromQuery(query, true); + deleted_songs << song; + } + + SqlQuery q(db); + q.prepare(QString("UPDATE %1 SET art_embedded = 0, art_automatic = '', art_manual = '', art_unset = :art_unset WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_)); + q.BindValue(":art_unset", art_unset ? 1 : 0); q.BindValue(":effective_albumartist", effective_albumartist); q.BindValue(":album", album); @@ -1655,7 +1766,6 @@ void CollectionBackend::UpdateAutomaticAlbumArt(const QString &effective_albumar return; } - // Now get the updated songs if (!query.Exec()) { ReportErrors(query); return; diff --git a/src/collection/collectionbackend.h b/src/collection/collectionbackend.h index fe6316ff..bbb9388c 100644 --- a/src/collection/collectionbackend.h +++ b/src/collection/collectionbackend.h @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2023, 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 @@ -54,12 +54,14 @@ class CollectionBackendInterface : public QObject { explicit CollectionBackendInterface(QObject *parent = nullptr) : QObject(parent) {} struct Album { - Album() : filetype(Song::FileType::Unknown) {} - 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() : art_embedded(false), art_unset(false), filetype(Song::FileType::Unknown) {} + Album(const QString &_album_artist, const QString &_album, const bool _art_embedded, const QUrl &_art_automatic, const QUrl &_art_manual, const bool _art_unset, const QList &_urls, const Song::FileType _filetype, const QString &_cue_path) : album_artist(_album_artist), album(_album), + art_embedded(_art_embedded), art_automatic(_art_automatic), art_manual(_art_manual), + art_unset(_art_unset), urls(_urls), filetype(_filetype), cue_path(_cue_path) {} @@ -67,8 +69,10 @@ class CollectionBackendInterface : public QObject { QString album_artist; QString album; + bool art_embedded; QUrl art_automatic; QUrl art_manual; + bool art_unset; QList urls; Song::FileType filetype; QString cue_path; @@ -109,8 +113,10 @@ class CollectionBackendInterface : public QObject { virtual AlbumList GetAlbumsByArtist(const QString &artist, const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0; virtual AlbumList GetCompilationAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) = 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, const bool clear_art_manual = false) = 0; + virtual void UpdateEmbeddedAlbumArtAsync(const QString &effective_albumartist, const QString &album, const bool art_embedded) = 0; + virtual void UpdateManualAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &art_manual) = 0; + virtual void UnsetAlbumArtAsync(const QString &effective_albumartist, const QString &album) = 0; + virtual void ClearAlbumArtAsync(const QString &effective_albumartist, const QString &album, const bool art_unset) = 0; virtual Album GetAlbumArt(const QString &effective_albumartist, const QString &album) = 0; @@ -179,8 +185,10 @@ class CollectionBackend : public CollectionBackendInterface { AlbumList GetCompilationAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) override; AlbumList GetAlbumsByArtist(const QString &artist, const CollectionFilterOptions &opt = CollectionFilterOptions()) override; - void UpdateManualAlbumArtAsync(const QString &effective_albumartist, 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, const bool clear_art_manual = false) override; + void UpdateEmbeddedAlbumArtAsync(const QString &effective_albumartist, const QString &album, const bool art_embedded) override; + void UpdateManualAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &art_manual) override; + void UnsetAlbumArtAsync(const QString &effective_albumartist, const QString &album) override; + void ClearAlbumArtAsync(const QString &effective_albumartist, const QString &album, const bool art_unset) override; Album GetAlbumArt(const QString &effective_albumartist, const QString &album) override; @@ -232,8 +240,10 @@ class CollectionBackend : public CollectionBackendInterface { void MarkSongsUnavailable(const SongList &songs, const bool unavailable = true); void AddOrUpdateSubdirs(const CollectionSubdirectoryList &subdirs); void CompilationsNeedUpdating(); - 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, const bool clear_art_manual = false); + void UpdateEmbeddedAlbumArt(const QString &effective_albumartist, const QString &album, const bool art_embedded); + void UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &art_manual); + void UnsetAlbumArt(const QString &effective_albumartist, const QString &album); + void ClearAlbumArt(const QString &effective_albumartist, const QString &album, const bool art_unset); void ForceCompilation(const QString &album, const QList &artists, const bool on); void IncrementPlayCount(const int id); void IncrementSkipCount(const int id, const float progress); @@ -303,7 +313,6 @@ class CollectionBackend : public CollectionBackendInterface { QString subdirs_table_; QString fts_table_; QThread *original_thread_; - }; #endif // COLLECTIONBACKEND_H diff --git a/src/collection/collectionmodel.cpp b/src/collection/collectionmodel.cpp index 6858f670..961067ac 100644 --- a/src/collection/collectionmodel.cpp +++ b/src/collection/collectionmodel.cpp @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2023, 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 @@ -71,6 +71,7 @@ #include "covermanager/albumcoverloader.h" #include "covermanager/albumcoverloaderresult.h" #include "settings/collectionsettingspage.h" +#include "settings/coverssettingspage.h" const int CollectionModel::kPrettyCoverSize = 32; const char *CollectionModel::kPixmapDiskCacheDir = "pixmapcache"; @@ -101,12 +102,6 @@ CollectionModel::CollectionModel(CollectionBackend *backend, Application *app, Q group_by_[1] = GroupBy::AlbumDisc; group_by_[2] = GroupBy::None; - cover_loader_options_.get_image_data_ = false; - cover_loader_options_.get_image_ = true; - 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); } @@ -150,6 +145,7 @@ void CollectionModel::set_pretty_covers(const bool use_pretty_covers) { use_pretty_covers_ = use_pretty_covers; Reset(); } + } void CollectionModel::set_show_dividers(const bool show_dividers) { @@ -177,6 +173,8 @@ void CollectionModel::ReloadSettings() { s.endGroup(); + cover_types_ = AlbumCoverLoaderOptions::LoadTypes(); + if (!use_disk_cache_) { ClearDiskCache(); } @@ -652,7 +650,10 @@ QVariant CollectionModel::AlbumIcon(const QModelIndex &idx) { // No art is cached and we're not loading it already. Load art for the first song in the album. SongList songs = GetChildSongs(idx); if (!songs.isEmpty()) { - const quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, songs.first()); + AlbumCoverLoaderOptions cover_loader_options(AlbumCoverLoaderOptions::Option::ScaledImage | AlbumCoverLoaderOptions::Option::PadScaledImage); + cover_loader_options.desired_scaled_size = QSize(kPrettyCoverSize, kPrettyCoverSize); + cover_loader_options.types = cover_types_; + const quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options, songs.first()); pending_art_[id] = ItemAndCacheKey(item, cache_key); pending_cache_keys_.insert(cache_key); } @@ -674,7 +675,7 @@ void CollectionModel::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderR pending_cache_keys_.remove(cache_key); // Insert this image in the cache. - if (!result.success || result.image_scaled.isNull() || result.type == AlbumCoverLoaderResult::Type::ManuallyUnset) { + if (!result.success || result.image_scaled.isNull() || result.type == AlbumCoverLoaderResult::Type::Unset) { // 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/collection/collectionmodel.h b/src/collection/collectionmodel.h index 9f0ffb8b..c60f6b60 100644 --- a/src/collection/collectionmodel.h +++ b/src/collection/collectionmodel.h @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2023, 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 @@ -49,11 +49,11 @@ #include "core/song.h" #include "core/sqlrow.h" #include "covermanager/albumcoverloader.h" +#include "covermanager/albumcoverloaderoptions.h" #include "collectionfilteroptions.h" #include "collectionquery.h" #include "collectionqueryoptions.h" #include "collectionitem.h" -#include "covermanager/albumcoverloaderoptions.h" class QSettings; @@ -310,7 +310,7 @@ class CollectionModel : public SimpleTreeModel { bool use_disk_cache_; bool use_lazy_loading_; - AlbumCoverLoaderOptions cover_loader_options_; + AlbumCoverLoaderOptions::Types cover_types_; using ItemAndCacheKey = QPair; QMap pending_art_; diff --git a/src/collection/collectionwatcher.cpp b/src/collection/collectionwatcher.cpp index 06318557..2d95802f 100644 --- a/src/collection/collectionwatcher.cpp +++ b/src/collection/collectionwatcher.cpp @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2023, 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 @@ -156,10 +156,10 @@ void CollectionWatcher::ReloadSettings() { overwrite_rating_ = s.value("overwrite_rating", false).toBool(); s.endGroup(); - best_image_filters_.clear(); + best_art_filters_.clear(); for (const QString &filter : filters) { QString str = filter.trimmed(); - if (!str.isEmpty()) best_image_filters_ << str; + if (!str.isEmpty()) best_art_filters_ << str; } if (!monitor_ && was_monitoring_before) { @@ -539,8 +539,8 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu bool changed = (matching_song.mtime() != qMax(fileinfo.lastModified().toSecsSinceEpoch(), matching_song_cue_mtime)) || cue_deleted || cue_added || cue_changed; // Also want to look to see whether the album art has changed - QUrl image = ImageForSong(file, album_art); - if ((matching_song.art_automatic().isEmpty() && !image.isEmpty()) || (!matching_song.art_automatic().isEmpty() && !matching_song.has_embedded_cover() && !QFile::exists(matching_song.art_automatic().toLocalFile()))) { + const QUrl art_automatic = ArtForSong(file, album_art); + if (matching_song.art_automatic() != art_automatic || (!matching_song.art_automatic().isEmpty() && !matching_song.art_automatic_is_valid())) { changed = true; } @@ -573,16 +573,16 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu #endif if (new_cue.isEmpty() || new_cue_mtime == 0) { // If no CUE or it's about to lose it. - UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, image, cue_deleted, t); + UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, cue_deleted, t); } else { // If CUE associated. - UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, image, matching_songs, t); + UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t); } } // Nothing has changed - mark the song available without re-scanning - else if (matching_song.is_unavailable()) { + else if (matching_song.unavailable()) { t->readded_songs << matching_songs; } @@ -633,13 +633,13 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu } // Get new album art - QUrl image = ImageForSong(file, album_art); + const QUrl art_automatic = ArtForSong(file, album_art); if (new_cue.isEmpty() || new_cue_mtime == 0) { // If no CUE or it's about to lose it. - UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, image, matching_songs_has_cue && new_cue_mtime == 0, t); + UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, matching_songs_has_cue && new_cue_mtime == 0, t); } else { // If CUE associated. - UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, image, matching_songs, t); + UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t); } } @@ -653,12 +653,12 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu qLog(Debug) << file << "is new."; - // Choose an image for the song(s) - QUrl image = ImageForSong(file, album_art); + // Choose art for the song(s) + const QUrl art_automatic = ArtForSong(file, album_art); for (Song song : songs) { song.set_directory_id(t->dir()); - if (song.art_automatic().isEmpty()) song.set_art_automatic(image); + if (song.art_automatic().isEmpty()) song.set_art_automatic(art_automatic); t->new_songs << song; } } @@ -669,7 +669,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu // Look for deleted songs for (const Song &song : songs_in_db) { QString file = song.url().toLocalFile(); - if (!song.is_unavailable() && !files_on_disk.contains(file) && !t->files_changed_path_.contains(file)) { + if (!song.unavailable() && !files_on_disk.contains(file) && !t->files_changed_path_.contains(file)) { qLog(Debug) << "Song deleted from disk:" << file; t->deleted_songs << song; } @@ -704,7 +704,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, - const QUrl &image, + const QUrl &art_automatic, const SongList &old_cue_songs, ScanTransaction *t) { @@ -733,7 +733,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file, if (sections_map.contains(new_cue_song.beginning_nanosec())) { // Changed section const Song matching_cue_song = sections_map[new_cue_song.beginning_nanosec()]; new_cue_song.set_id(matching_cue_song.id()); - if (!new_cue_song.has_embedded_cover()) new_cue_song.set_art_automatic(image); + new_cue_song.set_art_automatic(art_automatic); new_cue_song.MergeUserSetData(matching_cue_song, true, true); AddChangedSong(file, matching_cue_song, new_cue_song, t); used_ids.insert(matching_cue_song.id()); @@ -755,7 +755,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file, void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file, const QString &fingerprint, const SongList &matching_songs, - const QUrl &image, + const QUrl &art_automatic, const bool cue_deleted, ScanTransaction *t) { @@ -776,7 +776,7 @@ void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file, song_on_disk.set_directory_id(t->dir()); song_on_disk.set_id(matching_song.id()); song_on_disk.set_fingerprint(fingerprint); - if (!song_on_disk.has_embedded_cover()) song_on_disk.set_art_automatic(image); + song_on_disk.set_art_automatic(art_automatic); song_on_disk.MergeUserSetData(matching_song, !overwrite_playcount_, !overwrite_rating_); AddChangedSong(file, matching_song, song_on_disk, t); } @@ -838,7 +838,7 @@ void CollectionWatcher::AddChangedSong(const QString &file, const Song &matching bool notify_new = false; QStringList changes; - if (matching_song.is_unavailable()) { + if (matching_song.unavailable()) { qLog(Debug) << "unavailable song" << file << "restored."; notify_new = true; } @@ -1039,20 +1039,20 @@ void CollectionWatcher::RescanPathsNow() { } -QString CollectionWatcher::PickBestImage(const QStringList &images) { +QString CollectionWatcher::PickBestArt(const QStringList &art_automatic_list) { // This is used when there is more than one image in a directory. // Pick the biggest image that matches the most important filter QStringList filtered; - for (const QString &filter_text : best_image_filters_) { + for (const QString &filter_text : best_art_filters_) { // The images in the images list are represented by a full path, so we need to isolate just the filename - for (const QString &image : images) { - QFileInfo fileinfo(image); + for (const QString &art_automatic : art_automatic_list) { + QFileInfo fileinfo(art_automatic); QString filename(fileinfo.fileName()); if (filename.contains(filter_text, Qt::CaseInsensitive)) - filtered << image; + filtered << art_automatic; } // We assume the filters are give in the order best to worst, so if we've got a result, we go with it. @@ -1062,7 +1062,7 @@ QString CollectionWatcher::PickBestImage(const QStringList &images) { if (filtered.isEmpty()) { // The filter was too restrictive, just use the original list - filtered = images; + filtered = art_automatic_list; } int biggest_size = 0; @@ -1085,20 +1085,21 @@ QString CollectionWatcher::PickBestImage(const QStringList &images) { } -QUrl CollectionWatcher::ImageForSong(const QString &path, QMap &album_art) { +QUrl CollectionWatcher::ArtForSong(const QString &path, QMap &art_automatic_list) { QString dir(DirectoryPart(path)); - if (album_art.contains(dir)) { - if (album_art[dir].count() == 1) { - return QUrl::fromLocalFile(album_art[dir][0]); + if (art_automatic_list.contains(dir)) { + if (art_automatic_list[dir].count() == 1) { + return QUrl::fromLocalFile(art_automatic_list[dir][0]); } else { - QString best_image = PickBestImage(album_art[dir]); - album_art[dir] = QStringList() << best_image; - return QUrl::fromLocalFile(best_image); + const QString best_art = PickBestArt(art_automatic_list[dir]); + art_automatic_list[dir] = QStringList() << best_art; + return QUrl::fromLocalFile(best_art); } } + return QUrl(); } diff --git a/src/collection/collectionwatcher.h b/src/collection/collectionwatcher.h index 61004d39..505d878d 100644 --- a/src/collection/collectionwatcher.h +++ b/src/collection/collectionwatcher.h @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2023, 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 @@ -178,17 +178,17 @@ class CollectionWatcher : public QObject { inline static QString NoExtensionPart(const QString &fileName); inline static QString ExtensionPart(const QString &fileName); inline static QString DirectoryPart(const QString &fileName); - QString PickBestImage(const QStringList &images); - QUrl ImageForSong(const QString &path, QMap &album_art); + QString PickBestArt(const QStringList &art_automatic_list); + QUrl ArtForSong(const QString &path, QMap &art_automatic_list); void AddWatch(const CollectionDirectory &dir, const QString &path); void RemoveWatch(const CollectionDirectory &dir, const CollectionSubdirectory &subdir); static quint64 GetMtimeForCue(const QString &cue_path); void PerformScan(const bool incremental, const bool ignore_mtimes); // Updates the sections of a cue associated and altered (according to mtime) media file during a scan. - void UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, const QUrl &image, const SongList &old_cue_songs, ScanTransaction *t); + void UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, const QUrl &art_automatic, const SongList &old_cue_songs, ScanTransaction *t); // Updates a single non-cue associated and altered (according to mtime) song during a scan. - void UpdateNonCueAssociatedSong(const QString &file, const QString &fingerprint, const SongList &matching_songs, const QUrl &image, const bool cue_deleted, ScanTransaction *t); + void UpdateNonCueAssociatedSong(const QString &file, const QString &fingerprint, const SongList &matching_songs, const QUrl &art_automatic, const bool cue_deleted, ScanTransaction *t); // Scans a single media file that's present on the disk but not yet in the collection. // It may result in a multiple files added to the collection when the media file has many sections (like a CUE related media file). SongList ScanNewFile(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, QSet *cues_processed); @@ -210,9 +210,9 @@ class CollectionWatcher : public QObject { QThread *original_thread_; QHash subdir_mapping_; - // A list of words use to try to identify the (likely) best image found in an directory to use as cover artwork. + // A list of words use to try to identify the (likely) best album cover art found in an directory to use as cover artwork. // e.g. using ["front", "cover"] would identify front.jpg and exclude back.jpg. - QStringList best_image_filters_; + QStringList best_art_filters_; bool scan_on_startup_; bool monitor_; diff --git a/src/context/contextalbum.cpp b/src/context/contextalbum.cpp index 271e13f8..c4ed2adb 100644 --- a/src/context/contextalbum.cpp +++ b/src/context/contextalbum.cpp @@ -54,16 +54,14 @@ ContextAlbum::ContextAlbum(QWidget *parent) timeline_fade_(new QTimeLine(kFadeTimeLineMs, this)), image_strawberry_(":/pictures/strawberry.png"), image_original_(image_strawberry_), - pixmap_current_opacity_(1.0) { + pixmap_current_opacity_(1.0), + desired_height_(width()) { setObjectName("context-widget-album"); setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - cover_loader_options_.desired_height_ = width(); - cover_loader_options_.pad_output_image_ = true; - cover_loader_options_.scale_output_image_ = true; - QImage image = ImageUtils::ScaleAndPad(image_strawberry_, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_, devicePixelRatioF()); + QImage image = ImageUtils::ScaleImage(image_strawberry_, QSize(desired_height_, desired_height_), devicePixelRatioF(), true); if (!image.isNull()) { pixmap_current_ = QPixmap::fromImage(image); } @@ -128,8 +126,8 @@ void ContextAlbum::contextMenuEvent(QContextMenuEvent *e) { void ContextAlbum::UpdateWidth(const int new_width) { - if (new_width != cover_loader_options_.desired_height_) { - cover_loader_options_.desired_height_ = new_width; + if (new_width != desired_height_) { + desired_height_ = new_width; ScaleCover(); ScalePreviousCovers(); updateGeometry(); @@ -235,7 +233,7 @@ void ContextAlbum::FadePreviousCoverFinished(std::shared_ptr prev void ContextAlbum::ScaleCover() { - QImage image = ImageUtils::ScaleAndPad(image_original_, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_, devicePixelRatioF()); + const QImage image = ImageUtils::ScaleImage(image_original_, QSize(desired_height_, desired_height_), devicePixelRatioF(), true); if (image.isNull()) { pixmap_current_ = QPixmap(); } @@ -248,7 +246,7 @@ void ContextAlbum::ScaleCover() { void ContextAlbum::ScalePreviousCovers() { for (std::shared_ptr previous_cover : previous_covers_) { - QImage image = ImageUtils::ScaleAndPad(previous_cover->image, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_, devicePixelRatioF()); + QImage image = ImageUtils::ScaleImage(previous_cover->image, QSize(desired_height_, desired_height_), devicePixelRatioF(), true); if (image.isNull()) { previous_cover->pixmap = QPixmap(); } diff --git a/src/context/contextalbum.h b/src/context/contextalbum.h index fb68f440..6bebcb65 100644 --- a/src/context/contextalbum.h +++ b/src/context/contextalbum.h @@ -33,8 +33,6 @@ #include #include -#include "covermanager/albumcoverloaderoptions.h" - class QMenu; class QTimeLine; class QPainter; @@ -99,7 +97,6 @@ class ContextAlbum : public QWidget { QMenu *menu_; ContextView *context_view_; AlbumCoverChoiceController *album_cover_choice_controller_; - AlbumCoverLoaderOptions cover_loader_options_; bool downloading_covers_; QTimeLine *timeline_fade_; QImage image_strawberry_; @@ -107,6 +104,7 @@ class ContextAlbum : public QWidget { QPixmap pixmap_current_; qreal pixmap_current_opacity_; std::unique_ptr spinner_animation_; + int desired_height_; }; #endif // CONTEXTALBUM_H diff --git a/src/core/database.cpp b/src/core/database.cpp index bbf8fd1d..6a039beb 100644 --- a/src/core/database.cpp +++ b/src/core/database.cpp @@ -48,7 +48,7 @@ #include "scopedtransaction.h" const char *Database::kDatabaseFilename = "strawberry.db"; -const int Database::kSchemaVersion = 16; +const int Database::kSchemaVersion = 17; const int Database::kMinSupportedSchemaVersion = 10; const char *Database::kMagicAllSongsTables = "%allsongstables"; diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 32b91d47..39f4ab19 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -1177,6 +1177,7 @@ void MainWindow::ReloadAllSettings() { collection_view_->ReloadSettings(); ui_->playlist->view()->ReloadSettings(); app_->playlist_manager()->playlist_container()->ReloadSettings(); + app_->current_albumcover_loader()->ReloadSettingsAsync(); album_cover_choice_controller_->ReloadSettings(); context_view_->ReloadSettings(); file_view_->ReloadSettings(); @@ -1381,14 +1382,14 @@ void MainWindow::SongChanged(const Song &song) { SendNowPlaying(); const bool enable_change_art = song.is_collection_song() && !song.effective_albumartist().isEmpty() && !song.album().isEmpty(); - album_cover_choice_controller_->show_cover_action()->setEnabled(song.has_valid_art() && !song.has_manually_unset_cover()); - album_cover_choice_controller_->cover_to_file_action()->setEnabled(song.has_valid_art() && !song.has_manually_unset_cover()); + album_cover_choice_controller_->show_cover_action()->setEnabled(song.has_valid_art() && !song.art_unset()); + album_cover_choice_controller_->cover_to_file_action()->setEnabled(song.has_valid_art() && !song.art_unset()); album_cover_choice_controller_->cover_from_file_action()->setEnabled(enable_change_art); album_cover_choice_controller_->cover_from_url_action()->setEnabled(enable_change_art); album_cover_choice_controller_->search_for_cover_action()->setEnabled(app_->cover_providers()->HasAnyProviders() && enable_change_art); - album_cover_choice_controller_->unset_cover_action()->setEnabled(enable_change_art && !song.has_manually_unset_cover()); + album_cover_choice_controller_->unset_cover_action()->setEnabled(enable_change_art && !song.art_unset()); album_cover_choice_controller_->clear_cover_action()->setEnabled(enable_change_art && !song.art_manual().isEmpty()); - album_cover_choice_controller_->delete_cover_action()->setEnabled(enable_change_art && song.has_valid_art() && !song.has_manually_unset_cover()); + album_cover_choice_controller_->delete_cover_action()->setEnabled(enable_change_art && (song.art_embedded() || !song.art_automatic().isEmpty() || !song.art_manual().isEmpty())); } @@ -3072,14 +3073,14 @@ void MainWindow::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult emit AlbumCoverReady(song, result.album_cover.image); const bool enable_change_art = song.is_collection_song() && !song.effective_albumartist().isEmpty() && !song.album().isEmpty(); - 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_->show_cover_action()->setEnabled(result.success && result.type != AlbumCoverLoaderResult::Type::Unset); + album_cover_choice_controller_->cover_to_file_action()->setEnabled(result.success && result.type != AlbumCoverLoaderResult::Type::Unset); album_cover_choice_controller_->cover_from_file_action()->setEnabled(enable_change_art); album_cover_choice_controller_->cover_from_url_action()->setEnabled(enable_change_art); album_cover_choice_controller_->search_for_cover_action()->setEnabled(app_->cover_providers()->HasAnyProviders() && enable_change_art); - album_cover_choice_controller_->unset_cover_action()->setEnabled(enable_change_art && !song.has_manually_unset_cover()); + album_cover_choice_controller_->unset_cover_action()->setEnabled(enable_change_art && !song.art_unset()); album_cover_choice_controller_->clear_cover_action()->setEnabled(enable_change_art && !song.art_manual().isEmpty()); - album_cover_choice_controller_->delete_cover_action()->setEnabled(enable_change_art && result.success && result.type != AlbumCoverLoaderResult::Type::ManuallyUnset); + album_cover_choice_controller_->delete_cover_action()->setEnabled(enable_change_art && result.success && result.type != AlbumCoverLoaderResult::Type::Unset); GetCoverAutomatically(); @@ -3089,7 +3090,8 @@ void MainWindow::GetCoverAutomatically() { // Search for cover automatically? const bool search = album_cover_choice_controller_->search_cover_auto_action()->isChecked() && - !song_.has_manually_unset_cover() && + !song_.art_unset() && + !song_.art_embedded() && !song_.art_automatic_is_valid() && !song_.art_manual_is_valid() && !song_.effective_albumartist().isEmpty() && diff --git a/src/core/mpris2.cpp b/src/core/mpris2.cpp index 791b7e3f..04119d83 100644 --- a/src/core/mpris2.cpp +++ b/src/core/mpris2.cpp @@ -401,14 +401,16 @@ void Mpris2::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &re else if (result.temp_cover_url.isValid() && result.temp_cover_url.isLocalFile()) { cover_url = result.temp_cover_url; } - else if (song.art_manual().isValid() && song.art_manual().isLocalFile() && song.art_manual().path() != Song::kManuallyUnsetCover && song.art_manual().path() != Song::kEmbeddedCover) { + else if (song.art_manual().isValid() && song.art_manual().isLocalFile()) { cover_url = song.art_manual(); } - else if (song.art_automatic().isValid() && song.art_automatic().isLocalFile() && song.art_automatic().path() != Song::kManuallyUnsetCover && song.art_automatic().path() != Song::kEmbeddedCover) { + else if (song.art_automatic().isValid() && song.art_automatic().isLocalFile()) { cover_url = song.art_automatic(); } - if (cover_url.isValid()) AddMetadata("mpris:artUrl", cover_url.toString(), &last_metadata_); + if (cover_url.isValid()) { + AddMetadata("mpris:artUrl", cover_url.toString(), &last_metadata_); + } AddMetadata("year", song.year(), &last_metadata_); AddMetadata("bitrate", song.bitrate(), &last_metadata_); diff --git a/src/core/song.cpp b/src/core/song.cpp index 133afa8f..2c99122a 100644 --- a/src/core/song.cpp +++ b/src/core/song.cpp @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2023, 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 @@ -108,8 +108,10 @@ const QStringList Song::kColumns = QStringList() << "title" << "compilation_off" << "compilation_effective" + << "art_embedded" << "art_automatic" << "art_manual" + << "art_unset" << "effective_albumartist" << "effective_originalyear" @@ -170,17 +172,14 @@ struct Song::Private : public QSharedData { explicit Private(Source source = Source::Unknown); - bool valid_; int id_; + bool valid_; + QString title_; - QString title_sortable_; QString album_; - QString album_sortable_; QString artist_; - QString artist_sortable_; QString albumartist_; - QString albumartist_sortable_; int track_; int disc_; int year_; @@ -225,9 +224,10 @@ struct Song::Private : public QSharedData { bool compilation_on_; // Set by the user bool compilation_off_; // Set by the user - // Filenames to album art for this song. - QUrl art_automatic_; // Guessed by CollectionWatcher - QUrl art_manual_; // Set by the user - should take priority + bool art_embedded_; // if the song has embedded album cover art. + QUrl art_automatic_; // Guessed by CollectionWatcher. + QUrl art_manual_; // Set by the user - should take priority. + bool art_unset_; // If the art was unset by the user. QString cue_path_; // If the song has a CUE, this contains it's path. @@ -247,15 +247,21 @@ struct Song::Private : public QSharedData { QString musicbrainz_release_group_id_; QString musicbrainz_work_id_; - QUrl stream_url_; // Temporary stream url set by url handler. bool init_from_file_; // Whether this song was loaded from a file using taglib. bool suspicious_tags_; // Whether our encoding guesser thinks these tags might be incorrectly encoded. + QString title_sortable_; + QString album_sortable_; + QString artist_sortable_; + QString albumartist_sortable_; + + QUrl stream_url_; // Temporary stream url set by the URL handler. + }; Song::Private::Private(const Source source) - : valid_(false), - id_(-1), + : id_(-1), + valid_(false), track_(-1), disc_(-1), @@ -287,6 +293,9 @@ Song::Private::Private(const Source source) compilation_on_(false), compilation_off_(false), + art_embedded_(false), + art_unset_(false), + rating_(-1), init_from_file_(false), @@ -298,38 +307,30 @@ Song::Song(const Source source) : d(new Private(source)) {} Song::Song(const Song &other) = default; Song::~Song() = default; +bool Song::operator==(const Song &other) const { + return source() == other.source() && url() == other.url() && beginning_nanosec() == other.beginning_nanosec(); +} + +bool Song::operator!=(const Song &other) const { + return source() != other.source() || url() != other.url() || beginning_nanosec() != other.beginning_nanosec(); +} + Song &Song::operator=(const Song &other) { d = other.d; return *this; } -bool Song::is_valid() const { return d->valid_; } -bool Song::is_unavailable() const { return d->unavailable_; } int Song::id() const { return d->id_; } - -QString Song::artist_id() const { return d->artist_id_.isNull() ? "" : d->artist_id_; } -QString Song::album_id() const { return d->album_id_.isNull() ? "" : d->album_id_; } -QString Song::song_id() const { return d->song_id_.isNull() ? "" : d->song_id_; } +bool Song::is_valid() const { return d->valid_; } const QString &Song::title() const { return d->title_; } -const QString &Song::title_sortable() const { return d->title_sortable_; } const QString &Song::album() const { return d->album_; } -const QString &Song::album_sortable() const { return d->album_sortable_; } -// This value is useful for singles, which are one-track albums on their own. -const QString &Song::effective_album() const { return d->album_.isEmpty() ? d->title_ : d->album_; } const QString &Song::artist() const { return d->artist_; } -const QString &Song::artist_sortable() const { return d->artist_sortable_; } const QString &Song::albumartist() const { return d->albumartist_; } -const QString &Song::albumartist_sortable() const { return d->albumartist_sortable_; } -const QString &Song::effective_albumartist() const { return d->albumartist_.isEmpty() ? d->artist_ : d->albumartist_; } -const QString &Song::effective_albumartist_sortable() const { return d->albumartist_.isEmpty() ? d->artist_sortable_ : d->albumartist_sortable_; } -const QString &Song::playlist_albumartist() const { return is_compilation() ? d->albumartist_ : effective_albumartist(); } -const QString &Song::playlist_albumartist_sortable() const { return is_compilation() ? d->albumartist_sortable_ : effective_albumartist_sortable(); } int Song::track() const { return d->track_; } int Song::disc() const { return d->disc_; } int Song::year() const { return d->year_; } int Song::originalyear() const { return d->originalyear_; } -int Song::effective_originalyear() const { return d->originalyear_ < 0 ? d->year_ : d->originalyear_; } const QString &Song::genre() const { return d->genre_; } bool Song::compilation() const { return d->compilation_; } const QString &Song::composer() const { return d->composer_; } @@ -338,6 +339,10 @@ const QString &Song::grouping() const { return d->grouping_; } const QString &Song::comment() const { return d->comment_; } const QString &Song::lyrics() const { return d->lyrics_; } +QString Song::artist_id() const { return d->artist_id_.isNull() ? "" : d->artist_id_; } +QString Song::album_id() const { return d->album_id_.isNull() ? "" : d->album_id_; } +QString Song::song_id() const { return d->song_id_.isNull() ? "" : d->song_id_; } + qint64 Song::beginning_nanosec() const { return d->beginning_; } qint64 Song::end_nanosec() const { return d->end_; } qint64 Song::length_nanosec() const { return d->end_ - d->beginning_; } @@ -354,6 +359,7 @@ Song::FileType Song::filetype() const { return d->filetype_; } qint64 Song::filesize() const { return d->filesize_; } qint64 Song::mtime() const { return d->mtime_; } qint64 Song::ctime() const { return d->ctime_; } +bool Song::unavailable() const { return d->unavailable_; } QString Song::fingerprint() const { return d->fingerprint_; } @@ -366,99 +372,12 @@ bool Song::compilation_detected() const { return d->compilation_detected_; } bool Song::compilation_off() const { return d->compilation_off_; } bool Song::compilation_on() const { return d->compilation_on_; } +bool Song::art_embedded() const { return d->art_embedded_; } 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::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::additional_tags_supported() const { - return d->filetype_ == FileType::FLAC || - d->filetype_ == FileType::WavPack || - d->filetype_ == FileType::OggFlac || - d->filetype_ == FileType::OggVorbis || - d->filetype_ == FileType::OggOpus || - d->filetype_ == FileType::OggSpeex || - d->filetype_ == FileType::MPEG || - d->filetype_ == FileType::MP4 || - d->filetype_ == FileType::MPC || - d->filetype_ == FileType::APE; -} - -bool Song::albumartist_supported() const { - return additional_tags_supported(); -} - -bool Song::composer_supported() const { - return additional_tags_supported(); -} - -bool Song::performer_supported() const { - return d->filetype_ == FileType::FLAC || - d->filetype_ == FileType::WavPack || - d->filetype_ == FileType::OggFlac || - d->filetype_ == FileType::OggVorbis || - d->filetype_ == FileType::OggOpus || - d->filetype_ == FileType::OggSpeex || - d->filetype_ == FileType::MPEG || - d->filetype_ == FileType::MPC || - d->filetype_ == FileType::APE; -} - -bool Song::grouping_supported() const { - return additional_tags_supported(); -} - -bool Song::genre_supported() const { - return additional_tags_supported(); -} - -bool Song::compilation_supported() const { - return additional_tags_supported(); -} - -bool Song::rating_supported() const { - return d->filetype_ == FileType::FLAC || - d->filetype_ == FileType::WavPack || - d->filetype_ == FileType::OggFlac || - d->filetype_ == FileType::OggVorbis || - d->filetype_ == FileType::OggOpus || - d->filetype_ == FileType::OggSpeex || - d->filetype_ == FileType::MPEG || - d->filetype_ == FileType::MP4 || - d->filetype_ == FileType::ASF || - d->filetype_ == FileType::MPC || - d->filetype_ == FileType::APE; -} - -bool Song::comment_supported() const { - return additional_tags_supported(); -} - -bool Song::lyrics_supported() const { - return additional_tags_supported(); -} - -bool Song::save_embedded_cover_supported(const FileType filetype) { - - return filetype == FileType::FLAC || - filetype == FileType::OggVorbis || - filetype == FileType::OggOpus || - 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_; } -bool Song::init_from_file() const { return d->init_from_file_; } +bool Song::art_unset() const { return d->art_unset_; } const QString &Song::cue_path() const { return d->cue_path_; } -bool Song::has_cue() const { return !d->cue_path_.isEmpty(); } float Song::rating() const { return d->rating_; } @@ -476,60 +395,18 @@ const QString &Song::musicbrainz_disc_id() const { return d->musicbrainz_disc_id const QString &Song::musicbrainz_release_group_id() const { return d->musicbrainz_release_group_id_; } const QString &Song::musicbrainz_work_id() const { return d->musicbrainz_work_id_; } -bool Song::is_collection_song() const { return d->source_ == Source::Collection; } -bool Song::is_metadata_good() const { return !d->url_.isEmpty() && !d->artist_.isEmpty() && !d->title_.isEmpty(); } -bool Song::is_stream() const { return is_radio() || d->source_ == Source::Tidal || d->source_ == Source::Subsonic || d->source_ == Source::Qobuz; } -bool Song::is_radio() const { return d->source_ == Source::Stream || d->source_ == Source::SomaFM || d->source_ == Source::RadioParadise; } -bool Song::is_cdda() const { return d->source_ == Source::CDDA; } -bool Song::is_compilation() const { return (d->compilation_ || d->compilation_detected_ || d->compilation_on_) && !d->compilation_off_; } -bool Song::stream_url_can_expire() const { return d->source_ == Source::Tidal || d->source_ == Source::Qobuz; } -bool Song::is_module_music() const { return d->filetype_ == FileType::MOD || d->filetype_ == FileType::S3M || d->filetype_ == FileType::XM || d->filetype_ == FileType::IT; } +bool Song::init_from_file() const { return d->init_from_file_; } -bool Song::art_automatic_is_valid() const { - return !d->art_automatic_.isEmpty() && - ( - (d->art_automatic_.path() == kManuallyUnsetCover) || - (d->art_automatic_.path() == kEmbeddedCover) || - (d->art_automatic_.isValid() && !d->art_automatic_.isLocalFile()) || - (d->art_automatic_.isLocalFile() && QFile::exists(d->art_automatic_.toLocalFile())) || - (d->art_automatic_.scheme().isEmpty() && !d->art_automatic_.path().isEmpty() && QFile::exists(d->art_automatic_.path())) - ); -} +const QString &Song::title_sortable() const { return d->title_sortable_; } +const QString &Song::album_sortable() const { return d->album_sortable_; } +const QString &Song::artist_sortable() const { return d->artist_sortable_; } +const QString &Song::albumartist_sortable() const { return d->albumartist_sortable_; } -bool Song::art_manual_is_valid() const { - return !d->art_manual_.isEmpty() && - ( - (d->art_manual_.path() == kManuallyUnsetCover) || - (d->art_manual_.path() == kEmbeddedCover) || - (d->art_manual_.isValid() && !d->art_manual_.isLocalFile()) || - (d->art_manual_.isLocalFile() && QFile::exists(d->art_manual_.toLocalFile())) || - (d->art_manual_.scheme().isEmpty() && !d->art_manual_.path().isEmpty() && QFile::exists(d->art_manual_.path())) - ); -} - -bool Song::has_valid_art() const { return art_automatic_is_valid() || art_manual_is_valid(); } +const QUrl &Song::stream_url() const { return d->stream_url_; } void Song::set_id(const int id) { d->id_ = id; } void Song::set_valid(const bool v) { d->valid_ = v; } -void Song::set_artist_id(const QString &v) { d->artist_id_ = v; } -void Song::set_album_id(const QString &v) { d->album_id_ = v; } -void Song::set_song_id(const QString &v) { d->song_id_ = v; } - -QString Song::sortable(const QString &v) { - - QString copy = v.toLower(); - - for (const auto &i : kArticles) { - if (copy.startsWith(i)) { - qint64 ilen = i.length(); - return copy.right(copy.length() - ilen) + ", " + copy.left(ilen - 1); - } - } - - return copy; -} - void Song::set_title(const QString &v) { d->title_sortable_ = sortable(v); d->title_ = v; } void Song::set_album(const QString &v) { d->album_sortable_ = sortable(v); d->album_ = v; } void Song::set_artist(const QString &v) { d->artist_sortable_ = sortable(v); d->artist_ = v; } @@ -546,6 +423,10 @@ void Song::set_grouping(const QString &v) { d->grouping_ = v; } void Song::set_comment(const QString &v) { d->comment_ = v; } void Song::set_lyrics(const QString &v) { d->lyrics_ = v; } +void Song::set_artist_id(const QString &v) { d->artist_id_ = v; } +void Song::set_album_id(const QString &v) { d->album_id_ = v; } +void Song::set_song_id(const QString &v) { d->song_id_ = v; } + void Song::set_beginning_nanosec(const qint64 v) { d->beginning_ = qMax(0LL, v); } void Song::set_end_nanosec(const qint64 v) { d->end_ = v; } void Song::set_length_nanosec(const qint64 v) { d->end_ = d->beginning_ + v; } @@ -575,8 +456,11 @@ void Song::set_compilation_detected(const bool v) { d->compilation_detected_ = v void Song::set_compilation_on(const bool v) { d->compilation_on_ = v; } void Song::set_compilation_off(const bool v) { d->compilation_off_ = v; } +void Song::set_art_embedded(const bool v) { d->art_embedded_ = v; } void Song::set_art_automatic(const QUrl &v) { d->art_automatic_ = v; } void Song::set_art_manual(const QUrl &v) { d->art_manual_ = v; } +void Song::set_art_unset(const bool v) { d->art_unset_ = v; } + void Song::set_cue_path(const QString &v) { d->cue_path_ = v; } void Song::set_rating(const float v) { d->rating_ = v; } @@ -597,10 +481,325 @@ void Song::set_musicbrainz_work_id(const QString &v) { d->musicbrainz_work_id_ = void Song::set_stream_url(const QUrl &v) { d->stream_url_ = v; } +const QUrl &Song::effective_stream_url() const { return !d->stream_url_.isEmpty() && d->stream_url_.isValid() ? d->stream_url_ : d->url_; } +const QString &Song::effective_albumartist() const { return d->albumartist_.isEmpty() ? d->artist_ : d->albumartist_; } +const QString &Song::effective_albumartist_sortable() const { return d->albumartist_.isEmpty() ? d->artist_sortable_ : d->albumartist_sortable_; } +const QString &Song::effective_album() const { return d->album_.isEmpty() ? d->title_ : d->album_; } +int Song::effective_originalyear() const { return d->originalyear_ < 0 ? d->year_ : d->originalyear_; } +const QString &Song::playlist_albumartist() const { return is_compilation() ? d->albumartist_ : effective_albumartist(); } +const QString &Song::playlist_albumartist_sortable() const { return is_compilation() ? d->albumartist_sortable_ : effective_albumartist_sortable(); } + +bool Song::is_metadata_good() const { return !d->url_.isEmpty() && !d->artist_.isEmpty() && !d->title_.isEmpty(); } +bool Song::is_collection_song() const { return d->source_ == Source::Collection; } +bool Song::is_stream() const { return is_radio() || d->source_ == Source::Tidal || d->source_ == Source::Subsonic || d->source_ == Source::Qobuz; } +bool Song::is_radio() const { return d->source_ == Source::Stream || d->source_ == Source::SomaFM || d->source_ == Source::RadioParadise; } +bool Song::is_cdda() const { return d->source_ == Source::CDDA; } +bool Song::is_compilation() const { return (d->compilation_ || d->compilation_detected_ || d->compilation_on_) && !d->compilation_off_; } +bool Song::stream_url_can_expire() const { return d->source_ == Source::Tidal || d->source_ == Source::Qobuz; } +bool Song::is_module_music() const { return d->filetype_ == FileType::MOD || d->filetype_ == FileType::S3M || d->filetype_ == FileType::XM || d->filetype_ == FileType::IT; } +bool Song::has_cue() const { return !d->cue_path_.isEmpty(); } + +bool Song::art_automatic_is_valid() const { + return !d->art_automatic_.isEmpty() && d->art_automatic_.isValid() && (!d->art_automatic_.isLocalFile() || (d->art_automatic_.isLocalFile() && QFile::exists(d->art_automatic_.toLocalFile()))); +} +bool Song::art_manual_is_valid() const { + return !d->art_manual_.isEmpty() && d->art_manual_.isValid() && (!d->art_manual_.isLocalFile() || (d->art_manual_.isLocalFile() && QFile::exists(d->art_manual_.toLocalFile()))); +} +bool Song::has_valid_art() const { return art_embedded() || art_automatic_is_valid() || art_manual_is_valid(); } +void Song::clear_art_automatic() { d->art_automatic_.clear(); } +void Song::clear_art_manual() { d->art_manual_.clear(); } + +bool Song::additional_tags_supported() const { + + return d->filetype_ == FileType::FLAC || + d->filetype_ == FileType::WavPack || + d->filetype_ == FileType::OggFlac || + d->filetype_ == FileType::OggVorbis || + d->filetype_ == FileType::OggOpus || + d->filetype_ == FileType::OggSpeex || + d->filetype_ == FileType::MPEG || + d->filetype_ == FileType::MP4 || + d->filetype_ == FileType::MPC || + d->filetype_ == FileType::APE; + +} + +bool Song::albumartist_supported() const { + return additional_tags_supported(); +} + +bool Song::composer_supported() const { + return additional_tags_supported(); +} + +bool Song::performer_supported() const { + + return d->filetype_ == FileType::FLAC || + d->filetype_ == FileType::WavPack || + d->filetype_ == FileType::OggFlac || + d->filetype_ == FileType::OggVorbis || + d->filetype_ == FileType::OggOpus || + d->filetype_ == FileType::OggSpeex || + d->filetype_ == FileType::MPEG || + d->filetype_ == FileType::MPC || + d->filetype_ == FileType::APE; + +} + +bool Song::grouping_supported() const { + return additional_tags_supported(); +} + +bool Song::genre_supported() const { + return additional_tags_supported(); +} + +bool Song::compilation_supported() const { + return additional_tags_supported(); +} + +bool Song::rating_supported() const { + + return d->filetype_ == FileType::FLAC || + d->filetype_ == FileType::WavPack || + d->filetype_ == FileType::OggFlac || + d->filetype_ == FileType::OggVorbis || + d->filetype_ == FileType::OggOpus || + d->filetype_ == FileType::OggSpeex || + d->filetype_ == FileType::MPEG || + d->filetype_ == FileType::MP4 || + d->filetype_ == FileType::ASF || + d->filetype_ == FileType::MPC || + d->filetype_ == FileType::APE; + +} + +bool Song::comment_supported() const { + return additional_tags_supported(); +} + +bool Song::lyrics_supported() const { + return additional_tags_supported(); +} + +bool Song::save_embedded_cover_supported(const FileType filetype) { + + return filetype == FileType::FLAC || + filetype == FileType::OggVorbis || + filetype == FileType::OggOpus || + filetype == FileType::MPEG || + filetype == FileType::MP4; + +} + +QString Song::sortable(const QString &v) { + + QString copy = v.toLower(); + + for (const auto &i : kArticles) { + if (copy.startsWith(i)) { + qint64 ilen = i.length(); + return copy.right(copy.length() - ilen) + ", " + copy.left(ilen - 1); + } + } + + return copy; + +} + QString Song::JoinSpec(const QString &table) { return Utilities::Prepend(table + ".", kColumns).join(", "); } +QString Song::PrettyTitle() const { + + QString title(d->title_); + + if (title.isEmpty()) title = d->basefilename_; + if (title.isEmpty()) title = d->url_.toString(); + + return title; + +} + +QString Song::PrettyTitleWithArtist() const { + + QString title(PrettyTitle()); + + if (!d->artist_.isEmpty()) title = d->artist_ + " - " + title; + + return title; + +} + +QString Song::PrettyLength() const { + + if (length_nanosec() == -1) return QString(); + + return Utilities::PrettyTimeNanosec(length_nanosec()); + +} + +QString Song::PrettyYear() const { + + if (d->year_ == -1) return QString(); + + return QString::number(d->year_); + +} + +QString Song::PrettyOriginalYear() const { + + if (effective_originalyear() == -1) return QString(); + + return QString::number(effective_originalyear()); + +} + +QString Song::TitleWithCompilationArtist() const { + + QString title(d->title_); + + if (title.isEmpty()) title = d->basefilename_; + + if (is_compilation() && !d->artist_.isEmpty() && !d->artist_.contains("various", Qt::CaseInsensitive)) title = d->artist_ + " - " + title; + + return title; + +} + +QString Song::SampleRateBitDepthToText() const { + + if (d->samplerate_ == -1) return QString(""); + if (d->bitdepth_ == -1) return QString("%1 hz").arg(d->samplerate_); + + return QString("%1 hz / %2 bit").arg(d->samplerate_).arg(d->bitdepth_); + +} + +QString Song::PrettyRating() const { + + float rating = d->rating_; + + if (rating == -1.0F) return "0"; + + return QString::number(static_cast(rating * 100)); + +} + +bool Song::IsEditable() const { + return d->valid_ && d->url_.isValid() && (d->url_.isLocalFile() || d->source_ == Source::Stream) && !has_cue(); +} + +bool Song::IsMetadataEqual(const Song &other) const { + + return d->title_ == other.d->title_ && + d->album_ == other.d->album_ && + d->artist_ == other.d->artist_ && + d->albumartist_ == other.d->albumartist_ && + d->track_ == other.d->track_ && + d->disc_ == other.d->disc_ && + d->year_ == other.d->year_ && + d->originalyear_ == other.d->originalyear_ && + d->genre_ == other.d->genre_ && + d->compilation_ == other.d->compilation_ && + d->composer_ == other.d->composer_ && + d->performer_ == other.d->performer_ && + d->grouping_ == other.d->grouping_ && + d->comment_ == other.d->comment_ && + d->lyrics_ == other.d->lyrics_ && + d->artist_id_ == other.d->artist_id_ && + d->album_id_ == other.d->album_id_ && + d->song_id_ == other.d->song_id_ && + d->beginning_ == other.d->beginning_ && + length_nanosec() == other.length_nanosec() && + d->bitrate_ == other.d->bitrate_ && + d->samplerate_ == other.d->samplerate_ && + d->bitdepth_ == other.d->bitdepth_ && + d->cue_path_ == other.d->cue_path_; +} + +bool Song::IsPlayStatisticsEqual(const Song &other) const { + + return d->playcount_ == other.d->playcount_ && + d->skipcount_ == other.d->skipcount_ && + d->lastplayed_ == other.d->lastplayed_; + +} + +bool Song::IsRatingEqual(const Song &other) const { + + return d->rating_ == other.d->rating_; + +} + +bool Song::IsFingerprintEqual(const Song &other) const { + + return d->fingerprint_ == other.d->fingerprint_; + +} + +bool Song::IsAcoustIdEqual(const Song &other) const { + + return d->acoustid_id_ == other.d->acoustid_id_ && d->acoustid_fingerprint_ == other.d->acoustid_fingerprint_; + +} + +bool Song::IsMusicBrainzEqual(const Song &other) const { + + return d->musicbrainz_album_artist_id_ == other.d->musicbrainz_album_artist_id_ && + d->musicbrainz_artist_id_ == other.d->musicbrainz_artist_id_ && + d->musicbrainz_original_artist_id_ == other.d->musicbrainz_original_artist_id_ && + d->musicbrainz_album_id_ == other.d->musicbrainz_album_id_ && + d->musicbrainz_original_album_id_ == other.d->musicbrainz_original_album_id_ && + d->musicbrainz_recording_id_ == other.d->musicbrainz_recording_id_ && + d->musicbrainz_track_id_ == other.d->musicbrainz_track_id_ && + d->musicbrainz_disc_id_ == other.d->musicbrainz_disc_id_ && + d->musicbrainz_release_group_id_ == other.d->musicbrainz_release_group_id_ && + d->musicbrainz_work_id_ == other.d->musicbrainz_work_id_; + +} + +bool Song::IsArtEqual(const Song &other) const { + + return d->art_embedded_ == other.d->art_embedded_ && + d->art_automatic_ == other.d->art_automatic_ && + d->art_manual_ == other.d->art_manual_ && + d->art_unset_ == other.d->art_unset_; + +} + +bool Song::IsAllMetadataEqual(const Song &other) const { + + return IsMetadataEqual(other) && + IsPlayStatisticsEqual(other) && + IsRatingEqual(other) && + IsAcoustIdEqual(other) && + IsMusicBrainzEqual(other) && + IsArtEqual(other); + +} + +bool Song::IsOnSameAlbum(const Song &other) const { + + if (is_compilation() != other.is_compilation()) return false; + + if (has_cue() && other.has_cue() && cue_path() == other.cue_path()) { + return true; + } + + if (is_compilation() && album() == other.album()) return true; + + return effective_album() == other.effective_album() && effective_albumartist() == other.effective_albumartist(); + +} + +bool Song::IsSimilar(const Song &other) const { + return title().compare(other.title(), Qt::CaseInsensitive) == 0 && + artist().compare(other.artist(), Qt::CaseInsensitive) == 0 && + album().compare(other.album(), Qt::CaseInsensitive) == 0; +} + Song::Source Song::SourceFromURL(const QUrl &url) { if (url.isLocalFile()) return Source::LocalFile; @@ -791,6 +990,7 @@ QIcon Song::IconForFiletype(const FileType filetype) { } bool Song::IsFileLossless() const { + switch (filetype()) { case FileType::WAV: case FileType::FLAC: @@ -807,6 +1007,7 @@ bool Song::IsFileLossless() const { default: return false; } + } Song::FileType Song::FiletypeByMimetype(const QString &mimetype) { @@ -991,10 +1192,7 @@ void Song::InitFromProtobuf(const spb::tagreader::SongMetadata &pb) { d->rating_ = pb.rating(); } - if (pb.has_art_automatic()) { - QByteArray art_automatic(pb.art_automatic().data(), static_cast(pb.art_automatic().size())); - if (!art_automatic.isEmpty()) set_art_automatic(QUrl::fromLocalFile(art_automatic)); - } + d->art_embedded_ = pb.has_art_embedded(); d->acoustid_id_ = QString::fromUtf8(pb.acoustid_id().data(), pb.acoustid_id().size()); d->acoustid_fingerprint_ = QString::fromUtf8(pb.acoustid_fingerprint().data(), pb.acoustid_fingerprint().size()); @@ -1019,7 +1217,6 @@ void Song::InitFromProtobuf(const spb::tagreader::SongMetadata &pb) { void Song::ToProtobuf(spb::tagreader::SongMetadata *pb) const { const QByteArray url(d->url_.toEncoded()); - const QByteArray art_automatic(d->art_automatic_.toEncoded()); pb->set_valid(d->valid_); pb->set_title(d->title_.toStdString()); @@ -1051,7 +1248,7 @@ void Song::ToProtobuf(spb::tagreader::SongMetadata *pb) const { pb->set_skipcount(d->skipcount_); pb->set_lastplayed(d->lastplayed_); pb->set_lastseen(d->lastseen_); - pb->set_art_automatic(art_automatic.constData(), art_automatic.size()); + pb->set_art_embedded(d->art_embedded_); pb->set_rating(d->rating_); pb->set_acoustid_id(d->acoustid_id_.toStdString()); @@ -1116,22 +1313,32 @@ void Song::InitFromQuery(const SqlRow &q, const bool reliable_metadata) { d->compilation_detected_ = q.ValueToBool("compilation_detected"); d->compilation_on_ = q.ValueToBool("compilation_on"); d->compilation_off_ = q.ValueToBool("compilation_off"); - QString art_automatic = q.ValueToString("art_automatic"); + + d->art_embedded_ = q.ValueToBool("art_embedded"); + d->art_unset_ = q.ValueToBool("art_unset"); + + const QString art_automatic = q.ValueToString("art_automatic"); if (!art_automatic.isEmpty()) { - if (art_automatic.contains(QRegularExpression("..+:.*"))) { - set_art_automatic(QUrl::fromEncoded(art_automatic.toUtf8())); - } - else { - set_art_automatic(QUrl::fromLocalFile(art_automatic)); + QUrl url_art_automatic = QUrl::fromEncoded(art_automatic.toUtf8()); + if (url_art_automatic.isValid()) { + if (url_art_automatic.path() == kEmbeddedCover) { + d->art_embedded_ = true; + } + else { + d->art_automatic_ = url_art_automatic; + } } } - QString art_manual = q.ValueToString("art_manual"); + const QString art_manual = q.ValueToString("art_manual"); if (!art_manual.isEmpty()) { - if (art_manual.contains(QRegularExpression("..+:.*"))) { - set_art_manual(QUrl::fromEncoded(art_manual.toUtf8())); - } - else { - set_art_manual(QUrl::fromLocalFile(art_manual)); + const QUrl url_art_manual = QUrl::fromEncoded(art_manual.toUtf8()); + if (url_art_manual.isValid()) { + if (url_art_manual.path() == kManuallyUnsetCover) { + d->art_unset_ = true; + } + else { + d->art_manual_ = url_art_manual; + } } } @@ -1173,8 +1380,8 @@ void Song::InitFromFilePartial(const QString &filename, const QFileInfo &fileinf void Song::InitArtManual() { - // If we don't have an art, check if we have one in the cache - if (d->art_manual_.isEmpty() && d->art_automatic_.isEmpty() && !effective_albumartist().isEmpty() && !effective_album().isEmpty()) { + // If we don't have cover art, check if we have one in the cache + if (d->art_manual_.isEmpty() && !effective_albumartist().isEmpty() && !effective_album().isEmpty()) { QString filename(CoverUtils::Sha1CoverHash(effective_albumartist(), effective_album()).toHex() + ".jpg"); QString path(ImageCacheDir(d->source_) + "/" + filename); if (QFile::exists(path)) { @@ -1186,7 +1393,7 @@ void Song::InitArtManual() { void Song::InitArtAutomatic() { - if (d->source_ == Source::LocalFile && d->url_.isLocalFile() && d->art_automatic_.isEmpty()) { + if (d->art_automatic_.isEmpty() && d->source_ == Source::LocalFile && d->url_.isLocalFile()) { // Pick the first image file in the album directory. QFileInfo file(d->url_.toLocalFile()); QDir dir(file.path()); @@ -1385,6 +1592,127 @@ void Song::ToMTP(LIBMTP_track_t *track) const { } #endif +void Song::BindToQuery(SqlQuery *query) const { + + // Remember to bind these in the same order as kBindSpec + + query->BindStringValue(":title", d->title_); + query->BindStringValue(":album", d->album_); + query->BindStringValue(":artist", d->artist_); + query->BindStringValue(":albumartist", d->albumartist_); + query->BindIntValue(":track", d->track_); + query->BindIntValue(":disc", d->disc_); + query->BindIntValue(":year", d->year_); + query->BindIntValue(":originalyear", d->originalyear_); + query->BindStringValue(":genre", d->genre_); + query->BindBoolValue(":compilation", d->compilation_); + query->BindStringValue(":composer", d->composer_); + query->BindStringValue(":performer", d->performer_); + query->BindStringValue(":grouping", d->grouping_); + query->BindStringValue(":comment", d->comment_); + query->BindStringValue(":lyrics", d->lyrics_); + + query->BindStringValue(":artist_id", d->artist_id_); + query->BindStringValue(":album_id", d->album_id_); + query->BindStringValue(":song_id", d->song_id_); + + query->BindValue(":beginning", d->beginning_); + query->BindLongLongValue(":length", length_nanosec()); + + query->BindIntValue(":bitrate", d->bitrate_); + query->BindIntValue(":samplerate", d->samplerate_); + query->BindIntValue(":bitdepth", d->bitdepth_); + + query->BindValue(":source", static_cast(d->source_)); + query->BindNotNullIntValue(":directory_id", d->directory_id_); + query->BindUrlValue(":url", d->url_); + query->BindValue(":filetype", static_cast(d->filetype_)); + query->BindLongLongValueOrZero(":filesize", d->filesize_); + query->BindLongLongValueOrZero(":mtime", d->mtime_); + query->BindLongLongValueOrZero(":ctime", d->ctime_); + query->BindBoolValue(":unavailable", d->unavailable_); + + query->BindStringValue(":fingerprint", d->fingerprint_); + + query->BindValue(":playcount", d->playcount_); + query->BindValue(":skipcount", d->skipcount_); + query->BindLongLongValue(":lastplayed", d->lastplayed_); + query->BindLongLongValue(":lastseen", d->lastseen_); + + query->BindBoolValue(":compilation_detected", d->compilation_detected_); + query->BindBoolValue(":compilation_on", d->compilation_on_); + query->BindBoolValue(":compilation_off", d->compilation_off_); + query->BindBoolValue(":compilation_effective", is_compilation()); + + query->BindBoolValue(":art_embedded", d->art_embedded_); + query->BindUrlValue(":art_automatic", d->art_automatic_); + query->BindUrlValue(":art_manual", d->art_manual_); + query->BindBoolValue(":art_unset", d->art_unset_); + + query->BindStringValue(":effective_albumartist", effective_albumartist()); + query->BindIntValue(":effective_originalyear", effective_originalyear()); + + query->BindValue(":cue_path", d->cue_path_); + + query->BindFloatValue(":rating", d->rating_); + + query->BindStringValue(":acoustid_id", d->acoustid_id_); + query->BindStringValue(":acoustid_fingerprint", d->acoustid_fingerprint_); + + query->BindStringValue(":musicbrainz_album_artist_id", d->musicbrainz_album_artist_id_); + query->BindStringValue(":musicbrainz_artist_id", d->musicbrainz_artist_id_); + query->BindStringValue(":musicbrainz_original_artist_id", d->musicbrainz_original_artist_id_); + query->BindStringValue(":musicbrainz_album_id", d->musicbrainz_album_id_); + query->BindStringValue(":musicbrainz_original_album_id", d->musicbrainz_original_album_id_); + query->BindStringValue(":musicbrainz_recording_id", d->musicbrainz_recording_id_); + query->BindStringValue(":musicbrainz_track_id", d->musicbrainz_track_id_); + query->BindStringValue(":musicbrainz_disc_id", d->musicbrainz_disc_id_); + query->BindStringValue(":musicbrainz_release_group_id", d->musicbrainz_release_group_id_); + query->BindStringValue(":musicbrainz_work_id", d->musicbrainz_work_id_); + +} + +void Song::BindToFtsQuery(SqlQuery *query) const { + + query->BindValue(":ftstitle", d->title_); + query->BindValue(":ftsalbum", d->album_); + query->BindValue(":ftsartist", d->artist_); + query->BindValue(":ftsalbumartist", d->albumartist_); + query->BindValue(":ftscomposer", d->composer_); + query->BindValue(":ftsperformer", d->performer_); + query->BindValue(":ftsgrouping", d->grouping_); + query->BindValue(":ftsgenre", d->genre_); + query->BindValue(":ftscomment", d->comment_); + +} + +void Song::ToXesam(QVariantMap *map) const { + + using mpris::AddMetadata; + using mpris::AddMetadataAsList; + using mpris::AsMPRISDateTimeType; + + AddMetadata("xesam:url", effective_stream_url().toString(), map); + AddMetadata("xesam:title", PrettyTitle(), map); + AddMetadataAsList("xesam:artist", artist(), map); + AddMetadata("xesam:album", album(), map); + AddMetadataAsList("xesam:albumArtist", albumartist(), map); + AddMetadata("mpris:length", (length_nanosec() / kNsecPerUsec), map); + AddMetadata("xesam:trackNumber", track(), map); + AddMetadataAsList("xesam:genre", genre(), map); + AddMetadata("xesam:discNumber", disc(), map); + AddMetadataAsList("xesam:comment", comment(), map); + AddMetadata("xesam:contentCreated", AsMPRISDateTimeType(ctime()), map); + AddMetadata("xesam:lastUsed", AsMPRISDateTimeType(lastplayed()), map); + AddMetadataAsList("xesam:composer", composer(), map); + AddMetadata("xesam:useCount", static_cast(playcount()), map); + + if (rating() != -1.0) { + AddMetadata("xesam:userRating", rating(), map); + } + +} + bool Song::MergeFromEngineMetadata(const EngineMetadata &engine_metadata) { d->valid_ = true; @@ -1439,337 +1767,6 @@ bool Song::MergeFromEngineMetadata(const EngineMetadata &engine_metadata) { } -void Song::BindToQuery(SqlQuery *query) const { - - // Remember to bind these in the same order as kBindSpec - - query->BindStringValue(":title", d->title_); - query->BindStringValue(":album", d->album_); - query->BindStringValue(":artist", d->artist_); - query->BindStringValue(":albumartist", d->albumartist_); - query->BindIntValue(":track", d->track_); - query->BindIntValue(":disc", d->disc_); - query->BindIntValue(":year", d->year_); - query->BindIntValue(":originalyear", d->originalyear_); - query->BindStringValue(":genre", d->genre_); - query->BindBoolValue(":compilation", d->compilation_); - query->BindStringValue(":composer", d->composer_); - query->BindStringValue(":performer", d->performer_); - query->BindStringValue(":grouping", d->grouping_); - query->BindStringValue(":comment", d->comment_); - query->BindStringValue(":lyrics", d->lyrics_); - - query->BindStringValue(":artist_id", d->artist_id_); - query->BindStringValue(":album_id", d->album_id_); - query->BindStringValue(":song_id", d->song_id_); - - query->BindValue(":beginning", d->beginning_); - query->BindLongLongValue(":length", length_nanosec()); - - query->BindIntValue(":bitrate", d->bitrate_); - query->BindIntValue(":samplerate", d->samplerate_); - query->BindIntValue(":bitdepth", d->bitdepth_); - - query->BindValue(":source", static_cast(d->source_)); - query->BindNotNullIntValue(":directory_id", d->directory_id_); - query->BindUrlValue(":url", d->url_); - query->BindValue(":filetype", static_cast(d->filetype_)); - query->BindLongLongValueOrZero(":filesize", d->filesize_); - query->BindLongLongValueOrZero(":mtime", d->mtime_); - query->BindLongLongValueOrZero(":ctime", d->ctime_); - query->BindBoolValue(":unavailable", d->unavailable_); - - query->BindStringValue(":fingerprint", d->fingerprint_); - - query->BindValue(":playcount", d->playcount_); - query->BindValue(":skipcount", d->skipcount_); - query->BindLongLongValue(":lastplayed", d->lastplayed_); - query->BindLongLongValue(":lastseen", d->lastseen_); - - query->BindBoolValue(":compilation_detected", d->compilation_detected_); - query->BindBoolValue(":compilation_on", d->compilation_on_); - query->BindBoolValue(":compilation_off", d->compilation_off_); - query->BindBoolValue(":compilation_effective", is_compilation()); - - query->BindUrlValue(":art_automatic", d->art_automatic_); - query->BindUrlValue(":art_manual", d->art_manual_); - - query->BindStringValue(":effective_albumartist", effective_albumartist()); - query->BindIntValue(":effective_originalyear", effective_originalyear()); - - query->BindValue(":cue_path", d->cue_path_); - - query->BindFloatValue(":rating", d->rating_); - - query->BindStringValue(":acoustid_id", d->acoustid_id_); - query->BindStringValue(":acoustid_fingerprint", d->acoustid_fingerprint_); - - query->BindStringValue(":musicbrainz_album_artist_id", d->musicbrainz_album_artist_id_); - query->BindStringValue(":musicbrainz_artist_id", d->musicbrainz_artist_id_); - query->BindStringValue(":musicbrainz_original_artist_id", d->musicbrainz_original_artist_id_); - query->BindStringValue(":musicbrainz_album_id", d->musicbrainz_album_id_); - query->BindStringValue(":musicbrainz_original_album_id", d->musicbrainz_original_album_id_); - query->BindStringValue(":musicbrainz_recording_id", d->musicbrainz_recording_id_); - query->BindStringValue(":musicbrainz_track_id", d->musicbrainz_track_id_); - query->BindStringValue(":musicbrainz_disc_id", d->musicbrainz_disc_id_); - query->BindStringValue(":musicbrainz_release_group_id", d->musicbrainz_release_group_id_); - query->BindStringValue(":musicbrainz_work_id", d->musicbrainz_work_id_); - -} - -void Song::BindToFtsQuery(SqlQuery *query) const { - - query->BindValue(":ftstitle", d->title_); - query->BindValue(":ftsalbum", d->album_); - query->BindValue(":ftsartist", d->artist_); - query->BindValue(":ftsalbumartist", d->albumartist_); - query->BindValue(":ftscomposer", d->composer_); - query->BindValue(":ftsperformer", d->performer_); - query->BindValue(":ftsgrouping", d->grouping_); - query->BindValue(":ftsgenre", d->genre_); - query->BindValue(":ftscomment", d->comment_); - -} - -QString Song::PrettyTitle() const { - - QString title(d->title_); - - if (title.isEmpty()) title = d->basefilename_; - if (title.isEmpty()) title = d->url_.toString(); - - return title; - -} - -QString Song::PrettyTitleWithArtist() const { - - QString title(PrettyTitle()); - - if (!d->artist_.isEmpty()) title = d->artist_ + " - " + title; - - return title; - -} - -QString Song::PrettyLength() const { - - if (length_nanosec() == -1) return QString(); - - return Utilities::PrettyTimeNanosec(length_nanosec()); - -} - -QString Song::PrettyYear() const { - - if (d->year_ == -1) return QString(); - - return QString::number(d->year_); - -} - -QString Song::PrettyOriginalYear() const { - - if (effective_originalyear() == -1) return QString(); - - return QString::number(effective_originalyear()); - -} - -QString Song::TitleWithCompilationArtist() const { - - QString title(d->title_); - - if (title.isEmpty()) title = d->basefilename_; - - if (is_compilation() && !d->artist_.isEmpty() && !d->artist_.contains("various", Qt::CaseInsensitive)) title = d->artist_ + " - " + title; - - return title; - -} - -QString Song::SampleRateBitDepthToText() const { - - if (d->samplerate_ == -1) return QString(""); - if (d->bitdepth_ == -1) return QString("%1 hz").arg(d->samplerate_); - - return QString("%1 hz / %2 bit").arg(d->samplerate_).arg(d->bitdepth_); - -} - -QString Song::PrettyRating() const { - - float rating = d->rating_; - - if (rating == -1.0F) return "0"; - - return QString::number(static_cast(rating * 100)); - -} - -bool Song::IsMetadataEqual(const Song &other) const { - - return d->title_ == other.d->title_ && - d->album_ == other.d->album_ && - d->artist_ == other.d->artist_ && - d->albumartist_ == other.d->albumartist_ && - d->track_ == other.d->track_ && - d->disc_ == other.d->disc_ && - d->year_ == other.d->year_ && - d->originalyear_ == other.d->originalyear_ && - d->genre_ == other.d->genre_ && - d->compilation_ == other.d->compilation_ && - d->composer_ == other.d->composer_ && - d->performer_ == other.d->performer_ && - d->grouping_ == other.d->grouping_ && - d->comment_ == other.d->comment_ && - d->lyrics_ == other.d->lyrics_ && - d->artist_id_ == other.d->artist_id_ && - d->album_id_ == other.d->album_id_ && - d->song_id_ == other.d->song_id_ && - d->beginning_ == other.d->beginning_ && - length_nanosec() == other.length_nanosec() && - d->bitrate_ == other.d->bitrate_ && - d->samplerate_ == other.d->samplerate_ && - d->bitdepth_ == other.d->bitdepth_ && - d->cue_path_ == other.d->cue_path_; -} - -bool Song::IsPlayStatisticsEqual(const Song &other) const { - - return d->playcount_ == other.d->playcount_ && - d->skipcount_ == other.d->skipcount_ && - d->lastplayed_ == other.d->lastplayed_; - -} - -bool Song::IsRatingEqual(const Song &other) const { - - return d->rating_ == other.d->rating_; - -} - -bool Song::IsFingerprintEqual(const Song &other) const { - - return d->fingerprint_ == other.d->fingerprint_; - -} - -bool Song::IsAcoustIdEqual(const Song &other) const { - - return d->acoustid_id_ == other.d->acoustid_id_ && d->acoustid_fingerprint_ == other.d->acoustid_fingerprint_; - -} - -bool Song::IsMusicBrainzEqual(const Song &other) const { - - return d->musicbrainz_album_artist_id_ == other.d->musicbrainz_album_artist_id_ && - d->musicbrainz_artist_id_ == other.d->musicbrainz_artist_id_ && - d->musicbrainz_original_artist_id_ == other.d->musicbrainz_original_artist_id_ && - d->musicbrainz_album_id_ == other.d->musicbrainz_album_id_ && - d->musicbrainz_original_album_id_ == other.d->musicbrainz_original_album_id_ && - d->musicbrainz_recording_id_ == other.d->musicbrainz_recording_id_ && - d->musicbrainz_track_id_ == other.d->musicbrainz_track_id_ && - d->musicbrainz_disc_id_ == other.d->musicbrainz_disc_id_ && - d->musicbrainz_release_group_id_ == other.d->musicbrainz_release_group_id_ && - d->musicbrainz_work_id_ == other.d->musicbrainz_work_id_; - -} - -bool Song::IsArtEqual(const Song &other) const { - - return d->art_automatic_ == other.d->art_automatic_ && d->art_manual_ == other.d->art_manual_; - -} - -bool Song::IsAllMetadataEqual(const Song &other) const { - - return IsMetadataEqual(other) && - IsPlayStatisticsEqual(other) && - IsRatingEqual(other) && - IsAcoustIdEqual(other) && - IsMusicBrainzEqual(other) && - IsArtEqual(other); - -} - -bool Song::IsEditable() const { - return d->valid_ && d->url_.isValid() && (d->url_.isLocalFile() || d->source_ == Source::Stream) && !has_cue(); -} - -bool Song::operator==(const Song &other) const { - return source() == other.source() && url() == other.url() && beginning_nanosec() == other.beginning_nanosec(); -} - -bool Song::operator!=(const Song &other) const { - return source() != other.source() || url() != other.url() || beginning_nanosec() != other.beginning_nanosec(); -} - -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -size_t qHash(const Song &song) { -#else -uint qHash(const Song &song) { -#endif - // Should compare the same fields as operator== - return qHash(song.url().toString()) ^ qHash(song.beginning_nanosec()); -} - -bool Song::IsSimilar(const Song &other) const { - return title().compare(other.title(), Qt::CaseInsensitive) == 0 && - artist().compare(other.artist(), Qt::CaseInsensitive) == 0 && - album().compare(other.album(), Qt::CaseInsensitive) == 0; -} - -size_t HashSimilar(const Song &song) { - // Should compare the same fields as function IsSimilar - return qHash(song.title().toLower()) ^ qHash(song.artist().toLower()) ^ qHash(song.album().toLower()); -} - -bool Song::IsOnSameAlbum(const Song &other) const { - - if (is_compilation() != other.is_compilation()) return false; - - if (has_cue() && other.has_cue() && cue_path() == other.cue_path()) { - return true; - } - - if (is_compilation() && album() == other.album()) return true; - - return effective_album() == other.effective_album() && effective_albumartist() == other.effective_albumartist(); - -} - -QString Song::AlbumKey() const { - return QString("%1|%2|%3").arg(is_compilation() ? "_compilation" : effective_albumartist(), has_cue() ? cue_path() : "", effective_album()); -} - -void Song::ToXesam(QVariantMap *map) const { - - using mpris::AddMetadata; - using mpris::AddMetadataAsList; - using mpris::AsMPRISDateTimeType; - - AddMetadata("xesam:url", effective_stream_url().toString(), map); - AddMetadata("xesam:title", PrettyTitle(), map); - AddMetadataAsList("xesam:artist", artist(), map); - AddMetadata("xesam:album", album(), map); - AddMetadataAsList("xesam:albumArtist", albumartist(), map); - AddMetadata("mpris:length", (length_nanosec() / kNsecPerUsec), map); - AddMetadata("xesam:trackNumber", track(), map); - AddMetadataAsList("xesam:genre", genre(), map); - AddMetadata("xesam:discNumber", disc(), map); - AddMetadataAsList("xesam:comment", comment(), map); - AddMetadata("xesam:contentCreated", AsMPRISDateTimeType(ctime()), map); - AddMetadata("xesam:lastUsed", AsMPRISDateTimeType(lastplayed()), map); - AddMetadataAsList("xesam:composer", composer(), map); - AddMetadata("xesam:useCount", static_cast(playcount()), map); - - if (rating() != -1.0) { - AddMetadata("xesam:userRating", rating(), map); - } - -} - void Song::MergeUserSetData(const Song &other, const bool merge_playcount, const bool merge_rating) { if (merge_playcount && other.playcount() > 0) { @@ -1783,7 +1780,26 @@ void Song::MergeUserSetData(const Song &other, const bool merge_playcount, const set_skipcount(other.skipcount()); set_lastplayed(other.lastplayed()); set_art_manual(other.art_manual()); + set_art_unset(other.art_unset()); set_compilation_on(other.compilation_on()); set_compilation_off(other.compilation_off()); } + +QString Song::AlbumKey() const { + return QString("%1|%2|%3").arg(is_compilation() ? "_compilation" : effective_albumartist(), has_cue() ? cue_path() : "", effective_album()); +} + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +size_t qHash(const Song &song) { +#else +uint qHash(const Song &song) { +#endif + // Should compare the same fields as operator== + return qHash(song.url().toString()) ^ qHash(song.beginning_nanosec()); +} + +size_t HashSimilar(const Song &song) { + // Should compare the same fields as function IsSimilar + return qHash(song.title().toLower()) ^ qHash(song.artist().toLower()) ^ qHash(song.album().toLower()); +} diff --git a/src/core/song.h b/src/core/song.h index 293a5055..93b24d60 100644 --- a/src/core/song.h +++ b/src/core/song.h @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2023, 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 @@ -109,10 +109,6 @@ class Song { Stream = 91 }; - Song(const Source source = Source::Unknown); - Song(const Song &other); - ~Song(); - static const QStringList kColumns; static const QString kColumnSpec; static const QString kBindSpec; @@ -123,9 +119,6 @@ class Song { static const QString kFtsBindSpec; static const QString kFtsUpdateSpec; - static const QString kManuallyUnsetCover; - static const QString kEmbeddedCover; - static const QRegularExpression kAlbumRemoveDisc; static const QRegularExpression kAlbumRemoveMisc; static const QRegularExpression kTitleRemoveMisc; @@ -134,77 +127,22 @@ class Song { static const QStringList kAcceptedExtensions; - static QString JoinSpec(const QString &table); + Song(const Source source = Source::Unknown); + Song(const Song &other); + ~Song(); - static Source SourceFromURL(const QUrl &url); - static QString TextForSource(const Source source); - static QString DescriptionForSource(const Source source); - static Source SourceFromText(const QString &source); - static QIcon IconForSource(const Source source); - static QString TextForFiletype(const FileType filetype); - static QString ExtensionForFiletype(const FileType filetype); - static QIcon IconForFiletype(const FileType filetype); - - QString TextForSource() const { return TextForSource(source()); } - QString DescriptionForSource() const { return DescriptionForSource(source()); } - QIcon IconForSource() const { return IconForSource(source()); } - QString TextForFiletype() const { return TextForFiletype(filetype()); } - QIcon IconForFiletype() const { return IconForFiletype(filetype()); } - - bool IsFileLossless() const; - static FileType FiletypeByMimetype(const QString &mimetype); - static FileType FiletypeByDescription(const QString &text); - static FileType FiletypeByExtension(const QString &ext); - static QString ImageCacheDir(const Source source); - - // Sort songs alphabetically using their pretty title - static int CompareSongsName(const Song &song1, const Song &song2); - static void SortSongsListAlphabetically(QList *songs); - - // Constructors - void Init(const QString &title, const QString &artist, const QString &album, const qint64 length_nanosec); - void Init(const QString &title, const QString &artist, const QString &album, const qint64 beginning, const qint64 end); - void InitFromProtobuf(const spb::tagreader::SongMetadata &pb); - void InitFromQuery(const SqlRow &query, const bool reliable_metadata); - void InitFromFilePartial(const QString &filename, const QFileInfo &fileinfo); - void InitArtManual(); - void InitArtAutomatic(); - - bool MergeFromEngineMetadata(const EngineMetadata &engine_metadata); - -#ifdef HAVE_LIBGPOD - void InitFromItdb(_Itdb_Track *track, const QString &prefix); - void ToItdb(_Itdb_Track *track) const; -#endif - -#ifdef HAVE_LIBMTP - void InitFromMTP(const LIBMTP_track_struct *track, const QString &host); - void ToMTP(LIBMTP_track_struct *track) const; -#endif - - // Copies important statistics from the other song to this one, overwriting any data that already exists. - // Useful when you want updated tags from disk but you want to keep user stats. - void MergeUserSetData(const Song &other, const bool merge_playcount, const bool merge_rating); - - // Save - void BindToQuery(SqlQuery *query) const; - void BindToFtsQuery(SqlQuery *query) const; - void ToXesam(QVariantMap *map) const; - void ToProtobuf(spb::tagreader::SongMetadata *pb) const; + bool operator==(const Song &other) const; + bool operator!=(const Song &other) const; + Song &operator=(const Song &other); // Simple accessors - bool is_valid() const; - bool is_unavailable() const; int id() const; + bool is_valid() const; const QString &title() const; - const QString &title_sortable() const; const QString &album() const; - const QString &album_sortable() const; const QString &artist() const; - const QString &artist_sortable() const; const QString &albumartist() const; - const QString &albumartist_sortable() const; int track() const; int disc() const; int year() const; @@ -237,6 +175,7 @@ class Song { qint64 filesize() const; qint64 mtime() const; qint64 ctime() const; + bool unavailable() const; QString fingerprint() const; @@ -246,14 +185,15 @@ class Song { qint64 lastseen() const; bool compilation_detected() const; - bool compilation_off() const; bool compilation_on() const; + bool compilation_off() const; + bool art_embedded() const; const QUrl &art_automatic() const; const QUrl &art_manual() const; + bool art_unset() const; const QString &cue_path() const; - bool has_cue() const; float rating() const; @@ -271,74 +211,16 @@ class Song { const QString &musicbrainz_release_group_id() const; const QString &musicbrainz_work_id() const; - const QString &effective_album() const; - int effective_originalyear() const; - const QString &effective_albumartist() const; - const QString &effective_albumartist_sortable() const; - - bool is_collection_song() const; - bool is_stream() const; - bool is_radio() const; - bool is_cdda() const; - 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; - bool stream_url_can_expire() const; - bool is_module_music() const; - - // Playlist views are special because you don't want to fill in album artists automatically for compilations, but you do for normal albums: - const QString &playlist_albumartist() const; - const QString &playlist_albumartist_sortable() const; - - // 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 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(); }; - - bool additional_tags_supported() const; - bool albumartist_supported() const; - bool composer_supported() const; - bool performer_supported() const; - bool grouping_supported() const; - bool genre_supported() const; - bool compilation_supported() const; - bool rating_supported() const; - bool comment_supported() const; - bool lyrics_supported() const; - - const QUrl &stream_url() const; - const QUrl &effective_stream_url() const; bool init_from_file() const; - // Pretty accessors - QString PrettyTitle() const; - QString PrettyTitleWithArtist() const; - QString PrettyLength() const; - QString PrettyYear() const; - QString PrettyOriginalYear() const; + const QString &title_sortable() const; + const QString &album_sortable() const; + const QString &artist_sortable() const; + const QString &albumartist_sortable() const; - QString TitleWithCompilationArtist() const; - - QString SampleRateBitDepthToText() const; - - QString PrettyRating() const; + const QUrl &stream_url() const; // Setters - bool IsEditable() const; - void set_id(const int id); void set_valid(const bool v); @@ -391,8 +273,10 @@ class Song { void set_compilation_on(const bool v); void set_compilation_off(const bool v); + void set_art_embedded(const bool v); void set_art_automatic(const QUrl &v); void set_art_manual(const QUrl &v); + void set_art_unset(const bool v); void set_cue_path(const QString &v); @@ -414,6 +298,61 @@ class Song { void set_stream_url(const QUrl &v); + const QUrl &effective_stream_url() const; + const QString &effective_albumartist() const; + const QString &effective_albumartist_sortable() const; + const QString &effective_album() const; + int effective_originalyear() const; + const QString &playlist_albumartist() const; + const QString &playlist_albumartist_sortable() const; + + bool is_metadata_good() const; + bool is_collection_song() const; + bool is_stream() const; + bool is_radio() const; + bool is_cdda() const; + bool is_compilation() const; + bool stream_url_can_expire() const; + bool is_module_music() const; + bool has_cue() const; + + bool art_automatic_is_valid() const; + bool art_manual_is_valid() const; + bool has_valid_art() const; + void clear_art_automatic(); + void clear_art_manual(); + + bool additional_tags_supported() const; + bool albumartist_supported() const; + bool composer_supported() const; + bool performer_supported() const; + bool grouping_supported() const; + bool genre_supported() const; + bool compilation_supported() const; + bool rating_supported() const; + bool comment_supported() const; + bool lyrics_supported() const; + + 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(); }; + + static QString JoinSpec(const QString &table); + + // Pretty accessors + QString PrettyTitle() const; + QString PrettyTitleWithArtist() const; + QString PrettyLength() const; + QString PrettyYear() const; + QString PrettyOriginalYear() const; + + QString TitleWithCompilationArtist() const; + + QString SampleRateBitDepthToText() const; + + QString PrettyRating() const; + + bool IsEditable() const; + // Comparison functions bool IsMetadataEqual(const Song &other) const; bool IsPlayStatisticsEqual(const Song &other) const; @@ -427,16 +366,69 @@ class Song { bool IsOnSameAlbum(const Song &other) const; bool IsSimilar(const Song &other) const; - bool operator==(const Song &other) const; - bool operator!=(const Song &other) const; + static Source SourceFromURL(const QUrl &url); + static QString TextForSource(const Source source); + static QString DescriptionForSource(const Source source); + static Source SourceFromText(const QString &source); + static QIcon IconForSource(const Source source); + static QString TextForFiletype(const FileType filetype); + static QString ExtensionForFiletype(const FileType filetype); + static QIcon IconForFiletype(const FileType filetype); + + QString TextForSource() const { return TextForSource(source()); } + QString DescriptionForSource() const { return DescriptionForSource(source()); } + QIcon IconForSource() const { return IconForSource(source()); } + QString TextForFiletype() const { return TextForFiletype(filetype()); } + QIcon IconForFiletype() const { return IconForFiletype(filetype()); } + + bool IsFileLossless() const; + static FileType FiletypeByMimetype(const QString &mimetype); + static FileType FiletypeByDescription(const QString &text); + static FileType FiletypeByExtension(const QString &ext); + static QString ImageCacheDir(const Source source); + + // Sort songs alphabetically using their pretty title + static int CompareSongsName(const Song &song1, const Song &song2); + static void SortSongsListAlphabetically(QList *songs); + + // Constructors + void Init(const QString &title, const QString &artist, const QString &album, const qint64 length_nanosec); + void Init(const QString &title, const QString &artist, const QString &album, const qint64 beginning, const qint64 end); + void InitFromProtobuf(const spb::tagreader::SongMetadata &pb); + void InitFromQuery(const SqlRow &query, const bool reliable_metadata); + void InitFromFilePartial(const QString &filename, const QFileInfo &fileinfo); + void InitArtManual(); + void InitArtAutomatic(); + +#ifdef HAVE_LIBGPOD + void InitFromItdb(_Itdb_Track *track, const QString &prefix); + void ToItdb(_Itdb_Track *track) const; +#endif + +#ifdef HAVE_LIBMTP + void InitFromMTP(const LIBMTP_track_struct *track, const QString &host); + void ToMTP(LIBMTP_track_struct *track) const; +#endif + + // Save + void BindToQuery(SqlQuery *query) const; + void BindToFtsQuery(SqlQuery *query) const; + void ToXesam(QVariantMap *map) const; + void ToProtobuf(spb::tagreader::SongMetadata *pb) const; + + bool MergeFromEngineMetadata(const EngineMetadata &engine_metadata); + + // Copies important statistics from the other song to this one, overwriting any data that already exists. + // Useful when you want updated tags from disk but you want to keep user stats. + void MergeUserSetData(const Song &other, const bool merge_playcount, const bool merge_rating); // Two songs that are on the same album will have the same AlbumKey. // It is more efficient to use IsOnSameAlbum, but this function can be used when you need to hash the key to do fast lookups. QString AlbumKey() const; - Song &operator=(const Song &other); - private: + static const QString kManuallyUnsetCover; + static const QString kEmbeddedCover; struct Private; static QString sortable(const QString &v); diff --git a/src/core/standarditemiconloader.cpp b/src/core/standarditemiconloader.cpp index 65442167..cc48638a 100644 --- a/src/core/standarditemiconloader.cpp +++ b/src/core/standarditemiconloader.cpp @@ -30,6 +30,7 @@ #include #include "covermanager/albumcoverloader.h" +#include "covermanager/albumcoverloaderoptions.h" #include "standarditemiconloader.h" StandardItemIconLoader::StandardItemIconLoader(AlbumCoverLoader *cover_loader, QObject *parent) @@ -37,8 +38,6 @@ StandardItemIconLoader::StandardItemIconLoader(AlbumCoverLoader *cover_loader, Q cover_loader_(cover_loader), model_(nullptr) { - cover_options_.desired_height_ = 16; - QObject::connect(cover_loader_, &AlbumCoverLoader::AlbumCoverLoaded, this, &StandardItemIconLoader::AlbumCoverLoaded); } @@ -58,14 +57,20 @@ void StandardItemIconLoader::SetModel(QAbstractItemModel *model) { void StandardItemIconLoader::LoadIcon(const QUrl &art_automatic, const QUrl &art_manual, QStandardItem *for_item) { - const quint64 id = cover_loader_->LoadImageAsync(cover_options_, art_automatic, art_manual); - pending_covers_[id] = for_item; + AlbumCoverLoaderOptions cover_options(AlbumCoverLoaderOptions::Option::ScaledImage); + cover_options.desired_scaled_size = QSize(16, 16); + const quint64 id = cover_loader_->LoadImageAsync(cover_options, false, art_automatic, art_manual, false); + pending_covers_.insert(id, for_item); } void StandardItemIconLoader::LoadIcon(const Song &song, QStandardItem *for_item) { - const quint64 id = cover_loader_->LoadImageAsync(cover_options_, song); - pending_covers_[id] = for_item; + + AlbumCoverLoaderOptions cover_options(AlbumCoverLoaderOptions::Option::ScaledImage); + cover_options.desired_scaled_size = QSize(16, 16); + const quint64 id = cover_loader_->LoadImageAsync(cover_options, song); + pending_covers_.insert(id, for_item); + } void StandardItemIconLoader::RowsAboutToBeRemoved(const QModelIndex &parent, int begin, int end) { @@ -76,7 +81,7 @@ void StandardItemIconLoader::RowsAboutToBeRemoved(const QModelIndex &parent, int if (item_parent && item_parent->index() == parent && item->index().row() >= begin && item->index().row() <= end) { cover_loader_->CancelTask(it.key()); - it = pending_covers_.erase(it); // clazy:exclude=strict-iterators + it = pending_covers_.erase(it); } else { ++it; @@ -103,7 +108,7 @@ void StandardItemIconLoader::AlbumCoverLoaded(const quint64 id, const AlbumCover QStandardItem *item = pending_covers_.take(id); if (!item) return; - if (result.success && !result.image_scaled.isNull() && result.type != AlbumCoverLoaderResult::Type::ManuallyUnset) { + if (result.success && !result.image_scaled.isNull() && result.type != AlbumCoverLoaderResult::Type::Unset) { item->setIcon(QIcon(QPixmap::fromImage(result.image_scaled))); } diff --git a/src/core/standarditemiconloader.h b/src/core/standarditemiconloader.h index 0d135f71..e9cffa6a 100644 --- a/src/core/standarditemiconloader.h +++ b/src/core/standarditemiconloader.h @@ -29,7 +29,6 @@ #include #include -#include "covermanager/albumcoverloaderoptions.h" #include "covermanager/albumcoverloaderresult.h" class QAbstractItemModel; @@ -38,8 +37,6 @@ class QStandardItem; class Song; class AlbumCoverLoader; -class QModelIndex; - // Uses an AlbumCoverLoader to asynchronously load and set an icon on a QStandardItem. class StandardItemIconLoader : public QObject { Q_OBJECT @@ -47,8 +44,6 @@ class StandardItemIconLoader : public QObject { public: explicit StandardItemIconLoader(AlbumCoverLoader *cover_loader, QObject *parent = nullptr); - AlbumCoverLoaderOptions *options() { return &cover_options_; } - void SetModel(QAbstractItemModel *model); void LoadIcon(const QUrl &art_automatic, const QUrl &art_manual, QStandardItem *for_item); @@ -61,10 +56,7 @@ class StandardItemIconLoader : public QObject { private: AlbumCoverLoader *cover_loader_; - AlbumCoverLoaderOptions cover_options_; - QAbstractItemModel *model_; - QMap pending_covers_; }; diff --git a/src/core/tagreaderclient.cpp b/src/core/tagreaderclient.cpp index 2ea93225..f261cca2 100644 --- a/src/core/tagreaderclient.cpp +++ b/src/core/tagreaderclient.cpp @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2023, 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 @@ -93,25 +93,30 @@ TagReaderReply *TagReaderClient::ReadFile(const QString &filename) { } -TagReaderReply *TagReaderClient::SaveFile(const QString &filename, const Song &metadata, const SaveTags save_tags, const SavePlaycount save_playcount, const SaveRating save_rating, const SaveCoverOptions &save_cover_options) { +TagReaderReply *TagReaderClient::SaveFile(const QString &filename, const Song &metadata, const SaveTypes save_types, const SaveCoverOptions &save_cover_options) { spb::tagreader::Message message; spb::tagreader::SaveFileRequest *request = message.mutable_save_file_request(); const QByteArray filename_data = filename.toUtf8(); request->set_filename(filename_data.constData(), filename_data.length()); - request->set_save_tags(save_tags == SaveTags::On); - request->set_save_playcount(save_playcount == SavePlaycount::On); - request->set_save_rating(save_rating == SaveRating::On); - request->set_save_cover(save_cover_options.enabled); - request->set_cover_is_jpeg(save_cover_options.is_jpeg); + + request->set_save_tags(save_types.testFlag(SaveType::Tags)); + request->set_save_playcount(save_types.testFlag(SaveType::PlayCount)); + request->set_save_rating(save_types.testFlag(SaveType::Rating)); + request->set_save_cover(save_types.testFlag(SaveType::Cover)); + if (save_cover_options.cover_filename.length() > 0) { - const QByteArray cover_filename = filename.toUtf8(); + const QByteArray cover_filename = save_cover_options.cover_filename.toUtf8(); request->set_cover_filename(cover_filename.constData(), cover_filename.length()); } if (save_cover_options.cover_data.length() > 0) { request->set_cover_data(save_cover_options.cover_data.constData(), save_cover_options.cover_data.length()); } + if (save_cover_options.mime_type.length() > 0) { + const QByteArray cover_mime_type = save_cover_options.mime_type.toUtf8(); + request->set_cover_mime_type(cover_mime_type.constData(), cover_mime_type.length()); + } metadata.ToProtobuf(request->mutable_metadata()); ReplyType *reply = worker_pool_->SendMessageWithReply(&message); @@ -139,15 +144,17 @@ TagReaderReply *TagReaderClient::SaveEmbeddedArt(const QString &filename, const const QByteArray filename_data = filename.toUtf8(); request->set_filename(filename_data.constData(), filename_data.length()); - request->set_cover_is_jpeg(save_cover_options.is_jpeg); if (save_cover_options.cover_filename.length() > 0) { - const QByteArray cover_filename = filename.toUtf8(); + const QByteArray cover_filename = save_cover_options.cover_filename.toUtf8(); request->set_cover_filename(cover_filename.constData(), cover_filename.length()); } if (save_cover_options.cover_data.length() > 0) { request->set_cover_data(save_cover_options.cover_data.constData(), save_cover_options.cover_data.length()); } - + if (save_cover_options.mime_type.length() > 0) { + const QByteArray cover_mime_type = save_cover_options.mime_type.toUtf8(); + request->set_cover_mime_type(cover_mime_type.constData(), cover_mime_type.length()); + } return worker_pool_->SendMessageWithReply(&message); @@ -225,13 +232,13 @@ void TagReaderClient::ReadFileBlocking(const QString &filename, Song *song) { } -bool TagReaderClient::SaveFileBlocking(const QString &filename, const Song &metadata, const SaveTags save_tags, const SavePlaycount save_playcount, const SaveRating save_rating, const SaveCoverOptions &save_cover_options) { +bool TagReaderClient::SaveFileBlocking(const QString &filename, const Song &metadata, const SaveTypes save_types, const SaveCoverOptions &save_cover_options) { Q_ASSERT(QThread::currentThread() != thread()); bool ret = false; - TagReaderReply *reply = SaveFile(filename, metadata, save_tags, save_playcount, save_rating, save_cover_options); + TagReaderReply *reply = SaveFile(filename, metadata, save_types, save_cover_options); if (reply->WaitForFinished()) { ret = reply->message().save_file_response().success(); } diff --git a/src/core/tagreaderclient.h b/src/core/tagreaderclient.h index abb92bf1..1ad2dd71 100644 --- a/src/core/tagreaderclient.h +++ b/src/core/tagreaderclient.h @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2011, David Sansome - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2023, 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 @@ -53,35 +53,28 @@ class TagReaderClient : public QObject { void Start(); void ExitAsync(); - enum class SaveTags { - Off, - On - }; - - enum class SavePlaycount { - Off, - On - }; - - enum class SaveRating { - Off, - On + enum class SaveType { + NoType = 0, + Tags = 1, + PlayCount = 2, + Rating = 4, + Cover = 8 }; + Q_DECLARE_FLAGS(SaveTypes, SaveType) class SaveCoverOptions { public: - explicit SaveCoverOptions(const bool _enabled = false, const bool _is_jpeg = false, const QString &_cover_filename = QString(), const QByteArray &_cover_data = QByteArray()) : enabled(_enabled), is_jpeg(_is_jpeg), cover_filename(_cover_filename), cover_data(_cover_data) {} - explicit SaveCoverOptions(const QString &_cover_filename) : enabled(true), is_jpeg(false), cover_filename(_cover_filename) {} - explicit SaveCoverOptions(const QByteArray &_cover_data) : enabled(true), is_jpeg(false), cover_data(_cover_data) {} - bool enabled; - bool is_jpeg; + explicit SaveCoverOptions(const QString &_cover_filename = QString(), const QByteArray &_cover_data = QByteArray(), const QString &_mime_type = QString()) : cover_filename(_cover_filename), cover_data(_cover_data), mime_type(_mime_type) {} + explicit SaveCoverOptions(const QString &_cover_filename, const QString &_mime_type = QString()) : cover_filename(_cover_filename), mime_type(_mime_type) {} + explicit SaveCoverOptions(const QByteArray &_cover_data, const QString &_mime_type = QString()) : cover_data(_cover_data), mime_type(_mime_type) {} QString cover_filename; QByteArray cover_data; + QString mime_type; }; ReplyType *IsMediaFile(const QString &filename); ReplyType *ReadFile(const QString &filename); - ReplyType *SaveFile(const QString &filename, const Song &metadata, const SaveTags save_tags = SaveTags::On, const SavePlaycount save_playcount = SavePlaycount::Off, const SaveRating save_rating = SaveRating::Off, const SaveCoverOptions &save_cover_options = SaveCoverOptions()); + ReplyType *SaveFile(const QString &filename, const Song &metadata, const SaveTypes types = SaveType::Tags, const SaveCoverOptions &save_cover_options = SaveCoverOptions()); ReplyType *LoadEmbeddedArt(const QString &filename); ReplyType *SaveEmbeddedArt(const QString &filename, const SaveCoverOptions &save_cover_options); ReplyType *UpdateSongPlaycount(const Song &metadata); @@ -90,7 +83,7 @@ class TagReaderClient : public QObject { // Convenience functions that call the above functions and wait for a response. // These block the calling thread with a semaphore, and must NOT be called from the TagReaderClient's thread. void ReadFileBlocking(const QString &filename, Song *song); - bool SaveFileBlocking(const QString &filename, const Song &metadata, const SaveTags save_tags = SaveTags::On, const SavePlaycount save_playcount = SavePlaycount::Off, const SaveRating save_rating = SaveRating::Off, const SaveCoverOptions &save_cover_options = SaveCoverOptions()); + bool SaveFileBlocking(const QString &filename, const Song &metadata, const SaveTypes types = SaveType::Tags, const SaveCoverOptions &save_cover_options = SaveCoverOptions()); bool IsMediaFileBlocking(const QString &filename); QByteArray LoadEmbeddedArtBlocking(const QString &filename); QImage LoadEmbeddedArtAsImageBlocking(const QString &filename); diff --git a/src/covermanager/albumcoverchoicecontroller.cpp b/src/covermanager/albumcoverchoicecontroller.cpp index db398715..a1546ee7 100644 --- a/src/covermanager/albumcoverchoicecontroller.cpp +++ b/src/covermanager/albumcoverchoicecontroller.cpp @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2023, 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 @@ -62,10 +62,11 @@ #include "core/application.h" #include "core/song.h" #include "core/iconloader.h" +#include "core/tagreaderclient.h" #include "collection/collectionfilteroptions.h" #include "collection/collectionbackend.h" -#include "settings/collectionsettingspage.h" +#include "settings/coverssettingspage.h" #include "internet/internetservices.h" #include "internet/internetservice.h" #include "albumcoverchoicecontroller.h" @@ -135,22 +136,23 @@ 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); } void AlbumCoverChoiceController::ReloadSettings() { QSettings s; - s.beginGroup(CollectionSettingsPage::kSettingsGroup); - cover_options_.cover_type = static_cast(s.value("save_cover_type", static_cast(CoverOptions::CoverType::Cache)).toInt()); - cover_options_.cover_filename = static_cast(s.value("save_cover_filename", static_cast(CoverOptions::CoverFilename::Pattern)).toInt()); - cover_options_.cover_pattern = s.value("cover_pattern", "%albumartist-%album").toString(); - cover_options_.cover_overwrite = s.value("cover_overwrite", false).toBool(); - cover_options_.cover_lowercase = s.value("cover_lowercase", false).toBool(); - cover_options_.cover_replace_spaces = s.value("cover_replace_spaces", false).toBool(); + s.beginGroup(CoversSettingsPage::kSettingsGroup); + cover_options_.cover_type = static_cast(s.value(CoversSettingsPage::kSaveType, static_cast(CoverOptions::CoverType::Cache)).toInt()); + cover_options_.cover_filename = static_cast(s.value(CoversSettingsPage::kSaveFilename, static_cast(CoverOptions::CoverFilename::Pattern)).toInt()); + cover_options_.cover_pattern = s.value(CoversSettingsPage::kSavePattern, "%albumartist-%album").toString(); + cover_options_.cover_overwrite = s.value(CoversSettingsPage::kSaveOverwrite, false).toBool(); + cover_options_.cover_lowercase = s.value(CoversSettingsPage::kSaveLowercase, false).toBool(); + cover_options_.cover_replace_spaces = s.value(CoversSettingsPage::kSaveReplaceSpaces, false).toBool(); s.endGroup(); + cover_types_ = AlbumCoverLoaderOptions::LoadTypes(); + } QList AlbumCoverChoiceController::GetAllActions() { @@ -170,30 +172,31 @@ QList AlbumCoverChoiceController::GetAllActions() { AlbumCoverImageResult AlbumCoverChoiceController::LoadImageFromFile(Song *song) { - if (!song->url().isLocalFile()) return AlbumCoverImageResult(); + if (!song->url().isValid() || !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()) { - qLog(Error) << "Cover file" << cover_file << "is empty."; - emit Error(tr("Cover file %1 is empty.").arg(cover_file)); - } - else { - result.mime_type = Utilities::MimeTypeFromData(result.image_data); - result.image.loadFromData(result.image_data); - result.cover_url = QUrl::fromLocalFile(cover_file); - } - } - else { + if (!file.open(QIODevice::ReadOnly)) { qLog(Error) << "Failed to open cover file" << cover_file << "for reading:" << file.errorString(); emit Error(tr("Failed to open cover file %1 for reading: %2").arg(cover_file, file.errorString())); + return AlbumCoverImageResult(); + } + result.image_data = file.readAll(); + file.close(); + if (result.image_data.isEmpty()) { + qLog(Error) << "Cover file" << cover_file << "is empty."; + emit Error(tr("Cover file %1 is empty.").arg(cover_file)); + return AlbumCoverImageResult(); + } + + if (result.image.loadFromData(result.image_data)) { + result.cover_url = QUrl::fromLocalFile(cover_file); + result.mime_type = Utilities::MimeTypeFromData(result.image_data); } return result; @@ -202,24 +205,21 @@ AlbumCoverImageResult AlbumCoverChoiceController::LoadImageFromFile(Song *song) QUrl AlbumCoverChoiceController::LoadCoverFromFile(Song *song) { - if (!song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return QUrl(); + if (!song->url().isValid() || !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.isEmpty()) return QUrl(); - - if (QImage(cover_file).isNull()) return QUrl(); + if (cover_file.isEmpty() || QImage(cover_file).isNull()) return QUrl(); switch (get_save_album_cover_type()) { case CoverOptions::CoverType::Embedded: if (song->save_embedded_cover_supported()) { - SaveCoverEmbeddedAutomatic(*song, cover_file); - return QUrl::fromLocalFile(Song::kEmbeddedCover); + SaveCoverEmbeddedToCollectionSongs(*song, cover_file); + return QUrl(); } [[fallthrough]]; case CoverOptions::CoverType::Cache: case CoverOptions::CoverType::Album:{ - QUrl cover_url = QUrl::fromLocalFile(cover_file); + const QUrl cover_url = QUrl::fromLocalFile(cover_file); SaveArtManualToSong(song, cover_url); return cover_url; } @@ -258,17 +258,19 @@ void AlbumCoverChoiceController::SaveCoverToFileManual(const Song &song, const A if (result.is_jpeg() && fileinfo.completeSuffix().compare("jpg", Qt::CaseInsensitive) == 0) { QFile file(save_filename); - if (file.open(QIODevice::WriteOnly)) { - if (file.write(result.image_data) <= 0) { - qLog(Error) << "Failed writing cover to file" << save_filename << file.errorString(); - emit Error(tr("Failed writing cover to file %1: %2").arg(save_filename, file.errorString())); - } - file.close(); - } - else { + if (!file.open(QIODevice::WriteOnly)) { qLog(Error) << "Failed to open cover file" << save_filename << "for writing:" << file.errorString(); emit Error(tr("Failed to open cover file %1 for writing: %2").arg(save_filename, file.errorString())); + file.close(); + return; } + if (file.write(result.image_data) <= 0) { + qLog(Error) << "Failed writing cover to file" << save_filename << file.errorString(); + emit Error(tr("Failed writing cover to file %1: %2").arg(save_filename, file.errorString())); + file.close(); + return; + } + file.close(); } else { if (!result.image.save(save_filename)) { @@ -283,35 +285,26 @@ QString AlbumCoverChoiceController::GetInitialPathForFileDialog(const Song &song // Art automatic is first to show user which cover the album may be using now; // The song is using it if there's no manual path but we cannot use manual path here because it can contain cached paths - if (!song.art_automatic().isEmpty() && !song.art_automatic().path().isEmpty() && !song.has_embedded_cover()) { - if (song.art_automatic().scheme().isEmpty() && QFile::exists(QFileInfo(song.art_automatic().path()).path())) { - return song.art_automatic().path(); - } - else if (song.art_automatic().isLocalFile() && QFile::exists(QFileInfo(song.art_automatic().toLocalFile()).path())) { - return song.art_automatic().toLocalFile(); - } - // If no automatic art, start in the song's folder + if (song.art_automatic_is_valid()) { + return song.art_automatic().toLocalFile(); } - else if (!song.url().isEmpty() && song.url().toLocalFile().contains('/')) { + + // If no automatic art, start in the song's folder + if (!song.url().isEmpty() && song.url().isValid() && song.url().isLocalFile() && song.url().toLocalFile().contains('/')) { return song.url().toLocalFile().section('/', 0, -2) + filename; - // Fallback - start in home } return QDir::home().absolutePath() + filename; } -QUrl AlbumCoverChoiceController::LoadCoverFromURL(Song *song) { +void AlbumCoverChoiceController::LoadCoverFromURL(Song *song) { - if (!song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return QUrl(); + if (!song->url().isValid() || !song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return; const AlbumCoverImageResult result = LoadImageFromURL(); - - if (result.image.isNull()) { - return QUrl(); - } - else { - return SaveCoverAutomatic(song, result); + if (!result.image.isNull()) { + SaveCoverAutomatic(song, result); } } @@ -324,24 +317,21 @@ AlbumCoverImageResult AlbumCoverChoiceController::LoadImageFromURL() { } -QUrl AlbumCoverChoiceController::SearchForCover(Song *song) { +void AlbumCoverChoiceController::SearchForCover(Song *song) { - if (!song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return QUrl(); + if (!song->url().isValid() || !song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return; // Get something sensible to stick in the search box AlbumCoverImageResult result = SearchForImage(song); if (result.is_valid()) { - return SaveCoverAutomatic(song, result); - } - else { - return QUrl(); + SaveCoverAutomatic(song, result); } } AlbumCoverImageResult AlbumCoverChoiceController::SearchForImage(Song *song) { - if (!song->url().isLocalFile()) return AlbumCoverImageResult(); + if (!song->url().isValid() || !song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return AlbumCoverImageResult(); QString album = song->effective_album(); album = album.remove(Song::kAlbumRemoveDisc).remove(Song::kAlbumRemoveMisc); @@ -351,52 +341,38 @@ AlbumCoverImageResult AlbumCoverChoiceController::SearchForImage(Song *song) { } -QUrl AlbumCoverChoiceController::UnsetCover(Song *song, const bool clear_art_automatic) { +void AlbumCoverChoiceController::UnsetCover(Song *song) { - if (!song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return QUrl(); + if (!song->url().isValid() || !song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return; - QUrl cover_url = QUrl::fromLocalFile(Song::kManuallyUnsetCover); - SaveArtManualToSong(song, cover_url, clear_art_automatic); - - return cover_url; + UnsetAlbumCoverForSong(song); } -void AlbumCoverChoiceController::ClearCover(Song *song, const bool clear_art_automatic) { +void AlbumCoverChoiceController::ClearCover(Song *song) { - if (!song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return; + if (!song->url().isValid() || !song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return; - song->clear_art_manual(); - if (clear_art_automatic) song->clear_art_automatic(); - SaveArtManualToSong(song, QUrl(), clear_art_automatic); + ClearAlbumCoverForSong(song); } -bool AlbumCoverChoiceController::DeleteCover(Song *song, const bool manually_unset) { +bool AlbumCoverChoiceController::DeleteCover(Song *song, const bool unset) { - if (!song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return false; + if (!song->url().isValid() || !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(); + if (song->art_embedded() && song->save_embedded_cover_supported()) { + SaveCoverEmbeddedToCollectionSongs(*song, AlbumCoverImageResult()); } bool success = true; - if (!art_automatic.isEmpty()) { + if (song->art_automatic().isValid() && song->art_automatic().isLocalFile()) { + const QString art_automatic = song->art_automatic().toLocalFile(); QFile file(art_automatic); if (file.exists()) { if (file.remove()) { song->clear_art_automatic(); - if (art_automatic == art_manual) song->clear_art_manual(); } else { success = false; @@ -408,12 +384,12 @@ bool AlbumCoverChoiceController::DeleteCover(Song *song, const bool manually_uns } else song->clear_art_automatic(); - if (!art_manual.isEmpty()) { + if (song->art_manual().isValid() && song->art_manual().isLocalFile()) { + const QString art_manual = song->art_manual().toLocalFile(); QFile file(art_manual); if (file.exists()) { if (file.remove()) { song->clear_art_manual(); - if (art_automatic == art_manual) song->clear_art_automatic(); } else { success = false; @@ -426,8 +402,8 @@ bool AlbumCoverChoiceController::DeleteCover(Song *song, const bool manually_uns else song->clear_art_manual(); if (success) { - if (manually_unset) UnsetCover(song, true); - else ClearCover(song, true); + if (unset) UnsetCover(song); + else ClearCover(song); } return success; @@ -436,23 +412,55 @@ bool AlbumCoverChoiceController::DeleteCover(Song *song, const bool manually_uns void AlbumCoverChoiceController::ShowCover(const Song &song, const QImage &image) { - 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()) { - pixmap.setDevicePixelRatio(devicePixelRatioF()); - ShowCover(song, pixmap); - } - } - } - else { + if (!image.isNull()) { QPixmap pixmap = QPixmap::fromImage(image); if (!pixmap.isNull()) { - pixmap.setDevicePixelRatio(devicePixelRatioF()); - ShowCover(song, pixmap); + pixmap.setDevicePixelRatio(devicePixelRatioF()); + ShowCover(song, pixmap); + return; + } + } + + for (const AlbumCoverLoaderOptions::Type type : cover_types_) { + switch (type) { + case AlbumCoverLoaderOptions::Type::Unset: { + if (song.art_unset()) { + return; + } + break; + } + case AlbumCoverLoaderOptions::Type::Manual:{ + QPixmap pixmap; + if (song.art_manual_is_valid() && song.art_manual().isLocalFile() && pixmap.load(song.art_manual().toLocalFile())) { + pixmap.setDevicePixelRatio(devicePixelRatioF()); + ShowCover(song, pixmap); + return; + } + break; + } + case AlbumCoverLoaderOptions::Type::Embedded:{ + if (song.art_embedded() && !song.url().isEmpty() && song.url().isValid() && song.url().isLocalFile()) { + const QImage image_embedded_cover = TagReaderClient::Instance()->LoadEmbeddedArtAsImageBlocking(song.url().toLocalFile()); + if (!image_embedded_cover.isNull()) { + QPixmap pixmap = QPixmap::fromImage(image_embedded_cover); + if (!pixmap.isNull()) { + pixmap.setDevicePixelRatio(devicePixelRatioF()); + ShowCover(song, pixmap); + return; + } + } + } + break; + } + case AlbumCoverLoaderOptions::Type::Automatic:{ + QPixmap pixmap; + if (song.art_automatic_is_valid() && song.art_automatic().isLocalFile() && pixmap.load(song.art_automatic().toLocalFile())) { + pixmap.setDevicePixelRatio(devicePixelRatioF()); + ShowCover(song, pixmap); + return; + } + break; + } } } @@ -507,7 +515,7 @@ quint64 AlbumCoverChoiceController::SearchCoverAutomatically(const Song &song) { quint64 id = cover_fetcher_->FetchAlbumCover(song.effective_albumartist(), song.album(), song.title(), true); - cover_fetching_tasks_[id] = song; + cover_fetching_tasks_.insert(id, song); return id; @@ -530,17 +538,15 @@ void AlbumCoverChoiceController::AlbumCoverFetched(const quint64 id, const Album } -void AlbumCoverChoiceController::SaveArtAutomaticToSong(Song *song, const QUrl &art_automatic) { +void AlbumCoverChoiceController::SaveArtEmbeddedToSong(Song *song, const bool art_embedded) { if (!song->is_valid()) return; - song->set_art_automatic(art_automatic); - if (song->has_embedded_cover()) { - song->clear_art_manual(); - } + song->set_art_embedded(art_embedded); + song->set_art_unset(false); if (song->source() == Song::Source::Collection) { - app_->collection_backend()->UpdateAutomaticAlbumArtAsync(song->effective_albumartist(), song->album(), art_automatic, song->has_embedded_cover()); + app_->collection_backend()->UpdateEmbeddedAlbumArtAsync(song->effective_albumartist(), song->album(), art_embedded); } if (*song == app_->current_albumcover_loader()->last_song()) { @@ -549,17 +555,17 @@ void AlbumCoverChoiceController::SaveArtAutomaticToSong(Song *song, const QUrl & } -void AlbumCoverChoiceController::SaveArtManualToSong(Song *song, const QUrl &art_manual, const bool clear_art_automatic) { +void AlbumCoverChoiceController::SaveArtManualToSong(Song *song, const QUrl &art_manual) { if (!song->is_valid()) return; song->set_art_manual(art_manual); - if (clear_art_automatic) song->clear_art_automatic(); + song->set_art_unset(false); // Update the backends. switch (song->source()) { case Song::Source::Collection: - app_->collection_backend()->UpdateManualAlbumArtAsync(song->effective_albumartist(), song->album(), art_manual, clear_art_automatic); + app_->collection_backend()->UpdateManualAlbumArtAsync(song->effective_albumartist(), song->album(), art_manual); break; case Song::Source::LocalFile: case Song::Source::CDDA: @@ -575,13 +581,13 @@ void AlbumCoverChoiceController::SaveArtManualToSong(Song *song, const QUrl &art 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); + service->artists_collection_backend()->UpdateManualAlbumArtAsync(song->effective_albumartist(), song->album(), art_manual); } if (service->albums_collection_backend()) { - service->albums_collection_backend()->UpdateManualAlbumArtAsync(song->effective_albumartist(), song->album(), art_manual, clear_art_automatic); + service->albums_collection_backend()->UpdateManualAlbumArtAsync(song->effective_albumartist(), song->album(), art_manual); } if (service->songs_collection_backend()) { - service->songs_collection_backend()->UpdateManualAlbumArtAsync(song->effective_albumartist(), song->album(), art_manual, clear_art_automatic); + service->songs_collection_backend()->UpdateManualAlbumArtAsync(song->effective_albumartist(), song->album(), art_manual); } break; } @@ -592,6 +598,44 @@ void AlbumCoverChoiceController::SaveArtManualToSong(Song *song, const QUrl &art } +void AlbumCoverChoiceController::ClearAlbumCoverForSong(Song *song) { + + if (!song->is_valid()) return; + + song->set_art_unset(false); + song->set_art_embedded(false); + song->clear_art_automatic(); + song->clear_art_manual(); + + if (song->source() == Song::Source::Collection) { + app_->collection_backend()->ClearAlbumArtAsync(song->effective_albumartist(), song->album(), false); + } + + if (*song == app_->current_albumcover_loader()->last_song()) { + app_->current_albumcover_loader()->LoadAlbumCover(*song); + } + +} + +void AlbumCoverChoiceController::UnsetAlbumCoverForSong(Song *song) { + + if (!song->is_valid()) return; + + song->set_art_unset(true); + song->set_art_embedded(false); + song->clear_art_manual(); + song->clear_art_automatic(); + + if (song->source() == Song::Source::Collection) { + app_->collection_backend()->UnsetAlbumArtAsync(song->effective_albumartist(), song->album()); + } + + if (*song == app_->current_albumcover_loader()->last_song()) { + app_->current_albumcover_loader()->LoadAlbumCover(*song); + } + +} + QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song *song, const AlbumCoverImageResult &result, const bool force_overwrite) { return SaveCoverToFileAutomatic(song->source(), @@ -625,11 +669,10 @@ QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song::Source sou filepath = file.fileName(); } - QUrl cover_url; - if (result.is_jpeg()) { + if (!result.image_data.isEmpty() && result.is_jpeg()) { if (file.open(QIODevice::WriteOnly)) { if (file.write(result.image_data) > 0) { - cover_url = QUrl::fromLocalFile(filepath); + return QUrl::fromLocalFile(filepath); } else { qLog(Error) << "Failed to write cover to file" << file.fileName() << file.errorString(); @@ -643,88 +686,58 @@ QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song::Source sou } } else { - if (result.image.save(filepath, "JPG")) cover_url = QUrl::fromLocalFile(filepath); - } - - 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(), CollectionFilterOptions()); -#else - QFuture future = QtConcurrent::run(app_->collection_backend(), &CollectionBackend::GetAlbumSongs, song.effective_albumartist(), song.effective_album(), CollectionFilterOptions()); -#endif - QFutureWatcher *watcher = new QFutureWatcher(); - QObject::connect(watcher, &QFutureWatcher::finished, this, [this, watcher, song, result]() { - SongList songs = watcher->result(); - watcher->deleteLater(); - QList urls; - urls.reserve(songs.count()); - for (const Song &s : songs) urls << s.url(); - if (result.is_jpeg()) { - quint64 id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.image_data); - QMutexLocker l(&mutex_cover_save_tasks_); - cover_save_tasks_.insert(id, song); - } - else { - quint64 id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.image); - QMutexLocker l(&mutex_cover_save_tasks_); - cover_save_tasks_.insert(id, song); - } - }); - watcher->setFuture(future); - } - 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); + if (result.image.save(filepath, "JPG")) { + return QUrl::fromLocalFile(filepath); } } -} - -void AlbumCoverChoiceController::SaveCoverEmbeddedAutomatic(const Song &song, const QUrl &cover_url) { - - SaveCoverEmbeddedAutomatic(song, cover_url.toLocalFile()); + return QUrl(); } -void AlbumCoverChoiceController::SaveCoverEmbeddedAutomatic(const Song &song, const QString &cover_filename) { +void AlbumCoverChoiceController::SaveCoverEmbeddedToCollectionSongs(const Song &song, const AlbumCoverImageResult &result) { + + SaveCoverEmbeddedToCollectionSongs(song, QString(), result.image_data, result.mime_type); + +} + +void AlbumCoverChoiceController::SaveCoverEmbeddedToCollectionSongs(const Song &song, const QString &cover_filename, const QByteArray &image_data, const QString &mime_type) { 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(), CollectionFilterOptions()); -#else - QFuture future = QtConcurrent::run(app_->collection_backend(), &CollectionBackend::GetAlbumSongs, song.effective_albumartist(), song.effective_album(), CollectionFilterOptions()); -#endif - QFutureWatcher *watcher = new QFutureWatcher(); - QObject::connect(watcher, &QFutureWatcher::finished, this, [this, watcher, song, cover_filename]() { - SongList songs = watcher->result(); - watcher->deleteLater(); - QList urls; - urls.reserve(songs.count()); - for (const Song &s : songs) urls << s.url(); - quint64 id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, cover_filename); - QMutexLocker l(&mutex_cover_save_tasks_); - cover_save_tasks_.insert(id, song); - }); - watcher->setFuture(future); + SaveCoverEmbeddedToCollectionSongs(song.effective_albumartist(), song.effective_album(), cover_filename, image_data, mime_type); } else { - app_->album_cover_loader()->SaveEmbeddedCoverAsync(song.url().toLocalFile(), cover_filename); + SaveCoverEmbeddedToSong(song, cover_filename, image_data, mime_type); } } -void AlbumCoverChoiceController::SaveCoverEmbeddedAutomatic(const QList &urls, const QImage &image) { +void AlbumCoverChoiceController::SaveCoverEmbeddedToCollectionSongs(const QString &effective_albumartist, const QString &effective_album, const QString &cover_filename, const QByteArray &image_data, const QString &mime_type) { - app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, image); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QFuture future = QtConcurrent::run(&CollectionBackend::GetAlbumSongs, app_->collection_backend(), effective_albumartist, effective_album, CollectionFilterOptions()); +#else + QFuture future = QtConcurrent::run(app_->collection_backend(), &CollectionBackend::GetAlbumSongs, effective_albumartist, effective_album, CollectionFilterOptions()); +#endif + QFutureWatcher *watcher = new QFutureWatcher(); + QObject::connect(watcher, &QFutureWatcher::finished, this, [this, watcher, cover_filename, image_data, mime_type]() { + const SongList collection_songs = watcher->result(); + watcher->deleteLater(); + for (const Song &collection_song : collection_songs) { + SaveCoverEmbeddedToSong(collection_song, cover_filename, image_data, mime_type); + } + }); + watcher->setFuture(future); + +} + +void AlbumCoverChoiceController::SaveCoverEmbeddedToSong(const Song &song, const QString &cover_filename, const QByteArray &image_data, const QString &mime_type) { + + QMutexLocker l(&mutex_cover_save_tasks_); + cover_save_tasks_.append(song); + const bool art_embedded = !image_data.isNull(); + TagReaderReply *reply = app_->tag_reader_client()->SaveEmbeddedArt(song.url().toLocalFile(), TagReaderClient::SaveCoverOptions(cover_filename, image_data, mime_type)); + QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, song, art_embedded]() { SaveEmbeddedCoverFinished(reply, song, art_embedded); }); } @@ -749,7 +762,7 @@ bool AlbumCoverChoiceController::CanAcceptDrag(const QDragEnterEvent *e) { } -QUrl AlbumCoverChoiceController::SaveCover(Song *song, const QDropEvent *e) { +void AlbumCoverChoiceController::SaveCover(Song *song, const QDropEvent *e) { for (const QUrl &url : e->mimeData()->urls()) { @@ -758,25 +771,22 @@ QUrl AlbumCoverChoiceController::SaveCover(Song *song, const QDropEvent *e) { if (IsKnownImageExtension(suffix)) { if (get_save_album_cover_type() == CoverOptions::CoverType::Embedded && song->save_embedded_cover_supported()) { - SaveCoverEmbeddedAutomatic(*song, filename); - return QUrl::fromLocalFile(Song::kEmbeddedCover); + SaveCoverEmbeddedToCollectionSongs(*song, filename); } else { SaveArtManualToSong(song, url); } - return url; + return; } } if (e->mimeData()->hasImage()) { QImage image = qvariant_cast(e->mimeData()->imageData()); if (!image.isNull()) { - return SaveCoverAutomatic(song, AlbumCoverImageResult(image)); + SaveCoverAutomatic(song, AlbumCoverImageResult(image)); } } - return QUrl(); - } QUrl AlbumCoverChoiceController::SaveCoverAutomatic(Song *song, const AlbumCoverImageResult &result) { @@ -785,8 +795,7 @@ QUrl AlbumCoverChoiceController::SaveCoverAutomatic(Song *song, const AlbumCover switch(get_save_album_cover_type()) { case CoverOptions::CoverType::Embedded:{ if (song->save_embedded_cover_supported()) { - SaveCoverEmbeddedAutomatic(*song, result); - cover_url = QUrl::fromLocalFile(Song::kEmbeddedCover); + SaveCoverEmbeddedToCollectionSongs(*song, result); break; } } @@ -803,14 +812,16 @@ QUrl AlbumCoverChoiceController::SaveCoverAutomatic(Song *song, const AlbumCover } -void AlbumCoverChoiceController::SaveEmbeddedCoverAsyncFinished(quint64 id, const bool success, const bool cleared) { +void AlbumCoverChoiceController::SaveEmbeddedCoverFinished(TagReaderReply *reply, Song song, const bool art_embedded) { - if (!cover_save_tasks_.contains(id)) return; + if (!cover_save_tasks_.contains(song)) return; + cover_save_tasks_.removeAll(song); - Song song = cover_save_tasks_.take(id); - if (success) { - if (cleared) SaveArtAutomaticToSong(&song, QUrl()); - else SaveArtAutomaticToSong(&song, QUrl::fromLocalFile(Song::kEmbeddedCover)); + if (reply->is_successful()) { + SaveArtEmbeddedToSong(&song, art_embedded); + } + else { + emit Error(tr("Could not save cover to file %1.").arg(song.url().toLocalFile())); } } diff --git a/src/covermanager/albumcoverchoicecontroller.h b/src/covermanager/albumcoverchoicecontroller.h index 5ba4f99f..ddfdbaa9 100644 --- a/src/covermanager/albumcoverchoicecontroller.h +++ b/src/covermanager/albumcoverchoicecontroller.h @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2023, 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 @@ -38,8 +38,10 @@ #include #include "core/song.h" +#include "core/tagreaderclient.h" #include "utilities/coveroptions.h" #include "settings/collectionsettingspage.h" +#include "albumcoverloaderoptions.h" #include "albumcoverimageresult.h" class QFileDialog; @@ -108,22 +110,21 @@ class AlbumCoverChoiceController : public QWidget { // 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); + void 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); + void SearchForCover(Song *song); AlbumCoverImageResult SearchForImage(Song *song); - // Returns a path which indicates that the cover has been unset manually. - QUrl UnsetCover(Song *song, const bool clear_art_automatic = false); + void UnsetCover(Song *song); // Clears any album cover art associated with the song. - void ClearCover(Song *song, const bool clear_art_automatic = false); + void ClearCover(Song *song); // Physically deletes associated album covers from disk. - bool DeleteCover(Song *song, const bool manually_unset = false); + bool DeleteCover(Song *song, const bool unset = false); // Shows the cover of given song in it's original size. void ShowCover(const Song &song, const QImage &image = QImage()); @@ -133,20 +134,25 @@ class AlbumCoverChoiceController : public QWidget { quint64 SearchCoverAutomatically(const Song &song); // Saves the chosen cover as manual cover path of this song in collection. + void SaveArtEmbeddedToSong(Song *song, const bool art_embedded); void SaveArtAutomaticToSong(Song *song, const QUrl &art_automatic); - void SaveArtManualToSong(Song *song, const QUrl &art_manual, const bool clear_art_automatic = false); + void SaveArtManualToSong(Song *song, const QUrl &art_manual); + void ClearAlbumCoverForSong(Song *song); + void UnsetAlbumCoverForSong(Song *song); // Saves the cover that the user picked through a drag and drop operation. - QUrl SaveCover(Song *song, const QDropEvent *e); + void 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 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); + + void SaveCoverEmbeddedToCollectionSongs(const Song &song, const AlbumCoverImageResult &result); + void SaveCoverEmbeddedToCollectionSongs(const Song &song, const QString &cover_filename, const QByteArray &image_data = QByteArray(), const QString &mime_type = QString()); + void SaveCoverEmbeddedToCollectionSongs(const QString &effective_albumartist, const QString &effective_album, const QString &cover_filename, const QByteArray &image_data = QByteArray(), const QString &mime_type = QString()); + void SaveCoverEmbeddedToSong(const Song &song, const QString &cover_filename, const QByteArray &image_data, const QString &mime_type = QString()); static bool CanAcceptDrag(const QDragEnterEvent *e); @@ -155,7 +161,7 @@ class AlbumCoverChoiceController : public QWidget { private slots: void AlbumCoverFetched(const quint64 id, const AlbumCoverImageResult &result, const CoverSearchStatistics &statistics); - void SaveEmbeddedCoverAsyncFinished(quint64 id, const bool success, const bool cleared); + void SaveEmbeddedCoverFinished(TagReaderReply *reply, Song song, const bool art_embedded); signals: void Error(const QString &error); @@ -187,12 +193,13 @@ class AlbumCoverChoiceController : public QWidget { QAction *search_cover_auto_; QMap cover_fetching_tasks_; - QMap cover_save_tasks_; + QList cover_save_tasks_; QMutex mutex_cover_save_tasks_; CoverOptions cover_options_; bool save_embedded_cover_override_; + AlbumCoverLoaderOptions::Types cover_types_; }; #endif // ALBUMCOVERCHOICECONTROLLER_H diff --git a/src/covermanager/albumcoverexporter.cpp b/src/covermanager/albumcoverexporter.cpp index 8f3c9abb..dbf3457a 100644 --- a/src/covermanager/albumcoverexporter.cpp +++ b/src/covermanager/albumcoverexporter.cpp @@ -24,6 +24,7 @@ #include #include "core/song.h" +#include "albumcoverloaderoptions.h" #include "albumcoverexport.h" #include "albumcoverexporter.h" #include "coverexportrunnable.h" @@ -43,9 +44,15 @@ void AlbumCoverExporter::SetDialogResult(const AlbumCoverExport::DialogResult &d dialog_result_ = dialog_result; } +void AlbumCoverExporter::SetCoverTypes(const AlbumCoverLoaderOptions::Types cover_types) { + cover_types_ = cover_types; +} + void AlbumCoverExporter::AddExportRequest(const Song &song) { - requests_.append(new CoverExportRunnable(dialog_result_, song)); + + requests_.append(new CoverExportRunnable(dialog_result_, cover_types_, song)); all_ = static_cast(requests_.count()); + } void AlbumCoverExporter::Cancel() { requests_.clear(); } diff --git a/src/covermanager/albumcoverexporter.h b/src/covermanager/albumcoverexporter.h index 30aadab3..705dd47e 100644 --- a/src/covermanager/albumcoverexporter.h +++ b/src/covermanager/albumcoverexporter.h @@ -27,6 +27,7 @@ #include #include +#include "albumcoverloaderoptions.h" #include "albumcoverexport.h" class QThreadPool; @@ -42,6 +43,7 @@ class AlbumCoverExporter : public QObject { static const int kMaxConcurrentRequests; void SetDialogResult(const AlbumCoverExport::DialogResult &dialog_result); + void SetCoverTypes(const AlbumCoverLoaderOptions::Types cover_types); void AddExportRequest(const Song &song); void StartExporting(); void Cancel(); @@ -57,6 +59,8 @@ class AlbumCoverExporter : public QObject { private: void AddJobsToPool(); + + AlbumCoverLoaderOptions::Types cover_types_; AlbumCoverExport::DialogResult dialog_result_; QQueue requests_; diff --git a/src/covermanager/albumcoverimageresult.h b/src/covermanager/albumcoverimageresult.h index 83858d43..2ec4e87e 100644 --- a/src/covermanager/albumcoverimageresult.h +++ b/src/covermanager/albumcoverimageresult.h @@ -20,8 +20,6 @@ #ifndef ALBUMCOVERIMAGERESULT_H #define ALBUMCOVERIMAGERESULT_H -#include "config.h" - #include #include #include @@ -30,10 +28,7 @@ class AlbumCoverImageResult { public: - explicit AlbumCoverImageResult(const QUrl &_cover_url = QUrl(), - const QString &_mime_type = QString(), - const QByteArray &_image_data = QByteArray(), - const QImage &_image = QImage()) + 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), @@ -47,7 +42,6 @@ class AlbumCoverImageResult { 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) diff --git a/src/covermanager/albumcoverloader.cpp b/src/covermanager/albumcoverloader.cpp index 2556c2f3..324e9957 100644 --- a/src/covermanager/albumcoverloader.cpp +++ b/src/covermanager/albumcoverloader.cpp @@ -1,8 +1,6 @@ /* * Strawberry Music Player - * This file was part of Clementine. - * Copyright 2010, David Sansome - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2023, 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 @@ -19,19 +17,13 @@ * */ -#include "config.h" - #include #include #include -#include -#include #include #include -#include #include -#include #include #include #include @@ -39,7 +31,6 @@ #include #include #include -#include #include #include @@ -53,12 +44,13 @@ #include "albumcoverloaderresult.h" #include "albumcoverimageresult.h" +using std::make_shared; + AlbumCoverLoader::AlbumCoverLoader(QObject *parent) : QObject(parent), network_(new NetworkAccessManager(this)), stop_requested_(false), load_image_async_id_(1), - save_image_async_id_(1), original_thread_(nullptr) { original_thread_ = thread(); @@ -86,7 +78,7 @@ void AlbumCoverLoader::CancelTask(const quint64 id) { for (QQueue::iterator it = tasks_.begin(); it != tasks_.end(); ++it) { TaskPtr task = *it; if (task->id == id) { - tasks_.erase(it); // clazy:exclude=strict-iterators + tasks_.erase(it); break; } } @@ -99,7 +91,7 @@ void AlbumCoverLoader::CancelTasks(const QSet &ids) { for (QQueue::iterator it = tasks_.begin(); it != tasks_.end();) { TaskPtr task = *it; if (ids.contains(task->id)) { - it = tasks_.erase(it); // clazy:exclude=strict-iterators + it = tasks_.erase(it); } else { ++it; @@ -112,24 +104,28 @@ quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, TaskPtr task = std::make_shared(); task->options = options; + task->art_embedded = song.art_embedded(); + task->art_automatic = song.art_automatic(); + task->art_manual = song.art_manual(); + task->art_unset = song.art_unset(); + task->song_source = song.source(); + task->song_url = song.url(); task->song = song; - 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); +quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, const bool art_embedded, const QUrl &art_automatic, const QUrl &art_manual, const bool art_unset, const QUrl &song_url, const Song::Source song_source) { TaskPtr task = std::make_shared(); task->options = options; - task->song = song; - task->state = State::Manual; + task->art_embedded = art_embedded; + task->art_automatic = art_automatic; + task->art_manual = art_manual; + task->art_unset = art_unset; + task->song_source = song_source; + task->song_url = song_url; return EnqueueTask(task); @@ -137,7 +133,7 @@ quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, const AlbumCoverImageResult &album_cover) { - TaskPtr task = std::make_shared(); + TaskPtr task = make_shared(); task->options = options; task->album_cover = album_cover; @@ -147,7 +143,7 @@ quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, const QImage &image) { - TaskPtr task = std::make_shared(); + TaskPtr task = make_shared(); task->options = options; task->album_cover.image = image; @@ -171,390 +167,254 @@ quint64 AlbumCoverLoader::EnqueueTask(TaskPtr task) { void AlbumCoverLoader::ProcessTasks() { - while (!stop_requested_) { - // Get the next task - TaskPtr task; - { - QMutexLocker l(&mutex_load_image_async_); - if (tasks_.isEmpty()) return; - task = tasks_.dequeue(); - } - - ProcessTask(task); + TaskPtr task; + { + QMutexLocker l(&mutex_load_image_async_); + if (tasks_.isEmpty()) return; + task = tasks_.dequeue(); } + ProcessTask(task); + } void AlbumCoverLoader::ProcessTask(TaskPtr task) { - TryLoadResult result = TryLoadImage(task); - if (result.started_async) { - // The image is being loaded from a remote URL, we'll carry on later when it's done - return; - } - - if (result.loaded_success) { - 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; - } - - NextState(task); - -} - -void AlbumCoverLoader::NextState(TaskPtr task) { - - if (task->state == State::Manual) { - // Try the automatic one next - task->state = State::Automatic; - ProcessTask(task); + // If we have album cover already, only do scale and pad. + if (task->album_cover.is_valid()) { + task->success = true; } else { - // Give up - 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)); + InitArt(task); } + while (!task->success && !task->options.types.isEmpty()) { + const AlbumCoverLoaderOptions::Type type = task->options.types.takeFirst(); + const LoadImageResult result = LoadImage(task, type); + if (result.status == LoadImageResult::Status::Async) { + // The image is being loaded from a remote URL, we'll carry on later when it's done. + return; + } + if (result.status == LoadImageResult::Status::Success) { + task->success = true; + task->result_type = result.type; + break; + } + } + + if (!task->success && !task->options.default_cover.isEmpty()) { + LoadLocalFileImage(task, AlbumCoverLoaderResult::Type::None, task->options.default_cover); + } + + FinishTask(task, task->result_type); + } -AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(TaskPtr task) { +void AlbumCoverLoader::FinishTask(TaskPtr task, const AlbumCoverLoaderResult::Type result_type) { - // Only scale and pad. - if (task->album_cover.is_valid()) { - return TryLoadResult(false, true, AlbumCoverLoaderResult::Type::Embedded, task->album_cover); + QImage image_scaled; + if (task->success) { + task->result_type = result_type; + task->album_cover.mime_type = Utilities::MimeTypeFromData(task->album_cover.image_data); + if (task->scaled_image()) { + image_scaled = ImageUtils::ScaleImage(task->album_cover.image, task->options.desired_scaled_size, task->options.device_pixel_ratio, task->pad_scaled_image()); + } + if (!task->raw_image_data() && !task->album_cover.image_data.isNull()) { + task->album_cover.image_data = QByteArray(); + } + if (!task->original_image() && !task->album_cover.image.isNull()) { + task->album_cover.image = QImage(); + } } + emit AlbumCoverLoaded(task->id, AlbumCoverLoaderResult(task->success, task->result_type, task->album_cover, image_scaled, task->art_updated)); + +} + +void AlbumCoverLoader::InitArt(TaskPtr task) { + // For local files and streams initialize art if found. - if ((task->song.source() == Song::Source::LocalFile || task->song.is_radio()) && !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; + if (task->song.is_valid() && (task->song.source() == Song::Source::LocalFile || task->song.is_radio()) && !task->song.art_manual_is_valid() && !task->song.art_automatic_is_valid()) { + task->song.InitArtManual(); + if (task->song.art_manual_is_valid()) { + task->art_updated = true; + task->art_manual = task->song.art_manual(); + } + if (task->song.url().isLocalFile()) { + task->song.InitArtAutomatic(); + if (task->song.art_automatic_is_valid()) { + task->art_updated = true; + task->art_automatic = task->song.art_automatic(); + } } } - AlbumCoverLoaderResult::Type type = AlbumCoverLoaderResult::Type::None; - QUrl cover_url; - switch (task->state) { - case State::None: - case State::Automatic: - type = AlbumCoverLoaderResult::Type::Automatic; - cover_url = task->song.art_automatic(); - break; - case State::Manual: - type = AlbumCoverLoaderResult::Type::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, AlbumCoverImageResult(cover_url, QString(), QByteArray(), task->options.default_output_image_)); - } - 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 (!image_data.isEmpty() && task->options.get_image_ && image.loadFromData(image_data)) { - return TryLoadResult(false, !image.isNull(), AlbumCoverLoaderResult::Type::Embedded, AlbumCoverImageResult(cover_url, QString(), image_data, image)); - } - else { - return TryLoadResult(false, !image_data.isEmpty(), AlbumCoverLoaderResult::Type::Embedded, AlbumCoverImageResult(cover_url, QString(), image_data, 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_data.isEmpty(), type, AlbumCoverImageResult(cover_url, QString(), image_data, image.isNull() ? task->options.default_output_image_ : image)); - } - } - else { - qLog(Error) << "Failed to open cover file" << cover_url << "for reading" << file.errorString(); - } - } - else { - qLog(Error) << "Cover file" << cover_url << "does not exist"; - } - } - else if (cover_url.scheme().isEmpty()) { // Assume a local file with no scheme. - QFile file(cover_url.path()); - 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_data.isEmpty(), type, AlbumCoverImageResult(cover_url, QString(), image_data, image.isNull() ? task->options.default_output_image_ : image)); - } - } - else { - qLog(Error) << "Failed to open cover file" << cover_url << "for reading" << file.errorString(); - } - } - else { - qLog(Error) << "Cover file" << cover_url << "does not exist"; - } - } - else if (network_->supportedSchemes().contains(cover_url.scheme())) { // Remote URL - qLog(Debug) << "Loading remote cover from" << cover_url; - QNetworkRequest request(cover_url); - request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - QNetworkReply *reply = network_->get(request); - QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, cover_url]() { RemoteFetchFinished(reply, cover_url); }); - - remote_tasks_.insert(reply, task); - return TryLoadResult(true, false, type, AlbumCoverImageResult(cover_url)); - } - } - - return TryLoadResult(false, false, AlbumCoverLoaderResult::Type::None, AlbumCoverImageResult(cover_url, QString(), QByteArray(), task->options.default_output_image_)); - } -void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply *reply, const QUrl &cover_url) { +AlbumCoverLoader::LoadImageResult AlbumCoverLoader::LoadImage(TaskPtr task, const AlbumCoverLoaderOptions::Type &type) { + + switch (type) { + case AlbumCoverLoaderOptions::Type::Unset:{ + if (task->art_unset) { + if (!task->options.default_cover.isEmpty()) { + return LoadLocalFileImage(task, AlbumCoverLoaderResult::Type::Unset, task->options.default_cover); + } + return LoadImageResult(AlbumCoverLoaderResult::Type::Unset, LoadImageResult::Status::Success); + } + break; + } + case AlbumCoverLoaderOptions::Type::Embedded:{ + if (task->art_embedded && task->song_url.isValid() && task->song_url.isLocalFile()) { + return LoadEmbeddedImage(task); + } + break; + } + case AlbumCoverLoaderOptions::Type::Automatic:{ + if (task->art_automatic.isValid()) { + return LoadUrlImage(task, AlbumCoverLoaderResult::Type::Automatic, task->art_automatic); + } + break; + } + case AlbumCoverLoaderOptions::Type::Manual:{ + if (task->art_manual.isValid()) { + return LoadUrlImage(task, AlbumCoverLoaderResult::Type::Manual, task->art_manual); + } + break; + } + } + + return LoadImageResult(); + +} + +AlbumCoverLoader::LoadImageResult AlbumCoverLoader::LoadEmbeddedImage(TaskPtr task) { + + if (task->art_embedded && task->song_url.isValid() && task->song_url.isLocalFile()) { + task->album_cover.image_data = TagReaderClient::Instance()->LoadEmbeddedArtBlocking(task->song_url.toLocalFile()); + if (!task->album_cover.image_data.isEmpty() && task->album_cover.image.loadFromData(task->album_cover.image_data)) { + return LoadImageResult(AlbumCoverLoaderResult::Type::Embedded, LoadImageResult::Status::Success); + } + } + + return LoadImageResult(AlbumCoverLoaderResult::Type::Embedded, LoadImageResult::Status::Failure); + +} + +AlbumCoverLoader::LoadImageResult AlbumCoverLoader::LoadUrlImage(TaskPtr task, const AlbumCoverLoaderResult::Type result_type, const QUrl &cover_url) { + + if (cover_url.isValid()) { + if (cover_url.isLocalFile()) { + return LoadLocalUrlImage(task, result_type, cover_url); + } + else if (network_->supportedSchemes().contains(cover_url.scheme())) { + return LoadRemoteUrlImage(task, result_type, cover_url); + } + } + + return LoadImageResult(result_type, LoadImageResult::Status::Failure); + +} + +AlbumCoverLoader::LoadImageResult AlbumCoverLoader::LoadLocalUrlImage(TaskPtr task, const AlbumCoverLoaderResult::Type result_type, const QUrl &cover_url) { + + if (cover_url.isEmpty()) { + return LoadImageResult(result_type, LoadImageResult::Status::Failure); + } + + if (!cover_url.isValid()) { + return LoadImageResult(result_type, LoadImageResult::Status::Failure); + } + + if (!cover_url.isLocalFile()) { + return LoadImageResult(result_type, LoadImageResult::Status::Failure); + } + + return LoadLocalFileImage(task, result_type, cover_url.toLocalFile()); + +} + +AlbumCoverLoader::LoadImageResult AlbumCoverLoader::LoadLocalFileImage(TaskPtr task, const AlbumCoverLoaderResult::Type result_type, const QString &cover_file) { + + if (!QFileInfo::exists(cover_file)) { + qLog(Error) << "Cover file" << cover_file << "does not exist."; + return LoadImageResult(result_type, LoadImageResult::Status::Failure); + } + + QFile file(cover_file); + if (!file.open(QIODevice::ReadOnly)) { + qLog(Error) << "Unable to open cover file" << cover_file << "for reading:" << file.errorString(); + return LoadImageResult(result_type, LoadImageResult::Status::Failure); + } + + task->album_cover.image_data = file.readAll(); + file.close(); + + if (task->album_cover.image_data.isEmpty()) { + qLog(Error) << "Cover file" << cover_file << "is empty."; + return LoadImageResult(result_type, LoadImageResult::Status::Failure); + } + + if (!task->album_cover.image.loadFromData(task->album_cover.image_data)) { + qLog(Error) << "Failed to load image from cover file" << cover_file << ":" << file.errorString(); + return LoadImageResult(result_type, LoadImageResult::Status::Failure); + } + + return LoadImageResult(result_type, LoadImageResult::Status::Success); + +} + +AlbumCoverLoader::LoadImageResult AlbumCoverLoader::LoadRemoteUrlImage(TaskPtr task, const AlbumCoverLoaderResult::Type result_type, const QUrl &cover_url) { + + qLog(Debug) << "Loading remote cover from URL" << cover_url; + + QNetworkRequest request(cover_url); + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + QNetworkReply *reply = network_->get(request); + QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, task, result_type, cover_url]() { LoadRemoteImageFinished(reply, task, result_type, cover_url); }); + + return LoadImageResult(result_type, LoadImageResult::Status::Async); + +} + +void AlbumCoverLoader::LoadRemoteImageFinished(QNetworkReply *reply, TaskPtr task, const AlbumCoverLoaderResult::Type result_type, const QUrl &cover_url) { reply->deleteLater(); - if (!remote_tasks_.contains(reply)) return; - TaskPtr task = remote_tasks_.take(reply); - - // Handle redirects. QVariant redirect = reply->attribute(QNetworkRequest::RedirectionTargetAttribute); - if (redirect.isValid()) { - if (++task->redirects > kMaxRedirects) { - return; // Give up. +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + if (redirect.isValid() && redirect.metaType().id() == QMetaType::QUrl) { +#else + if (redirect.isValid() && redirect.type() == QVariant::Url) { +#endif + if (task->redirects++ >= kMaxRedirects) { + ProcessTask(task); + return; } + const QUrl redirect_url = redirect.toUrl(); + qLog(Debug) << "Loading remote cover from redirected URL" << redirect_url; QNetworkRequest request = reply->request(); request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - request.setUrl(redirect.toUrl()); + request.setUrl(redirect_url); QNetworkReply *redirected_reply = network_->get(request); - QObject::connect(redirected_reply, &QNetworkReply::finished, this, [this, redirected_reply, redirect]() { RemoteFetchFinished(redirected_reply, redirect.toUrl()); }); - - remote_tasks_.insert(redirected_reply, task); + QObject::connect(redirected_reply, &QNetworkReply::finished, this, [this, reply, task, result_type, redirect_url]() { LoadRemoteImageFinished(reply, task, result_type, redirect_url); }); return; } 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.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)); + task->album_cover.image_data = reply->readAll(); + if (!task->album_cover.image_data.isEmpty() && task->album_cover.image.loadFromData(task->album_cover.image_data)) { + task->success = true; + FinishTask(task, result_type); return; } else { - qLog(Error) << "Unable to load album cover image" << cover_url; + qLog(Error) << "Unable to load album cover image from URL" << cover_url; } } else { - qLog(Error) << "Unable to get album cover" << cover_url << reply->error() << reply->errorString(); + qLog(Error) << "Unable to get album cover from URL" << cover_url << reply->error() << reply->errorString(); } - NextState(task); - -} - -quint64 AlbumCoverLoader::SaveEmbeddedCoverAsync(const QString &song_filename, const QString &cover_filename) { - - QMutexLocker l(&mutex_save_image_async_); - quint64 id = ++save_image_async_id_; - QMetaObject::invokeMethod(this, "SaveEmbeddedCover", Qt::QueuedConnection, Q_ARG(quint64, id), Q_ARG(QString, song_filename), Q_ARG(QString, cover_filename)); - return id; - -} - -quint64 AlbumCoverLoader::SaveEmbeddedCoverAsync(const QString &song_filename, const QImage &image) { - - QMutexLocker l(&mutex_save_image_async_); - quint64 id = ++save_image_async_id_; - QMetaObject::invokeMethod(this, "SaveEmbeddedCover", Qt::QueuedConnection, Q_ARG(quint64, id), Q_ARG(QString, song_filename), Q_ARG(QImage, image)); - return id; - -} - -quint64 AlbumCoverLoader::SaveEmbeddedCoverAsync(const QString &song_filename, const QByteArray &image_data) { - - QMutexLocker l(&mutex_save_image_async_); - quint64 id = ++save_image_async_id_; - QMetaObject::invokeMethod(this, "SaveEmbeddedCover", Qt::QueuedConnection, Q_ARG(quint64, id), Q_ARG(QString, song_filename), Q_ARG(QByteArray, image_data)); - return id; - -} - -quint64 AlbumCoverLoader::SaveEmbeddedCoverAsync(const QList &urls, const QString &cover_filename) { - - QMutexLocker l(&mutex_save_image_async_); - quint64 id = ++save_image_async_id_; - QMetaObject::invokeMethod(this, "SaveEmbeddedCover", Qt::QueuedConnection, Q_ARG(quint64, id), Q_ARG(QList, urls), Q_ARG(QString, cover_filename)); - return id; - -} - -quint64 AlbumCoverLoader::SaveEmbeddedCoverAsync(const QList &urls, const QImage &image) { - - QMutexLocker l(&mutex_save_image_async_); - quint64 id = ++save_image_async_id_; - QMetaObject::invokeMethod(this, "SaveEmbeddedCover", Qt::QueuedConnection, Q_ARG(quint64, id), Q_ARG(QList, urls), Q_ARG(QImage, image)); - return id; - -} - -quint64 AlbumCoverLoader::SaveEmbeddedCoverAsync(const QList &urls, const QByteArray &image_data) { - - QMutexLocker l(&mutex_save_image_async_); - quint64 id = ++save_image_async_id_; - QMetaObject::invokeMethod(this, "SaveEmbeddedCover", Qt::QueuedConnection, Q_ARG(quint64, id), Q_ARG(QList, urls), Q_ARG(QByteArray, image_data)); - return id; - -} - -void AlbumCoverLoader::SaveEmbeddedCover(const quint64 id, const QString &song_filename, const QByteArray &image_data) { - - TagReaderReply *reply = TagReaderClient::Instance()->SaveEmbeddedArt(song_filename, TagReaderClient::SaveCoverOptions(image_data)); - tagreader_save_embedded_art_requests_.insert(id, reply); - const bool clear = image_data.isEmpty(); - QObject::connect(reply, &TagReaderReply::Finished, this, [this, id, reply, clear]() { SaveEmbeddedArtFinished(id, reply, clear); }, Qt::QueuedConnection); - -} - -void AlbumCoverLoader::SaveEmbeddedCover(const quint64 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 quint64 id, const QString &song_filename, const QString &cover_filename) { - - QFile file(cover_filename); - - if (file.size() >= 209715200) { // Max 200 MB. - emit SaveEmbeddedCoverAsyncFinished(id, false, false); - return; - } - - if (!file.open(QIODevice::ReadOnly)) { - qLog(Error) << "Failed to open cover file" << cover_filename << "for reading:" << file.errorString(); - emit SaveEmbeddedCoverAsyncFinished(id, false, false); - return; - } - - QByteArray image_data = file.readAll(); - file.close(); - - SaveEmbeddedCover(id, song_filename, image_data); - -} - -void AlbumCoverLoader::SaveEmbeddedCover(const quint64 id, const QList &urls, const QImage &image) { - - if (image.isNull()) { - for (const QUrl &url : urls) { - SaveEmbeddedCover(id, url.toLocalFile(), QByteArray()); - } - return; - } - else { - 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(); - } - } - - emit SaveEmbeddedCoverAsyncFinished(id, false, image.isNull()); - -} - -void AlbumCoverLoader::SaveEmbeddedCover(const quint64 id, const QList &urls, const QString &cover_filename) { - - QFile file(cover_filename); - - if (file.size() >= 209715200) { // Max 200 MB. - emit SaveEmbeddedCoverAsyncFinished(id, false, false); - return; - } - - if (!file.open(QIODevice::ReadOnly)) { - qLog(Error) << "Failed to open cover file" << cover_filename << "for reading:" << file.errorString(); - emit SaveEmbeddedCoverAsyncFinished(id, false, false); - return; - } - - QByteArray image_data = file.readAll(); - file.close(); - SaveEmbeddedCover(id, urls, image_data); - -} - -void AlbumCoverLoader::SaveEmbeddedCover(const quint64 id, const QList &urls, const QByteArray &image_data) { - - for (const QUrl &url : urls) { - SaveEmbeddedCover(id, url.toLocalFile(), image_data); - } - -} - -void AlbumCoverLoader::SaveEmbeddedArtFinished(const quint64 id, TagReaderReply *reply, const bool cleared) { - - 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(), cleared); - } - - reply->deleteLater(); + ProcessTask(task); } diff --git a/src/covermanager/albumcoverloader.h b/src/covermanager/albumcoverloader.h index 6b69dff4..9493fae2 100644 --- a/src/covermanager/albumcoverloader.h +++ b/src/covermanager/albumcoverloader.h @@ -1,8 +1,6 @@ /* * Strawberry Music Player - * This file was part of Clementine. - * Copyright 2010, David Sansome - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2023, 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 @@ -24,21 +22,19 @@ #include "config.h" +#include + #include #include #include -#include #include #include -#include #include #include #include #include -#include #include "core/song.h" -#include "core/tagreaderclient.h" #include "albumcoverloaderoptions.h" #include "albumcoverloaderresult.h" #include "albumcoverimageresult.h" @@ -47,111 +43,96 @@ class QThread; class QNetworkReply; class NetworkAccessManager; +using std::shared_ptr; + class AlbumCoverLoader : public QObject { Q_OBJECT public: explicit AlbumCoverLoader(QObject *parent = nullptr); - enum class State { - None, - Manual, - Automatic - }; - void ExitAsync(); void Stop() { stop_requested_ = true; } quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const Song &song); - 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 bool art_embedded, const QUrl &art_automatic, const QUrl &art_manual, const bool art_unset, 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); - quint64 SaveEmbeddedCoverAsync(const QString &song_filename, const QString &cover_filename); - quint64 SaveEmbeddedCoverAsync(const QString &song_filename, const QImage &image); - quint64 SaveEmbeddedCoverAsync(const QString &song_filename, const QByteArray &image_data); - quint64 SaveEmbeddedCoverAsync(const QList &urls, const QString &cover_filename); - quint64 SaveEmbeddedCoverAsync(const QList &urls, const QImage &image); - quint64 SaveEmbeddedCoverAsync(const QList &urls, const QByteArray &image_data); - signals: void ExitFinished(); void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result); - void SaveEmbeddedCoverAsyncFinished(const quint64 id, const bool success, const bool cleared); - protected slots: - void Exit(); - void ProcessTasks(); - void RemoteFetchFinished(QNetworkReply *reply, const QUrl &cover_url); + private: + class Task { + public: + explicit Task() : id(0), success(false), art_embedded(false), art_unset(false), result_type(AlbumCoverLoaderResult::Type::None), art_updated(false), redirects(0) {} - void SaveEmbeddedCover(const quint64 id, const QString &song_filename, const QString &cover_filename); - void SaveEmbeddedCover(const quint64 id, const QString &song_filename, const QImage &image); - void SaveEmbeddedCover(const quint64 id, const QString &song_filename, const QByteArray &image_data); - void SaveEmbeddedCover(const quint64 id, const QList &urls, const QImage &image); - void SaveEmbeddedCover(const quint64 id, const QList &urls, const QString &cover_filename); - void SaveEmbeddedCover(const quint64 id, const QList &urls, const QByteArray &image_data); - - void SaveEmbeddedArtFinished(const quint64 id, TagReaderReply *reply, const bool cleared); - - protected: - - struct Task { - explicit Task() : id(0), state(State::None), type(AlbumCoverLoaderResult::Type::None), art_updated(false), redirects(0) {} + quint64 id; + bool success; AlbumCoverLoaderOptions options; - quint64 id; + bool raw_image_data() const { return options.options & AlbumCoverLoaderOptions::Option::RawImageData; } + bool original_image() const { return options.options & AlbumCoverLoaderOptions::Option::OriginalImage; } + bool scaled_image() const { return options.options & AlbumCoverLoaderOptions::Option::ScaledImage; } + bool pad_scaled_image() const { return options.options & AlbumCoverLoaderOptions::Option::PadScaledImage; } + + bool art_embedded; + QUrl art_automatic; + QUrl art_manual; + bool art_unset; + QUrl song_url; + Song::Source song_source; Song song; AlbumCoverImageResult album_cover; - State state; - AlbumCoverLoaderResult::Type type; + AlbumCoverLoaderResult::Type result_type; bool art_updated; int redirects; }; - using TaskPtr = std::shared_ptr; - - struct TryLoadResult { - 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; + using TaskPtr = shared_ptr; + class LoadImageResult { + public: + enum class Status { + Failure, + Async, + Success + }; + explicit LoadImageResult(AlbumCoverLoaderResult::Type _type = AlbumCoverLoaderResult::Type::None, Status _status = Status::Failure) : type(_type), status(_status) {} AlbumCoverLoaderResult::Type type; - AlbumCoverImageResult album_cover; + Status status; }; + private: quint64 EnqueueTask(TaskPtr task); void ProcessTask(TaskPtr task); void NextState(TaskPtr task); - TryLoadResult TryLoadImage(TaskPtr task); + void InitArt(TaskPtr task); + LoadImageResult LoadImage(TaskPtr task, const AlbumCoverLoaderOptions::Type &type); + LoadImageResult LoadEmbeddedImage(TaskPtr task); + LoadImageResult LoadUrlImage(TaskPtr task, const AlbumCoverLoaderResult::Type result_type, const QUrl &cover_url); + LoadImageResult LoadLocalUrlImage(TaskPtr task, const AlbumCoverLoaderResult::Type result_type, const QUrl &cover_url); + LoadImageResult LoadLocalFileImage(TaskPtr task, const AlbumCoverLoaderResult::Type result_type, const QString &cover_file); + LoadImageResult LoadRemoteUrlImage(TaskPtr task, const AlbumCoverLoaderResult::Type result_type, const QUrl &cover_url); + void FinishTask(TaskPtr task, const AlbumCoverLoaderResult::Type result_type); - NetworkAccessManager *network_; - - bool stop_requested_; - - QMutex mutex_load_image_async_; - QMutex mutex_save_image_async_; - QQueue tasks_; - QHash remote_tasks_; - quint64 load_image_async_id_; - quint64 save_image_async_id_; + private slots: + void Exit(); + void ProcessTasks(); + void LoadRemoteImageFinished(QNetworkReply *reply, TaskPtr task, const AlbumCoverLoaderResult::Type result_type, const QUrl &cover_url); + private: static const int kMaxRedirects = 3; - + NetworkAccessManager *network_; + bool stop_requested_; + QMutex mutex_load_image_async_; + QQueue tasks_; + quint64 load_image_async_id_; QThread *original_thread_; - - QMultiMap tagreader_save_embedded_art_requests_; - }; #endif // ALBUMCOVERLOADER_H diff --git a/src/covermanager/albumcoverloaderoptions.cpp b/src/covermanager/albumcoverloaderoptions.cpp new file mode 100644 index 00000000..d9a65343 --- /dev/null +++ b/src/covermanager/albumcoverloaderoptions.cpp @@ -0,0 +1,59 @@ +/* +* Strawberry Music Player +* Copyright 2018-2023, 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 "albumcoverloaderoptions.h" + +#include + +#include "settings/coverssettingspage.h" + +AlbumCoverLoaderOptions::AlbumCoverLoaderOptions(const Options _options, const QSize _desired_scaled_size, const qreal _device_pixel_ratio, const Types _types) + : options(_options), + desired_scaled_size(_desired_scaled_size), + device_pixel_ratio(_device_pixel_ratio), + types(_types) {} + +AlbumCoverLoaderOptions::Types AlbumCoverLoaderOptions::LoadTypes() { + + Types cover_types; + + QSettings s; + s.beginGroup(CoversSettingsPage::kSettingsGroup); + const QStringList all_cover_types = QStringList() << "art_unset" << "art_embedded" << "art_manual" << "art_automatic"; + const QStringList cover_types_strlist = s.value(CoversSettingsPage::kTypes, all_cover_types).toStringList(); + for (const QString &cover_type_str : cover_types_strlist) { + if (cover_type_str == "art_unset") { + cover_types << AlbumCoverLoaderOptions::Type::Unset; + } + else if (cover_type_str == "art_embedded") { + cover_types << AlbumCoverLoaderOptions::Type::Embedded; + } + else if (cover_type_str == "art_manual") { + cover_types << AlbumCoverLoaderOptions::Type::Manual; + } + else if (cover_type_str == "art_automatic") { + cover_types << AlbumCoverLoaderOptions::Type::Automatic; + } + } + + s.endGroup(); + + return cover_types; + +} \ No newline at end of file diff --git a/src/covermanager/albumcoverloaderoptions.h b/src/covermanager/albumcoverloaderoptions.h index fae098f7..4cf131f5 100644 --- a/src/covermanager/albumcoverloaderoptions.h +++ b/src/covermanager/albumcoverloaderoptions.h @@ -1,8 +1,6 @@ /* * Strawberry Music Player - * This file was part of Clementine. - * Copyright 2012, David Sansome - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2023, 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 @@ -22,34 +20,40 @@ #ifndef ALBUMCOVERLOADEROPTIONS_H #define ALBUMCOVERLOADEROPTIONS_H -#include "config.h" - +#include #include #include class AlbumCoverLoaderOptions { public: - explicit AlbumCoverLoaderOptions() - : get_image_data_(true), - get_image_(true), - scale_output_image_(true), - pad_output_image_(true), - create_thumbnail_(false), - pad_thumbnail_image_(false), - desired_height_(120), - thumbnail_size_(120, 120) {} + enum class Option { + NoOptions = 0x0, + RawImageData = 0x2, + OriginalImage = 0x4, + ScaledImage = 0x8, + PadScaledImage = 0x16 + }; + Q_DECLARE_FLAGS(Options, Option) - 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_; + enum class Type { + Embedded, + Automatic, + Manual, + Unset + }; + using Types = QList; + + explicit AlbumCoverLoaderOptions(const Options _options = AlbumCoverLoaderOptions::Option::ScaledImage, const QSize _desired_scaled_size = QSize(32, 32), const qreal device_pixel_ratio = 1.0F, const Types _types = QList() << AlbumCoverLoaderOptions::Type::Embedded << AlbumCoverLoaderOptions::Type::Automatic << AlbumCoverLoaderOptions::Type::Manual); + + Options options; + QSize desired_scaled_size; + qreal device_pixel_ratio; + Types types; + QString default_cover; + + static Types LoadTypes(); }; +Q_DECLARE_OPERATORS_FOR_FLAGS(AlbumCoverLoaderOptions::Options) + #endif // ALBUMCOVERLOADEROPTIONS_H diff --git a/src/covermanager/albumcoverloaderresult.h b/src/covermanager/albumcoverloaderresult.h index 0e09afec..ffd623f2 100644 --- a/src/covermanager/albumcoverloaderresult.h +++ b/src/covermanager/albumcoverloaderresult.h @@ -32,35 +32,33 @@ class AlbumCoverLoaderResult { enum class Type { None, - ManuallyUnset, + Unset, Embedded, Automatic, - Manual, - Remote + Manual }; explicit AlbumCoverLoaderResult(const bool _success = false, const Type _type = Type::None, AlbumCoverImageResult _album_cover = AlbumCoverImageResult(), const QImage &_image_scaled = QImage(), - const QImage &_image_thumbnail = QImage(), + const bool _remote_cover = false, const bool _updated = false) : success(_success), type(_type), album_cover(_album_cover), image_scaled(_image_scaled), - image_thumbnail(_image_thumbnail), + remote_cover(_remote_cover), updated(_updated) {} bool success; Type type; AlbumCoverImageResult album_cover; QImage image_scaled; - QImage image_thumbnail; + bool remote_cover; bool updated; QUrl temp_cover_url; - }; Q_DECLARE_METATYPE(AlbumCoverLoaderResult) diff --git a/src/covermanager/albumcovermanager.cpp b/src/covermanager/albumcovermanager.cpp index 24f8f243..864242f2 100644 --- a/src/covermanager/albumcovermanager.cpp +++ b/src/covermanager/albumcovermanager.cpp @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2023, 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 @@ -95,6 +95,7 @@ #include "ui_albumcovermanager.h" const char *AlbumCoverManager::kSettingsGroup = "CoverManager"; +constexpr int AlbumCoverManager::kThumbnailSize = 120; AlbumCoverManager::AlbumCoverManager(Application *app, CollectionBackend *collection_backend, QMainWindow *mainwindow, QWidget *parent) : QMainWindow(parent), @@ -112,7 +113,7 @@ AlbumCoverManager::AlbumCoverManager(Application *app, CollectionBackend *collec cover_exporter_(new AlbumCoverExporter(this)), artist_icon_(IconLoader::Load("folder-sound")), all_artists_icon_(IconLoader::Load("library-music")), - image_nocover_thumbnail_(ImageUtils::GenerateNoCoverImage(QSize(120 * devicePixelRatio(), 120 * devicePixelRatio()))), + image_nocover_thumbnail_(ImageUtils::GenerateNoCoverImage(QSize(120, 120), devicePixelRatio())), icon_nocover_item_(QPixmap::fromImage(image_nocover_thumbnail_)), context_menu_(new QMenu(this)), progress_bar_(new QProgressBar(this)), @@ -150,11 +151,6 @@ 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 * devicePixelRatio(); - cover_loader_options_.create_thumbnail_ = false; - EnableCoversButtons(); } @@ -236,7 +232,6 @@ void AlbumCoverManager::Init() { 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_); @@ -248,6 +243,7 @@ void AlbumCoverManager::showEvent(QShowEvent *e) { if (!e->spontaneous()) { LoadGeometry(); + cover_types_ = AlbumCoverLoaderOptions::LoadTypes(); album_cover_choice_controller_->ReloadSettings(); Reset(); } @@ -321,7 +317,6 @@ void AlbumCoverManager::CancelRequests() { #endif cover_loading_tasks_.clear(); cover_save_tasks_.clear(); - cover_save_tasks2_.clear(); cover_exporter_->Cancel(); @@ -390,41 +385,43 @@ void AlbumCoverManager::ArtistChanged(QListWidgetItem *current) { // Sort by album name. The list is already sorted by sqlite but it was done case sensitively. std::stable_sort(albums.begin(), albums.end(), CompareAlbumNameNocase); - for (const CollectionBackend::Album &info : albums) { + for (const CollectionBackend::Album &album_info : albums) { // Don't show songs without an album, obviously - if (info.album.isEmpty()) continue; + if (album_info.album.isEmpty()) continue; QString display_text; if (current->type() == Specific_Artist) { - display_text = info.album; + display_text = album_info.album; } else { - display_text = info.album_artist + " - " + info.album; + display_text = album_info.album_artist + " - " + album_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, QVariant::fromValue(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; + AlbumItem *album_item = new AlbumItem(icon_nocover_item_, display_text, ui_->albums); + album_item->setData(Role_AlbumArtist, album_info.album_artist); + album_item->setData(Role_Album, album_info.album); + album_item->setData(Role_Filetype, QVariant::fromValue(album_info.filetype)); + album_item->setData(Role_CuePath, album_info.cue_path); + album_item->setData(Qt::TextAlignmentRole, QVariant(Qt::AlignTop | Qt::AlignHCenter)); + album_item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled); + album_item->urls = album_info.urls; - if (info.album_artist.isEmpty()) { - item->setToolTip(info.album); + if (album_info.album_artist.isEmpty()) { + album_item->setToolTip(album_info.album); } else { - item->setToolTip(info.album_artist + " - " + info.album); + album_item->setToolTip(album_info.album_artist + " - " + album_info.album); } - if (!info.art_automatic.isEmpty() || !info.art_manual.isEmpty()) { - 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; + album_item->setData(Role_ArtEmbedded, album_info.art_embedded); + album_item->setData(Role_ArtAutomatic, album_info.art_automatic); + album_item->setData(Role_ArtManual, album_info.art_manual); + album_item->setData(Role_ArtUnset, album_info.art_unset); + + if (album_info.art_embedded || !album_info.art_automatic.isEmpty() || !album_info.art_manual.isEmpty()) { + LoadAlbumCoverAsync(album_item); } } @@ -437,18 +434,15 @@ void AlbumCoverManager::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoade if (!cover_loading_tasks_.contains(id)) return; - AlbumItem *item = cover_loading_tasks_.take(id); + AlbumItem *album_item = cover_loading_tasks_.take(id); - if (!result.success || result.image_scaled.isNull() || result.type == AlbumCoverLoaderResult::Type::ManuallyUnset) { - item->setIcon(icon_nocover_item_); + if (!result.success || result.image_scaled.isNull() || result.type == AlbumCoverLoaderResult::Type::Unset) { + album_item->setIcon(icon_nocover_item_); } else { - item->setIcon(QPixmap::fromImage(result.image_scaled)); + album_item->setIcon(QPixmap::fromImage(result.image_scaled)); } - //item->setData(Role_Image, result.image_original); - //item->setData(Role_ImageData, result.image_data); - UpdateFilter(); } @@ -471,13 +465,13 @@ void AlbumCoverManager::UpdateFilter() { qint32 without_cover = 0; for (int i = 0; i < ui_->albums->count(); ++i) { - AlbumItem *item = static_cast(ui_->albums->item(i)); - bool should_hide = ShouldHide(*item, filter, hide_covers); - item->setHidden(should_hide); + AlbumItem *album_item = static_cast(ui_->albums->item(i)); + bool should_hide = ShouldHide(*album_item, filter, hide_covers); + album_item->setHidden(should_hide); if (!should_hide) { ++total_count; - if (!ItemHasCover(*item)) { + if (!ItemHasCover(*album_item)) { ++without_cover; } } @@ -488,9 +482,9 @@ void AlbumCoverManager::UpdateFilter() { } -bool AlbumCoverManager::ShouldHide(const AlbumItem &item, const QString &filter, const HideCovers hide_covers) const { +bool AlbumCoverManager::ShouldHide(const AlbumItem &album_item, const QString &filter, const HideCovers hide_covers) const { - bool has_cover = ItemHasCover(item); + bool has_cover = ItemHasCover(album_item); if (hide_covers == HideCovers::WithCovers && has_cover) { return true; } @@ -504,8 +498,8 @@ bool AlbumCoverManager::ShouldHide(const AlbumItem &item, const QString &filter, QStringList query = filter.split(' '); for (const QString &s : query) { - bool in_text = item.text().contains(s, Qt::CaseInsensitive); - bool in_albumartist = item.data(Role_AlbumArtist).toString().contains(s, Qt::CaseInsensitive); + bool in_text = album_item.text().contains(s, Qt::CaseInsensitive); + bool in_albumartist = album_item.data(Role_AlbumArtist).toString().contains(s, Qt::CaseInsensitive); if (!in_text && !in_albumartist) { return true; } @@ -518,12 +512,12 @@ bool AlbumCoverManager::ShouldHide(const AlbumItem &item, const QString &filter, void AlbumCoverManager::FetchAlbumCovers() { for (int i = 0; i < ui_->albums->count(); ++i) { - AlbumItem *item = static_cast(ui_->albums->item(i)); - if (item->isHidden()) continue; - if (ItemHasCover(*item)) continue; + AlbumItem *album_item = static_cast(ui_->albums->item(i)); + if (album_item->isHidden()) continue; + if (ItemHasCover(*album_item)) continue; - quint64 id = cover_fetcher_->FetchAlbumCover(item->data(Role_AlbumArtist).toString(), item->data(Role_Album).toString(), QString(), true); - cover_fetching_tasks_[id] = item; + quint64 id = cover_fetcher_->FetchAlbumCover(album_item->data(Role_AlbumArtist).toString(), album_item->data(Role_Album).toString(), QString(), true); + cover_fetching_tasks_[id] = album_item; jobs_++; } @@ -541,9 +535,9 @@ void AlbumCoverManager::AlbumCoverFetched(const quint64 id, const AlbumCoverImag if (!cover_fetching_tasks_.contains(id)) return; - AlbumItem *item = cover_fetching_tasks_.take(id); + AlbumItem *album_item = cover_fetching_tasks_.take(id); if (!result.image.isNull()) { - SaveAndSetCover(item, result); + SaveAndSetCover(album_item, result); } if (cover_fetching_tasks_.isEmpty()) { @@ -593,13 +587,13 @@ bool AlbumCoverManager::eventFilter(QObject *obj, QEvent *e) { bool some_unset = false; bool some_clear = false; - for (QListWidgetItem *item : context_menu_items_) { - AlbumItem *album_item = static_cast(item); + for (QListWidgetItem *list_widget_item : context_menu_items_) { + AlbumItem *album_item = static_cast(list_widget_item); if (ItemHasCover(*album_item)) some_with_covers = true; - if (album_item->data(Role_PathManual).toUrl().path() == Song::kManuallyUnsetCover) { + if (album_item->data(Role_ArtUnset).toBool()) { some_unset = true; } - else if (album_item->data(Role_PathAutomatic).toUrl().isEmpty() && album_item->data(Role_PathManual).toUrl().isEmpty()) { + else if (!album_item->data(Role_ArtEmbedded).toBool() && album_item->data(Role_ArtAutomatic).toUrl().isEmpty() && album_item->data(Role_ArtManual).toUrl().isEmpty()) { some_clear = true; } } @@ -623,19 +617,19 @@ bool AlbumCoverManager::eventFilter(QObject *obj, QEvent *e) { } Song AlbumCoverManager::GetSingleSelectionAsSong() { - return context_menu_items_.size() != 1 ? Song() : ItemAsSong(context_menu_items_[0]); + return context_menu_items_.size() != 1 ? Song() : AlbumItemAsSong(context_menu_items_[0]); } Song AlbumCoverManager::GetFirstSelectedAsSong() { - return context_menu_items_.isEmpty() ? Song() : ItemAsSong(context_menu_items_[0]); + return context_menu_items_.isEmpty() ? Song() : AlbumItemAsSong(context_menu_items_[0]); } -Song AlbumCoverManager::ItemAsSong(AlbumItem *item) { +Song AlbumCoverManager::AlbumItemAsSong(AlbumItem *album_item) { Song result(Song::Source::Collection); - QString title = item->data(Role_Album).toString(); - QString artist_name = item->data(Role_AlbumArtist).toString(); + QString title = album_item->data(Role_Album).toString(); + QString artist_name = album_item->data(Role_AlbumArtist).toString(); if (!artist_name.isEmpty()) { result.set_title(artist_name + " - " + title); } @@ -643,27 +637,30 @@ Song AlbumCoverManager::ItemAsSong(AlbumItem *item) { result.set_title(title); } - 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_artist(album_item->data(Role_AlbumArtist).toString()); + result.set_albumartist(album_item->data(Role_AlbumArtist).toString()); + result.set_album(album_item->data(Role_Album).toString()); - result.set_filetype(static_cast(item->data(Role_Filetype).toInt())); - result.set_url(item->urls.first()); - result.set_cue_path(item->data(Role_CuePath).toString()); + result.set_filetype(static_cast(album_item->data(Role_Filetype).toInt())); + result.set_url(album_item->urls.first()); + result.set_cue_path(album_item->data(Role_CuePath).toString()); - result.set_art_automatic(item->data(Role_PathAutomatic).toUrl()); - result.set_art_manual(item->data(Role_PathManual).toUrl()); + result.set_art_embedded(album_item->data(Role_ArtEmbedded).toBool()); + result.set_art_automatic(album_item->data(Role_ArtAutomatic).toUrl()); + result.set_art_manual(album_item->data(Role_ArtManual).toUrl()); + result.set_art_unset(album_item->data(Role_ArtUnset).toBool()); // force validity result.set_valid(true); result.set_id(0); return result; + } void AlbumCoverManager::ShowCover() { - Song song = GetSingleSelectionAsSong(); + const Song song = GetSingleSelectionAsSong(); if (!song.is_valid()) return; album_cover_choice_controller_->ShowCover(song); @@ -672,8 +669,8 @@ void AlbumCoverManager::ShowCover() { void AlbumCoverManager::FetchSingleCover() { - for (QListWidgetItem *item : context_menu_items_) { - AlbumItem *album_item = static_cast(item); + for (QListWidgetItem *list_widget_item : context_menu_items_) { + AlbumItem *album_item = static_cast(list_widget_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_++; @@ -686,11 +683,11 @@ void AlbumCoverManager::FetchSingleCover() { } -void AlbumCoverManager::UpdateCoverInList(AlbumItem *item, const QUrl &cover_url) { +void AlbumCoverManager::UpdateCoverInList(AlbumItem *album_item, const QUrl &cover_url) { - quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, QUrl(), cover_url); - item->setData(Role_PathManual, cover_url); - cover_loading_tasks_[id] = item; + album_item->setData(Role_ArtManual, cover_url); + album_item->setData(Role_ArtUnset, false); + LoadAlbumCoverAsync(album_item); } @@ -709,26 +706,31 @@ void AlbumCoverManager::LoadCoverFromFile() { void AlbumCoverManager::SaveCoverToFile() { Song song = GetSingleSelectionAsSong(); - if (!song.is_valid() || song.has_manually_unset_cover()) return; - - AlbumCoverImageResult result; + if (!song.is_valid() || song.art_unset()) return; // Load the image from disk - - 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().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()); + AlbumCoverImageResult result; + for (const AlbumCoverLoaderOptions::Type cover_type : cover_types_) { + switch (cover_type) { + case AlbumCoverLoaderOptions::Type::Unset: + return; + case AlbumCoverLoaderOptions::Type::Embedded: + if (song.art_embedded()) { + result.image_data = TagReaderClient::Instance()->LoadEmbeddedArtBlocking(song.url().toLocalFile()); + } + break; + case AlbumCoverLoaderOptions::Type::Automatic: + if (song.art_automatic_is_valid()) { + result.image_data = Utilities::ReadDataFromFile(song.art_automatic().toLocalFile()); + } + break; + case AlbumCoverLoaderOptions::Type::Manual: + if (song.art_manual_is_valid()) { + result.image_data = Utilities::ReadDataFromFile(song.art_manual().toLocalFile()); + } + break; + } + if (result.is_valid()) break; } if (!result.is_valid()) return; @@ -778,24 +780,32 @@ void AlbumCoverManager::SaveImageToAlbums(Song *song, const AlbumCoverImageResul } break; case CoverOptions::CoverType::Embedded: - cover_url = QUrl::fromLocalFile(Song::kEmbeddedCover); + cover_url.clear(); break; } // Force the found cover on all of the selected items QList urls; QList album_items; - for (QListWidgetItem *item : context_menu_items_) { - AlbumItem *album_item = static_cast(item); + for (QListWidgetItem *list_widget_item : context_menu_items_) { + AlbumItem *album_item = static_cast(list_widget_item); switch (album_cover_choice_controller_->get_save_album_cover_type()) { case CoverOptions::CoverType::Cache: case CoverOptions::CoverType::Album:{ - Song current_song = ItemAsSong(album_item); + Song current_song = AlbumItemAsSong(album_item); album_cover_choice_controller_->SaveArtManualToSong(¤t_song, cover_url); UpdateCoverInList(album_item, cover_url); break; } case CoverOptions::CoverType::Embedded:{ + for (const QUrl &url : album_item->urls) { + const bool art_embedded = !result.image_data.isEmpty(); + TagReaderReply *reply = app_->tag_reader_client()->SaveEmbeddedArt(url.toLocalFile(), TagReaderClient::SaveCoverOptions(result.image_data, result.mime_type)); + QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, album_item, url, art_embedded]() { + SaveEmbeddedCoverFinished(reply, album_item, url, art_embedded); + }); + cover_save_tasks_.insert(album_item, url); + } urls << album_item->urls; album_items << album_item; break; @@ -803,78 +813,53 @@ void AlbumCoverManager::SaveImageToAlbums(Song *song, const AlbumCoverImageResul } } - if (album_cover_choice_controller_->get_save_album_cover_type() == CoverOptions::CoverType::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); - } - } - } void AlbumCoverManager::UnsetCover() { - Song song = GetFirstSelectedAsSong(); - if (!song.is_valid()) return; - - AlbumItem *first_album_item = static_cast(context_menu_items_[0]); - - QUrl cover_url = album_cover_choice_controller_->UnsetCover(&song); + if (context_menu_items_.isEmpty()) return; // Force the 'none' cover on all of the selected items - for (QListWidgetItem *item : context_menu_items_) { - AlbumItem *album_item = static_cast(item); + for (QListWidgetItem *list_widget_item : context_menu_items_) { + AlbumItem *album_item = static_cast(list_widget_item); album_item->setIcon(icon_nocover_item_); - album_item->setData(Role_PathManual, cover_url); + album_item->setData(Role_ArtManual, QUrl()); + album_item->setData(Role_ArtUnset, true); - // 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, cover_url); - } + Song current_song = AlbumItemAsSong(album_item); + album_cover_choice_controller_->UnsetAlbumCoverForSong(¤t_song); } } 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); + if (context_menu_items_.isEmpty()) return; // Force the 'none' cover on all of the selected items - for (QListWidgetItem *item : context_menu_items_) { - AlbumItem *album_item = static_cast(item); + for (QListWidgetItem *list_widget_item : context_menu_items_) { + AlbumItem *album_item = static_cast(list_widget_item); album_item->setIcon(icon_nocover_item_); - album_item->setData(Role_PathManual, QUrl()); + album_item->setData(Role_ArtEmbedded, false); + album_item->setData(Role_ArtAutomatic, QUrl()); + album_item->setData(Role_ArtManual, 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); - } + Song current_song = AlbumItemAsSong(album_item); + album_cover_choice_controller_->ClearAlbumCoverForSong(¤t_song); } } void AlbumCoverManager::DeleteCover() { - for (QListWidgetItem *item : context_menu_items_) { - AlbumItem *album_item = static_cast(item); - Song song = ItemAsSong(album_item); + for (QListWidgetItem *list_widget_item : context_menu_items_) { + AlbumItem *album_item = static_cast(list_widget_item); + Song song = AlbumItemAsSong(album_item); album_cover_choice_controller_->DeleteCover(&song); album_item->setIcon(icon_nocover_item_); - album_item->setData(Role_PathManual, QUrl()); - album_item->setData(Role_PathAutomatic, QUrl()); + album_item->setData(Role_ArtEmbedded, QUrl()); + album_item->setData(Role_ArtManual, QUrl()); + album_item->setData(Role_ArtAutomatic, QUrl()); } } @@ -933,9 +918,9 @@ SongMimeData *AlbumCoverManager::GetMimeDataForAlbums(const QModelIndexList &ind void AlbumCoverManager::AlbumDoubleClicked(const QModelIndex &idx) { - AlbumItem *item = static_cast(idx.internalPointer()); - if (!item) return; - album_cover_choice_controller_->ShowCover(ItemAsSong(item)); + AlbumItem *album_item = static_cast(idx.internalPointer()); + if (!album_item) return; + album_cover_choice_controller_->ShowCover(AlbumItemAsSong(album_item)); } @@ -953,29 +938,25 @@ void AlbumCoverManager::LoadSelectedToPlaylist() { } -void AlbumCoverManager::SaveAndSetCover(AlbumItem *item, const AlbumCoverImageResult &result) { +void AlbumCoverManager::SaveAndSetCover(AlbumItem *album_item, const AlbumCoverImageResult &result) { - 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 = static_cast(item->data(Role_Filetype).toInt()); - const bool has_cue = !item->data(Role_CuePath).toString().isEmpty(); + const QList &urls = album_item->urls; + const Song::FileType filetype = static_cast(album_item->data(Role_Filetype).toInt()); + const bool has_cue = !album_item->data(Role_CuePath).toString().isEmpty(); if (album_cover_choice_controller_->get_save_album_cover_type() == CoverOptions::CoverType::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 if (!result.image.isNull()) { - quint64 id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.image); - cover_save_tasks_.insert(id, item); - } - else if (!result.cover_url.isEmpty() && result.cover_url.isLocalFile()) { - quint64 id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.cover_url.toLocalFile()); - cover_save_tasks_.insert(id, item); + for (const QUrl &url : urls) { + const bool art_embedded = !result.image_data.isEmpty(); + TagReaderReply *reply = app_->tag_reader_client()->SaveEmbeddedArt(url.toLocalFile(), TagReaderClient::SaveCoverOptions(result.cover_url.isValid() ? result.cover_url.toLocalFile() : QString(), result.image_data, result.mime_type)); + QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, album_item, url, art_embedded]() { + SaveEmbeddedCoverFinished(reply, album_item, url, art_embedded); + }); + cover_save_tasks_.insert(album_item, url); } } else { + const QString albumartist = album_item->data(Role_AlbumArtist).toString(); + const QString album = album_item->data(Role_Album).toString(); QUrl cover_url; if (!result.cover_url.isEmpty() && result.cover_url.isValid() && result.cover_url.isLocalFile()) { cover_url = result.cover_url; @@ -990,7 +971,7 @@ void AlbumCoverManager::SaveAndSetCover(AlbumItem *item, const AlbumCoverImageRe collection_backend_->UpdateManualAlbumArtAsync(albumartist, album, cover_url); // Update the icon in our list - UpdateCoverInList(item, cover_url); + UpdateCoverInList(album_item, cover_url); } } @@ -1006,16 +987,17 @@ void AlbumCoverManager::ExportCovers() { DisableCoversButtons(); cover_exporter_->SetDialogResult(result); + cover_exporter_->SetCoverTypes(cover_types_); for (int i = 0; i < ui_->albums->count(); ++i) { - AlbumItem *item = static_cast(ui_->albums->item(i)); + AlbumItem *album_item = static_cast(ui_->albums->item(i)); // skip hidden and coverless albums - if (item->isHidden() || !ItemHasCover(*item)) { + if (album_item->isHidden() || !ItemHasCover(*album_item)) { continue; } - cover_exporter_->AddExportRequest(ItemAsSong(item)); + cover_exporter_->AddExportRequest(AlbumItemAsSong(album_item)); } if (cover_exporter_->request_count() > 0) { @@ -1060,20 +1042,40 @@ void AlbumCoverManager::UpdateExportStatus(const int exported, const int skipped } -bool AlbumCoverManager::ItemHasCover(const AlbumItem &item) const { - return item.icon().cacheKey() != icon_nocover_item_.cacheKey(); +bool AlbumCoverManager::ItemHasCover(const AlbumItem &album_item) const { + return album_item.icon().cacheKey() != icon_nocover_item_.cacheKey(); } -void AlbumCoverManager::SaveEmbeddedCoverAsyncFinished(quint64 id, const bool success) { +void AlbumCoverManager::SaveEmbeddedCoverFinished(TagReaderReply *reply, AlbumItem *album_item, const QUrl &url, const bool art_embedded) { - 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; + if (cover_save_tasks_.contains(album_item, url)) { + cover_save_tasks_.remove(album_item, url); } + if (!reply->is_successful()) { + emit Error(tr("Could not save cover to file %1.").arg(url.toLocalFile())); + return; + } + + if (cover_save_tasks_.contains(album_item)) { + return; + } + + album_item->setData(Role_ArtEmbedded, true); + album_item->setData(Role_ArtUnset, false); + Song song = AlbumItemAsSong(album_item); + album_cover_choice_controller_->SaveArtEmbeddedToSong(&song, art_embedded); + LoadAlbumCoverAsync(album_item); + +} + +void AlbumCoverManager::LoadAlbumCoverAsync(AlbumItem *album_item) { + + AlbumCoverLoaderOptions cover_options(AlbumCoverLoaderOptions::Option::ScaledImage | AlbumCoverLoaderOptions::Option::PadScaledImage); + cover_options.types = cover_types_; + cover_options.desired_scaled_size = QSize(kThumbnailSize, kThumbnailSize); + cover_options.device_pixel_ratio = devicePixelRatioF(); + quint64 cover_load_id = app_->album_cover_loader()->LoadImageAsync(cover_options, album_item->data(Role_ArtEmbedded).toBool(), album_item->data(Role_ArtAutomatic).toUrl(), album_item->data(Role_ArtManual).toUrl(), album_item->data(Role_ArtUnset).toBool(), album_item->urls.first()); + cover_loading_tasks_.insert(cover_load_id, album_item); + } diff --git a/src/covermanager/albumcovermanager.h b/src/covermanager/albumcovermanager.h index 267137c1..205cce46 100644 --- a/src/covermanager/albumcovermanager.h +++ b/src/covermanager/albumcovermanager.h @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2023, 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 @@ -37,6 +37,7 @@ #include #include "core/song.h" +#include "core/tagreaderclient.h" #include "albumcoverloaderoptions.h" #include "albumcoverloaderresult.h" #include "albumcoverchoicecontroller.h" @@ -79,8 +80,6 @@ class AlbumCoverManager : public QMainWindow { explicit AlbumCoverManager(Application *app, CollectionBackend *collection_backend, QMainWindow *mainwindow, QWidget *parent = nullptr); ~AlbumCoverManager() override; - static const char *kSettingsGroup; - void Reset(); void Init(); @@ -108,8 +107,10 @@ class AlbumCoverManager : public QMainWindow { enum Role { Role_AlbumArtist = Qt::UserRole + 1, Role_Album, - Role_PathAutomatic, - Role_PathManual, + Role_ArtEmbedded, + Role_ArtAutomatic, + Role_ArtManual, + Role_ArtUnset, Role_Filetype, Role_CuePath, Role_ImageData, @@ -132,19 +133,21 @@ class AlbumCoverManager : public QMainWindow { // 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) { return ItemAsSong(static_cast(item)); } - static Song ItemAsSong(AlbumItem *item); + Song AlbumItemAsSong(QListWidgetItem *list_widget_item) { return AlbumItemAsSong(static_cast(list_widget_item)); } + static Song AlbumItemAsSong(AlbumItem *album_item); void UpdateStatusText(); - bool ShouldHide(const AlbumItem &item, const QString &filter, const HideCovers hide_covers) const; - void SaveAndSetCover(AlbumItem *item, const AlbumCoverImageResult &result); + bool ShouldHide(const AlbumItem &album_item, const QString &filter, const HideCovers hide_covers) const; + void SaveAndSetCover(AlbumItem *album_item, const AlbumCoverImageResult &result); void SaveImageToAlbums(Song *song, const AlbumCoverImageResult &result); SongList GetSongsInAlbums(const QModelIndexList &indexes) const; SongMimeData *GetMimeDataForAlbums(const QModelIndexList &indexes) const; - bool ItemHasCover(const AlbumItem &item) const; + bool ItemHasCover(const AlbumItem &album_item) const; + + void LoadAlbumCoverAsync(AlbumItem *album_item); signals: void Error(const QString &error); @@ -176,12 +179,15 @@ class AlbumCoverManager : public QMainWindow { void AddSelectedToPlaylist(); void LoadSelectedToPlaylist(); - void UpdateCoverInList(AlbumItem *item, const QUrl &cover); + void UpdateCoverInList(AlbumItem *album_item, const QUrl &cover); void UpdateExportStatus(const int exported, const int skipped, const int max); - void SaveEmbeddedCoverAsyncFinished(quint64 id, const bool success); + void SaveEmbeddedCoverFinished(TagReaderReply *reply, AlbumItem *album_item, const QUrl &url, const bool art_embedded); private: + static const char *kSettingsGroup; + static const int kThumbnailSize; + Ui_CoverManager *ui_; QMainWindow *mainwindow_; Application *app_; @@ -192,7 +198,6 @@ class AlbumCoverManager : public QMainWindow { QAction *filter_with_covers_; QAction *filter_without_covers_; - AlbumCoverLoaderOptions cover_loader_options_; QMap cover_loading_tasks_; AlbumCoverFetcher *cover_fetcher_; @@ -215,11 +220,11 @@ class AlbumCoverManager : public QMainWindow { QPushButton *abort_progress_; int jobs_; - QMultiMap cover_save_tasks_; - QList cover_save_tasks2_; + QMultiMap cover_save_tasks_; QListWidgetItem *all_artists_; + AlbumCoverLoaderOptions::Types cover_types_; }; #endif // ALBUMCOVERMANAGER_H diff --git a/src/covermanager/albumcoversearcher.cpp b/src/covermanager/albumcoversearcher.cpp index 347d04a1..d064cd6b 100644 --- a/src/covermanager/albumcoversearcher.cpp +++ b/src/covermanager/albumcoversearcher.cpp @@ -128,14 +128,6 @@ 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; - options_.pad_thumbnail_image_ = true; - options_.thumbnail_size_ = ui_->covers->iconSize(); - QObject::connect(app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &AlbumCoverSearcher::AlbumCoverLoaded); QObject::connect(ui_->search, &QPushButton::clicked, this, &AlbumCoverSearcher::Search); QObject::connect(ui_->covers, &GroupedIconView::doubleClicked, this, &AlbumCoverSearcher::CoverDoubleClicked); @@ -222,7 +214,9 @@ void AlbumCoverSearcher::SearchFinished(const quint64 id, const CoverProviderSea if (result.image_url.isEmpty()) continue; - quint64 new_id = app_->album_cover_loader()->LoadImageAsync(options_, result.image_url, QUrl()); + AlbumCoverLoaderOptions cover_options(AlbumCoverLoaderOptions::Option::RawImageData | AlbumCoverLoaderOptions::Option::OriginalImage | AlbumCoverLoaderOptions::Option::ScaledImage | AlbumCoverLoaderOptions::Option::PadScaledImage); + cover_options.desired_scaled_size = ui_->covers->iconSize(), ui_->covers->iconSize(); + quint64 new_id = app_->album_cover_loader()->LoadImageAsync(cover_options, false, result.image_url, QUrl(), false); QStandardItem *item = new QStandardItem; item->setIcon(no_cover_icon_); @@ -249,12 +243,12 @@ void AlbumCoverSearcher::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoad if (cover_loading_tasks_.isEmpty()) ui_->busy->hide(); - if (!result.success || result.album_cover.image_data.isNull() || result.album_cover.image.isNull() || result.image_thumbnail.isNull()) { + if (!result.success || result.album_cover.image_data.isNull() || result.album_cover.image.isNull() || result.image_scaled.isNull()) { model_->removeRow(item->row()); return; } - const QPixmap pixmap = QPixmap::fromImage(result.image_thumbnail); + const QPixmap pixmap = QPixmap::fromImage(result.image_scaled); if (pixmap.isNull()) { model_->removeRow(item->row()); return; diff --git a/src/covermanager/albumcoversearcher.h b/src/covermanager/albumcoversearcher.h index 972a1636..73505d83 100644 --- a/src/covermanager/albumcoversearcher.h +++ b/src/covermanager/albumcoversearcher.h @@ -107,7 +107,6 @@ class AlbumCoverSearcher : public QDialog { QStandardItemModel *model_; QIcon no_cover_icon_; - AlbumCoverLoaderOptions options_; AlbumCoverFetcher *fetcher_; quint64 id_; diff --git a/src/covermanager/coverexportrunnable.cpp b/src/covermanager/coverexportrunnable.cpp index 6ef908d3..499f73b7 100644 --- a/src/covermanager/coverexportrunnable.cpp +++ b/src/covermanager/coverexportrunnable.cpp @@ -28,20 +28,19 @@ #include "core/song.h" #include "core/tagreaderclient.h" +#include "albumcoverloaderoptions.h" #include "albumcoverexport.h" #include "coverexportrunnable.h" -CoverExportRunnable::CoverExportRunnable(const AlbumCoverExport::DialogResult &dialog_result, const Song &song, QObject *parent) +CoverExportRunnable::CoverExportRunnable(const AlbumCoverExport::DialogResult &dialog_result, const AlbumCoverLoaderOptions::Types cover_types, const Song &song, QObject *parent) : QObject(parent), dialog_result_(dialog_result), + cover_types_(cover_types), song_(song) {} void CoverExportRunnable::run() { - QString cover_path = GetCoverPath(); - - // Manually unset? - if (cover_path.isEmpty()) { + if (song_.art_unset() || (!song_.art_embedded() && !song_.art_automatic_is_valid() && !song_.art_manual_is_valid())) { EmitCoverSkipped(); } else { @@ -55,25 +54,6 @@ void CoverExportRunnable::run() { } -QString CoverExportRunnable::GetCoverPath() { - - if (song_.has_manually_unset_cover()) { - return QString(); - // Export downloaded covers? - } - else if (!song_.art_manual().isEmpty() && dialog_result_.export_downloaded_) { - return song_.art_manual().toLocalFile(); - // Export embedded covers? - } - else if (!song_.art_automatic().isEmpty() && song_.art_automatic().path() == Song::kEmbeddedCover && dialog_result_.export_embedded_) { - return song_.art_automatic().toLocalFile(); - } - else { - return QString(); - } - -} - // Exports a single album cover using a "save QImage to file" approach. // For performance reasons this method will be invoked only if loading and in memory processing of images is necessary for current settings which means that: // - either the force size flag is being used @@ -81,45 +61,57 @@ QString CoverExportRunnable::GetCoverPath() { // In all other cases, the faster ExportCover() method will be used. void CoverExportRunnable::ProcessAndExportCover() { - QString cover_path = GetCoverPath(); + QImage image; + QString extension; - // either embedded or disk - the one we'll export for the current album - QImage cover; - - QImage embedded_cover; - QImage disk_cover; - - // load embedded cover if any - if (song_.has_embedded_cover()) { - embedded_cover = TagReaderClient::Instance()->LoadEmbeddedArtAsImageBlocking(song_.url().toLocalFile()); - - if (embedded_cover.isNull()) { - EmitCoverSkipped(); - return; + for (const AlbumCoverLoaderOptions::Type cover_type : cover_types_) { + switch (cover_type) { + case AlbumCoverLoaderOptions::Type::Unset: + if (song_.art_unset()) { + EmitCoverSkipped(); + return; + } + break; + case AlbumCoverLoaderOptions::Type::Embedded: + if (song_.art_embedded() && dialog_result_.export_embedded_) { + image = TagReaderClient::Instance()->LoadEmbeddedArtAsImageBlocking(song_.url().toLocalFile()); + if (!image.isNull()) { + extension = "jpg"; + } + } + break; + case AlbumCoverLoaderOptions::Type::Manual: + if (dialog_result_.export_downloaded_ && song_.art_manual_is_valid()) { + const QString cover_path = song_.art_manual().toLocalFile(); + if (image.load(cover_path)) { + extension = cover_path.section('.', -1); + } + } + break; + case AlbumCoverLoaderOptions::Type::Automatic: + if (dialog_result_.export_downloaded_ && song_.art_automatic_is_valid()) { + const QString cover_path = song_.art_automatic().toLocalFile(); + if (image.load(cover_path)) { + extension = cover_path.section('.', -1); + } + } + break; } - cover = embedded_cover; + if (!image.isNull() && !extension.isEmpty()) break; } - // load a file cover which iss mandatory if there's no embedded cover - disk_cover.load(cover_path); - - if (embedded_cover.isNull()) { - if (disk_cover.isNull()) { - EmitCoverSkipped(); - return; - } - cover = disk_cover; + if (image.isNull() || extension.isEmpty()) { + EmitCoverSkipped(); + return; } - // rescale if necessary + // Rescale if necessary if (dialog_result_.IsSizeForced()) { - cover = cover.scaled(QSize(dialog_result_.width_, dialog_result_.height_), Qt::IgnoreAspectRatio); + image = image.scaled(QSize(dialog_result_.width_, dialog_result_.height_), Qt::IgnoreAspectRatio); } - QString dir = song_.url().toLocalFile().section('/', 0, -2); - QString extension = cover_path.section('.', -1); - - QString new_file = dir + '/' + dialog_result_.filename_ + '.' + (cover_path == Song::kEmbeddedCover ? "jpg" : extension); + QString cover_dir = song_.url().toLocalFile().section('/', 0, -2); + QString new_file = cover_dir + '/' + dialog_result_.filename_ + '.' + (song_.art_embedded() ? "jpg" : extension); // If the file exists, do not override! if (dialog_result_.overwrite_ == AlbumCoverExport::OverwriteMode::None && QFile::exists(new_file)) { @@ -127,16 +119,15 @@ void CoverExportRunnable::ProcessAndExportCover() { return; } - // we're handling overwrite as remove + copy so we need to delete the old file first + // We're handling overwrite as remove + copy so we need to delete the old file first if (QFile::exists(new_file) && dialog_result_.overwrite_ != AlbumCoverExport::OverwriteMode::None) { // if the mode is "overwrite smaller" then skip the cover if a bigger one is already available in the folder if (dialog_result_.overwrite_ == AlbumCoverExport::OverwriteMode::Smaller) { - QImage existing; - existing.load(new_file); - - if (existing.isNull() || existing.size().height() >= cover.size().height() || existing.size().width() >= cover.size().width()) { + QImage image_existing; + image_existing.load(new_file); + if (image_existing.isNull() || image_existing.size().height() >= image.size().height() || image_existing.size().width() >= image.size().width()) { EmitCoverSkipped(); return; } @@ -148,7 +139,7 @@ void CoverExportRunnable::ProcessAndExportCover() { } } - if (cover.save(new_file)) { + if (image.save(new_file)) { EmitCoverExported(); } else { @@ -160,12 +151,55 @@ void CoverExportRunnable::ProcessAndExportCover() { // Exports a single album cover using a "copy file" approach. void CoverExportRunnable::ExportCover() { - QString cover_path = GetCoverPath(); + QImage image; + QString extension; + QString cover_path; + bool embedded_cover = false; - QString dir = song_.url().toLocalFile().section('/', 0, -2); - QString extension = cover_path.section('.', -1); + for (const AlbumCoverLoaderOptions::Type cover_type : cover_types_) { + switch (cover_type) { + case AlbumCoverLoaderOptions::Type::Unset: + if (song_.art_unset()) { + EmitCoverSkipped(); + return; + } + break; + case AlbumCoverLoaderOptions::Type::Embedded: + if (song_.art_embedded() && dialog_result_.export_embedded_) { + image = TagReaderClient::Instance()->LoadEmbeddedArtAsImageBlocking(song_.url().toLocalFile()); + if (!image.isNull()) { + embedded_cover = true; + extension = "jpg"; + } + } + break; + case AlbumCoverLoaderOptions::Type::Manual: + if (dialog_result_.export_downloaded_ && song_.art_manual_is_valid()) { + cover_path = song_.art_manual().toLocalFile(); + if (image.load(cover_path)) { + extension = cover_path.section('.', -1); + } + } + break; + case AlbumCoverLoaderOptions::Type::Automatic: + if (dialog_result_.export_downloaded_ && song_.art_automatic_is_valid()) { + cover_path = song_.art_automatic().toLocalFile(); + if (image.load(cover_path)) { + extension = cover_path.section('.', -1); + } + } + break; + } + if (!image.isNull() && !extension.isEmpty() && (embedded_cover || !cover_path.isEmpty())) break; + } - QString new_file = dir + '/' + dialog_result_.filename_ + '.' + (cover_path == Song::kEmbeddedCover ? "jpg" : extension); + if (image.isNull() || extension.isEmpty()) { + EmitCoverSkipped(); + return; + } + + QString cover_dir = song_.url().toLocalFile().section('/', 0, -2); + QString new_file = cover_dir + '/' + dialog_result_.filename_ + '.' + extension; // If the file exists, do not override! if (dialog_result_.overwrite_ == AlbumCoverExport::OverwriteMode::None && QFile::exists(new_file)) { @@ -181,10 +215,8 @@ void CoverExportRunnable::ExportCover() { } } - if (cover_path == Song::kEmbeddedCover) { - // an embedded cover - QImage embedded = TagReaderClient::Instance()->LoadEmbeddedArtAsImageBlocking(song_.url().toLocalFile()); - if (!embedded.save(new_file)) { + if (embedded_cover) { + if (!image.save(new_file)) { EmitCoverSkipped(); return; } @@ -196,6 +228,7 @@ void CoverExportRunnable::ExportCover() { return; } } + EmitCoverExported(); } diff --git a/src/covermanager/coverexportrunnable.h b/src/covermanager/coverexportrunnable.h index ff06cf95..cfb3e6a3 100644 --- a/src/covermanager/coverexportrunnable.h +++ b/src/covermanager/coverexportrunnable.h @@ -29,13 +29,14 @@ #include #include "core/song.h" +#include "albumcoverloaderoptions.h" #include "albumcoverexport.h" class CoverExportRunnable : public QObject, public QRunnable { Q_OBJECT public: - explicit CoverExportRunnable(const AlbumCoverExport::DialogResult &dialog_result, const Song &song, QObject *parent = nullptr); + explicit CoverExportRunnable(const AlbumCoverExport::DialogResult &dialog_result, const AlbumCoverLoaderOptions::Types cover_types, const Song &song, QObject *parent = nullptr); void run() override; @@ -49,11 +50,10 @@ class CoverExportRunnable : public QObject, public QRunnable { void ProcessAndExportCover(); void ExportCover(); - QString GetCoverPath(); AlbumCoverExport::DialogResult dialog_result_; + AlbumCoverLoaderOptions::Types cover_types_; Song song_; - }; #endif // COVEREXPORTRUNNABLE_H diff --git a/src/covermanager/coverproviders.cpp b/src/covermanager/coverproviders.cpp index b958cff6..4042ec1a 100644 --- a/src/covermanager/coverproviders.cpp +++ b/src/covermanager/coverproviders.cpp @@ -58,7 +58,7 @@ void CoverProviders::ReloadSettings() { QSettings s; s.beginGroup(CoversSettingsPage::kSettingsGroup); - QStringList providers_enabled = s.value("providers", QStringList() << all_providers.values()).toStringList(); + QStringList providers_enabled = s.value(CoversSettingsPage::kProviders, QStringList() << all_providers.values()).toStringList(); s.endGroup(); int i = 0; diff --git a/src/covermanager/currentalbumcoverloader.cpp b/src/covermanager/currentalbumcoverloader.cpp index f48799b5..529fdefa 100644 --- a/src/covermanager/currentalbumcoverloader.cpp +++ b/src/covermanager/currentalbumcoverloader.cpp @@ -42,18 +42,15 @@ CurrentAlbumCoverLoader::CurrentAlbumCoverLoader(Application *app, QObject *pare temp_file_pattern_(QDir::tempPath() + "/strawberry-cover-XXXXXX.jpg"), 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; - options_.thumbnail_size_ = QSize(120, 120); - options_.default_output_image_ = QImage(":/pictures/cdcase.png"); - options_.default_thumbnail_image_ = options_.default_output_image_.scaledToHeight(120, Qt::SmoothTransformation); + options_.options = AlbumCoverLoaderOptions::Option::RawImageData | AlbumCoverLoaderOptions::Option::OriginalImage | AlbumCoverLoaderOptions::Option::ScaledImage; + options_.desired_scaled_size = QSize(120, 120); + options_.default_cover = ":/pictures/cdcase.png"; QObject::connect(app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &CurrentAlbumCoverLoader::TempAlbumCoverLoaded); QObject::connect(app_->playlist_manager(), &PlaylistManager::CurrentSongChanged, this, &CurrentAlbumCoverLoader::LoadAlbumCover); + ReloadSettingsAsync(); + } CurrentAlbumCoverLoader::~CurrentAlbumCoverLoader() { @@ -63,6 +60,18 @@ CurrentAlbumCoverLoader::~CurrentAlbumCoverLoader() { } +void CurrentAlbumCoverLoader::ReloadSettingsAsync() { + + QMetaObject::invokeMethod(this, &CurrentAlbumCoverLoader::ReloadSettings); + +} + +void CurrentAlbumCoverLoader::ReloadSettings() { + + options_.types = AlbumCoverLoaderOptions::LoadTypes(); + +} + void CurrentAlbumCoverLoader::LoadAlbumCover(const Song &song) { last_song_ = song; @@ -92,11 +101,11 @@ void CurrentAlbumCoverLoader::TempAlbumCoverLoaded(const quint64 id, AlbumCoverL } QUrl thumbnail_url; - if (!result.image_thumbnail.isNull()) { + if (!result.image_scaled.isNull()) { temp_cover_thumbnail_ = std::make_unique(temp_file_pattern_); temp_cover_thumbnail_->setAutoRemove(true); if (temp_cover_thumbnail_->open()) { - if (result.image_thumbnail.save(temp_cover_thumbnail_->fileName(), "JPEG")) { + if (result.image_scaled.save(temp_cover_thumbnail_->fileName(), "JPEG")) { thumbnail_url = QUrl::fromLocalFile(temp_cover_thumbnail_->fileName()); } else { @@ -113,6 +122,6 @@ void CurrentAlbumCoverLoader::TempAlbumCoverLoaded(const quint64 id, AlbumCoverL } emit AlbumCoverLoaded(last_song_, result); - emit ThumbnailLoaded(last_song_, thumbnail_url, result.image_thumbnail); + emit ThumbnailLoaded(last_song_, thumbnail_url, result.image_scaled); } diff --git a/src/covermanager/currentalbumcoverloader.h b/src/covermanager/currentalbumcoverloader.h index 22f3b00c..b9ef6201 100644 --- a/src/covermanager/currentalbumcoverloader.h +++ b/src/covermanager/currentalbumcoverloader.h @@ -48,7 +48,10 @@ class CurrentAlbumCoverLoader : public QObject { const AlbumCoverLoaderOptions &options() const { return options_; } const Song &last_song() const { return last_song_; } + void ReloadSettingsAsync(); + public slots: + void ReloadSettings(); void LoadAlbumCover(const Song &song); signals: diff --git a/src/dialogs/edittagdialog.cpp b/src/dialogs/edittagdialog.cpp index b66423e8..851866a3 100644 --- a/src/dialogs/edittagdialog.cpp +++ b/src/dialogs/edittagdialog.cpp @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2023, 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 @@ -98,9 +98,10 @@ #include "ui_edittagdialog.h" #include "tagreadermessages.pb.h" -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"; +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"; +const int EditTagDialog::kSmallImageSize = 128; EditTagDialog::EditTagDialog(Application *app, QWidget *parent) : QDialog(parent), @@ -111,7 +112,7 @@ EditTagDialog::EditTagDialog(Application *app, QWidget *parent) tag_fetcher_(new TagFetcher(app->network(), this)), results_dialog_(new TrackSelectionDialog(this)), #endif - image_no_cover_thumbnail_(ImageUtils::GenerateNoCoverImage(QSize(128, 128))), + image_no_cover_thumbnail_(ImageUtils::GenerateNoCoverImage(QSize(128, 128), devicePixelRatioF())), loading_(false), ignore_edits_(false), summary_cover_art_id_(-1), @@ -251,11 +252,6 @@ EditTagDialog::EditTagDialog(Application *app, QWidget *parent) new TagCompleter(app_->collection_backend(), Playlist::Column_Performer, ui_->performer); new TagCompleter(app_->collection_backend(), Playlist::Column_Grouping, ui_->grouping); - cover_options_.get_image_data_ = true; - cover_options_.get_image_ = true; - cover_options_.scale_output_image_ = true; - cover_options_.desired_height_ = 128; - } EditTagDialog::~EditTagDialog() { @@ -280,6 +276,8 @@ void EditTagDialog::showEvent(QShowEvent *e) { album_cover_choice_controller_->ReloadSettings(); + cover_types_ = AlbumCoverLoaderOptions::LoadTypes(); + } QDialog::showEvent(e); @@ -328,7 +326,7 @@ bool EditTagDialog::eventFilter(QObject *o, QEvent *e) { ShowCover(); break; - case QEvent::DragEnter: { + case QEvent::DragEnter:{ QDragEnterEvent *event = static_cast(e); if (AlbumCoverChoiceController::CanAcceptDrag(event)) { event->acceptProposedAction(); @@ -336,7 +334,7 @@ bool EditTagDialog::eventFilter(QObject *o, QEvent *e) { break; } - case QEvent::Drop: { + case QEvent::Drop:{ const QDropEvent *event = static_cast(e); if (event->mimeData()->hasImage()) { QImage image = qvariant_cast(event->mimeData()->imageData()); @@ -438,7 +436,7 @@ void EditTagDialog::SetSongsFinished() { ui_->tab_widget->setEnabled(false); // Show a summary with empty information - UpdateSummaryTab(Song(), UpdateCoverAction::None); + UpdateSummaryTab(Song()); ui_->tab_widget->setCurrentWidget(ui_->tab_summary); SetSongListVisibility(false); @@ -614,11 +612,11 @@ void EditTagDialog::SelectionChanged() { ui_->tab_widget->setTabEnabled(ui_->tab_widget->indexOf(ui_->tab_lyrics), !multiple); if (multiple) { - UpdateSummaryTab(Song(), UpdateCoverAction::None); + UpdateSummaryTab(Song()); UpdateStatisticsTab(Song()); } else { - UpdateSummaryTab(data_[indexes.first().row()].original_, data_[indexes.first().row()].cover_action_); + UpdateSummaryTab(data_[indexes.first().row()].original_); UpdateStatisticsTab(data_[indexes.first().row()].original_); } @@ -645,9 +643,9 @@ void EditTagDialog::SelectionChanged() { } 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())) + song.art_embedded() != first_song.art_embedded() || + song.art_automatic() != first_song.art_automatic() || + (song.art_embedded() && first_song.art_embedded() && (first_song.effective_albumartist() != song.effective_albumartist() || first_song.album() != song.album())) ) { art_different = true; } @@ -681,7 +679,6 @@ void EditTagDialog::SelectionChanged() { } QString summary; - if (indexes.count() == 1) { summary += "

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

"; } @@ -690,6 +687,7 @@ void EditTagDialog::SelectionChanged() { summary += tr("%1 songs selected.").arg(indexes.count()); summary += "

"; } + ui_->tags_summary->setText(summary); const bool enable_change_art = first_song.is_collection_song(); ui_->tags_art_button->setEnabled(enable_change_art); @@ -709,26 +707,27 @@ void EditTagDialog::SelectionChanged() { } 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_->show_cover_action()->setEnabled(first_song.has_valid_art() && !first_song.art_unset()); + album_cover_choice_controller_->cover_to_file_action()->setEnabled(first_song.has_valid_art() && !first_song.art_unset()); album_cover_choice_controller_->cover_from_file_action()->setEnabled(enable_change_art); album_cover_choice_controller_->cover_from_url_action()->setEnabled(enable_change_art); album_cover_choice_controller_->search_for_cover_action()->setEnabled(app_->cover_providers()->HasAnyProviders() && enable_change_art); - album_cover_choice_controller_->unset_cover_action()->setEnabled(enable_change_art && !first_song.has_manually_unset_cover()); + album_cover_choice_controller_->unset_cover_action()->setEnabled(enable_change_art && !first_song.art_unset()); album_cover_choice_controller_->clear_cover_action()->setEnabled(enable_change_art && !first_song.art_manual().isEmpty()); - album_cover_choice_controller_->delete_cover_action()->setEnabled(enable_change_art && first_song.has_valid_art() && !first_song.has_manually_unset_cover()); + album_cover_choice_controller_->delete_cover_action()->setEnabled(enable_change_art && (first_song.art_embedded() || !first_song.art_automatic().isEmpty() || !first_song.art_manual().isEmpty())); + AlbumCoverLoaderOptions cover_options(AlbumCoverLoaderOptions::Option::RawImageData | AlbumCoverLoaderOptions::Option::OriginalImage | AlbumCoverLoaderOptions::Option::ScaledImage | AlbumCoverLoaderOptions::Option::PadScaledImage); + cover_options.types = cover_types_; + cover_options.desired_scaled_size = QSize(kSmallImageSize, kSmallImageSize); + cover_options.device_pixel_ratio = devicePixelRatioF(); if (data_[indexes.first().row()].cover_action_ == UpdateCoverAction::None) { - tags_cover_art_id_ = app_->album_cover_loader()->LoadImageAsync(cover_options_, first_song); + 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_); + 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); - - const bool embedded_cover = (first_song.save_embedded_cover_supported() && (first_song.has_embedded_cover() || album_cover_choice_controller_->get_collection_save_album_cover_type() == CoverOptions::CoverType::Embedded)); + const bool embedded_cover = (first_song.save_embedded_cover_supported() && (first_song.art_embedded() || album_cover_choice_controller_->get_collection_save_album_cover_type() == CoverOptions::CoverType::Embedded)); ui_->checkbox_embedded_cover->setChecked(embedded_cover); album_cover_choice_controller_->set_save_embedded_cover_override(embedded_cover); @@ -769,15 +768,15 @@ void EditTagDialog::SetDate(QLabel *label, const uint time) { } -void EditTagDialog::UpdateSummaryTab(const Song &song, const UpdateCoverAction cover_action) { +void EditTagDialog::UpdateSummaryTab(const Song &song) { - summary_cover_art_id_ = app_->album_cover_loader()->LoadImageAsync(cover_options_, song); + AlbumCoverLoaderOptions cover_options(AlbumCoverLoaderOptions::Option::ScaledImage | AlbumCoverLoaderOptions::Option::PadScaledImage); + cover_options.types = cover_types_; + cover_options.desired_scaled_size = QSize(kSmallImageSize, kSmallImageSize); + cover_options.device_pixel_ratio = devicePixelRatioF(); + summary_cover_art_id_ = app_->album_cover_loader()->LoadImageAsync(cover_options, song); - QString summary = "

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

"; - - summary += GetArtSummary(song, cover_action); - - ui_->summary->setText(summary); + ui_->summary->setText("

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

"); ui_->length->setText(Utilities::PrettyTimeNanosec(song.length_nanosec())); @@ -805,12 +804,11 @@ void EditTagDialog::UpdateSummaryTab(const Song &song, const UpdateCoverAction c ui_->path->clear(); } + ui_->art_embedded->setText(song.art_embedded() ? tr("Yes") : tr("No")); + 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()); } @@ -818,58 +816,38 @@ void EditTagDialog::UpdateSummaryTab(const Song &song, const UpdateCoverAction c 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()); } + ui_->art_unset->setText(song.art_unset() ? tr("Yes") : tr("No")); + } -QString EditTagDialog::GetArtSummary(const Song &song, const UpdateCoverAction cover_action) { +QString EditTagDialog::GetArtSummary(const Song &song, const AlbumCoverLoaderResult::Type cover_type) { 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; - } + switch (cover_type) { + case AlbumCoverLoaderResult::Type::None: + break; + case AlbumCoverLoaderResult::Type::Unset: + summary = tr("Cover is unset.").toHtmlEscaped(); + break; + case AlbumCoverLoaderResult::Type::Embedded: + summary = tr("Cover from embedded image."); + break; + case AlbumCoverLoaderResult::Type::Manual: + summary = tr("Cover from %1").arg(song.art_manual().toString()).toHtmlEscaped(); + break; + case AlbumCoverLoaderResult::Type::Automatic: + summary = tr("Cover from %1").arg(song.art_automatic().toString()).toHtmlEscaped(); + break; } - else if (song.art_manual().isEmpty() && song.art_automatic().isEmpty()) { + + if (summary.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(); - } if (!song.is_collection_song()) { if (!summary.isEmpty()) summary += "
"; @@ -880,6 +858,25 @@ QString EditTagDialog::GetArtSummary(const Song &song, const UpdateCoverAction c } +QString EditTagDialog::GetArtSummary(const UpdateCoverAction cover_action) { + + switch (cover_action) { + case UpdateCoverAction::Clear: + return tr("Cover changed: Will be cleared when saved.").toHtmlEscaped(); + case UpdateCoverAction::Unset: + return tr("Cover changed: Will be unset when saved.").toHtmlEscaped(); + case UpdateCoverAction::Delete: + return tr("Cover changed: Will be deleted when saved.").toHtmlEscaped(); + case UpdateCoverAction::New: + return tr("Cover changed: Will set new when saved.").toHtmlEscaped(); + case UpdateCoverAction::None: + break; + } + + return QString(); + +} + void EditTagDialog::UpdateStatisticsTab(const Song &song) { ui_->playcount->setText(QString::number(song.playcount())); @@ -890,38 +887,58 @@ void EditTagDialog::UpdateStatisticsTab(const Song &song) { void EditTagDialog::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result) { - if (id == tags_cover_art_id_) { - ui_->tags_art->clear(); - bool enable_change_art = false; - 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; - enable_change_art = data_[idx.row()].original_.is_collection_song(); - } - } - else { - ui_->tags_art->setPixmap(QPixmap::fromImage(image_no_cover_thumbnail_)); - for (const QModelIndex &idx : ui_->song_list->selectionModel()->selectedIndexes()) { - data_[idx.row()].cover_result_ = AlbumCoverImageResult(); - enable_change_art = data_[idx.row()].original_.is_collection_song(); - } - } - 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(enable_change_art && 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) { + if (id == summary_cover_art_id_) { + if (result.success && !result.image_scaled.isNull() && result.type != AlbumCoverLoaderResult::Type::Unset) { ui_->summary_art->setPixmap(QPixmap::fromImage(result.image_scaled)); } else { ui_->summary_art->setPixmap(QPixmap::fromImage(image_no_cover_thumbnail_)); } + if (ui_->song_list->selectionModel()->selectedIndexes().count() > 0) { + const QModelIndex idx = ui_->song_list->selectionModel()->selectedIndexes().first(); + QString summary = ui_->summary->toPlainText(); + summary += "
"; + summary += "
"; + summary += GetArtSummary(data_[idx.row()].current_, result.type); + ui_->summary->setText(summary); + } summary_cover_art_id_ = -1; } + else if (id == tags_cover_art_id_) { + if (result.success && !result.image_scaled.isNull() && result.type != AlbumCoverLoaderResult::Type::Unset) { + ui_->tags_art->setPixmap(QPixmap::fromImage(result.image_scaled)); + } + else { + ui_->tags_art->setPixmap(QPixmap::fromImage(image_no_cover_thumbnail_)); + } + Song first_song; + UpdateCoverAction cover_action; + for (const QModelIndex &idx : ui_->song_list->selectionModel()->selectedIndexes()) { + data_[idx.row()].cover_result_ = result.album_cover; + if (!first_song.is_valid()) { + first_song = data_[idx.row()].current_; + cover_action = data_[idx.row()].cover_action_; + } + } + bool enable_change_art = false; + if (first_song.is_valid()) { + QString summary = ui_->tags_summary->toPlainText(); + summary += "
"; + summary += "
"; + if (cover_action == UpdateCoverAction::None) { + summary += GetArtSummary(first_song, result.type); + } + else { + summary += GetArtSummary(cover_action); + } + ui_->tags_summary->setText(summary); + enable_change_art = first_song.is_collection_song() && !first_song.effective_albumartist().isEmpty() && !first_song.album().isEmpty(); + } + tags_cover_art_id_ = -1; + album_cover_choice_controller_->show_cover_action()->setEnabled(result.success && result.type != AlbumCoverLoaderResult::Type::Unset); + album_cover_choice_controller_->cover_to_file_action()->setEnabled(result.success && result.type != AlbumCoverLoaderResult::Type::Unset); + album_cover_choice_controller_->delete_cover_action()->setEnabled(enable_change_art && result.success && result.type != AlbumCoverLoaderResult::Type::Unset); + } } @@ -1018,7 +1035,10 @@ void EditTagDialog::UnsetCover() { Song *song = GetFirstSelected(); if (!song) return; - song->set_manually_unset_cover(); + song->set_art_embedded(false); + song->clear_art_automatic(); + song->clear_art_manual(); + song->set_art_unset(true); UpdateCover(UpdateCoverAction::Unset); @@ -1029,8 +1049,10 @@ void EditTagDialog::ClearCover() { Song *song = GetFirstSelected(); if (!song) return; + song->set_art_embedded(false); song->clear_art_automatic(); song->clear_art_manual(); + song->set_art_unset(false); UpdateCover(UpdateCoverAction::Clear); @@ -1050,7 +1072,7 @@ void EditTagDialog::ShowCover() { } -void EditTagDialog::UpdateCover(const UpdateCoverAction action, const AlbumCoverImageResult &result) { +void EditTagDialog::UpdateCover(const UpdateCoverAction cover_action, const AlbumCoverImageResult &cover_result) { const QModelIndexList indexes = ui_->song_list->selectionModel()->selectedIndexes(); if (indexes.isEmpty()) return; @@ -1059,17 +1081,20 @@ void EditTagDialog::UpdateCover(const UpdateCoverAction action, const AlbumCover 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()].cover_action_ = cover_action; + data_[idx.row()].cover_result_ = cover_result; + if (cover_action == UpdateCoverAction::New) { data_[idx.row()].current_.clear_art_manual(); + data_[idx.row()].current_.set_art_unset(false); } - else if (action == UpdateCoverAction::Unset) { - data_[idx.row()].current_.set_manually_unset_cover(); + else if (cover_action == UpdateCoverAction::Unset) { + data_[idx.row()].current_.set_art_unset(true); } - else if (action == UpdateCoverAction::Clear || action == UpdateCoverAction::Delete) { + else if (cover_action == UpdateCoverAction::Clear || cover_action == UpdateCoverAction::Delete) { + data_[idx.row()].current_.set_art_embedded(false); data_[idx.row()].current_.clear_art_manual(); data_[idx.row()].current_.clear_art_automatic(); + data_[idx.row()].current_.set_art_unset(false); } if (artist != data_[idx.row()].current_.effective_albumartist() || album != data_[idx.row()].current_.effective_albumartist()) { artist.clear(); @@ -1081,23 +1106,26 @@ void EditTagDialog::UpdateCover(const UpdateCoverAction action, const AlbumCover 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].cover_action_ = cover_action; + data_[i].cover_result_ = cover_result; + if (cover_action == UpdateCoverAction::New) { data_[i].current_.clear_art_manual(); + data_[i].current_.set_art_unset(false); } - else if (action == UpdateCoverAction::Unset) { - data_[i].current_.set_manually_unset_cover(); + else if (cover_action == UpdateCoverAction::Unset) { + data_[i].current_.set_art_unset(true); } - else if (action == UpdateCoverAction::Clear || action == UpdateCoverAction::Delete) { + else if (cover_action == UpdateCoverAction::Clear || cover_action == UpdateCoverAction::Delete) { + data_[i].current_.set_art_embedded(false); data_[i].current_.clear_art_manual(); data_[i].current_.clear_art_automatic(); + data_[i].current_.set_art_unset(false); } } } } - UpdateSummaryTab(data_[indexes.first().row()].current_, data_[indexes.first().row()].cover_action_); + UpdateSummaryTab(data_[indexes.first().row()].current_); SelectionChanged(); } @@ -1141,12 +1169,12 @@ void EditTagDialog::SaveData() { QString embedded_cover_from_file; // 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()) { - if (ref.original_.art_manual().isValid() && ref.original_.art_manual().isLocalFile() && QFile::exists(ref.original_.art_manual().toLocalFile())) { + if (ui_->checkbox_embedded_cover->isChecked() && ref.cover_action_ == UpdateCoverAction::None && !ref.original_.art_embedded() && ref.original_.save_embedded_cover_supported()) { + if (ref.original_.art_manual_is_valid()) { ref.cover_action_ = UpdateCoverAction::New; embedded_cover_from_file = ref.original_.art_manual().toLocalFile(); } - else if (ref.original_.art_automatic().isValid() && ref.original_.art_automatic().isLocalFile() && QFile::exists(ref.original_.art_automatic().toLocalFile())) { + else if (ref.original_.art_automatic_is_valid()) { ref.cover_action_ = UpdateCoverAction::New; embedded_cover_from_file = ref.original_.art_automatic().toLocalFile(); } @@ -1175,38 +1203,49 @@ void EditTagDialog::SaveData() { } else { cover_url = album_cover_choice_controller_->SaveCoverToFileAutomatic(&ref.current_, ref.cover_result_); - cover_urls.insert(cover_hash, cover_url); + if (cover_url.isValid()) { + cover_urls.insert(cover_hash, cover_url); + } } } ref.current_.set_art_manual(cover_url); + ref.current_.set_art_unset(false); } break; } case UpdateCoverAction::Unset: - ref.current_.set_manually_unset_cover(); + ref.current_.set_art_embedded(false); + ref.current_.clear_art_manual(); + ref.current_.clear_art_automatic(); + ref.current_.set_art_unset(true); break; case UpdateCoverAction::Clear: + ref.current_.set_art_embedded(false); ref.current_.clear_art_manual(); + ref.current_.clear_art_automatic(); + ref.current_.set_art_unset(false); break; case UpdateCoverAction::Delete:{ + ref.current_.set_art_embedded(false); if (!ref.original_.art_automatic().isEmpty()) { - if (ref.original_.art_automatic().isValid() && !ref.original_.has_embedded_cover() && ref.original_.art_automatic().isLocalFile()) { - QString art_automatic = ref.original_.art_automatic().toLocalFile(); + if (ref.original_.art_automatic_is_valid()) { + const QString art_automatic = ref.original_.art_automatic().toLocalFile(); if (QFile::exists(art_automatic)) { QFile::remove(art_automatic); } } ref.current_.clear_art_automatic(); } - if (!ref.original_.art_manual().isEmpty() && !ref.original_.has_manually_unset_cover()) { - if (ref.original_.art_manual().isValid() && ref.original_.art_manual().isLocalFile()) { - QString art_manual = ref.original_.art_manual().toLocalFile(); + if (!ref.original_.art_manual().isEmpty()) { + if (ref.original_.art_manual_is_valid()) { + const QString art_manual = ref.original_.art_manual().toLocalFile(); if (QFile::exists(art_manual)) { QFile::remove(art_manual); } } ref.current_.clear_art_manual(); } + ref.current_.set_art_unset(false); break; } } @@ -1221,17 +1260,29 @@ void EditTagDialog::SaveData() { if (ref.current_.lastplayed() <= 0) { ref.current_.set_lastplayed(-1); } ++save_tag_pending_; TagReaderClient::SaveCoverOptions savecover_options; - savecover_options.enabled = save_embedded_cover; if (save_embedded_cover && ref.cover_action_ == UpdateCoverAction::New) { if (!ref.cover_result_.image.isNull()) { - savecover_options.is_jpeg = ref.cover_result_.is_jpeg(); - savecover_options.cover_data = ref.cover_result_.image_data; + savecover_options.mime_type = ref.cover_result_.mime_type; } else if (!embedded_cover_from_file.isEmpty()) { savecover_options.cover_filename = embedded_cover_from_file; } + savecover_options.cover_data = ref.cover_result_.image_data; } - TagReaderReply *reply = TagReaderClient::Instance()->SaveFile(ref.current_.url().toLocalFile(), ref.current_, save_tags ? TagReaderClient::SaveTags::On : TagReaderClient::SaveTags::Off, save_playcount ? TagReaderClient::SavePlaycount::On : TagReaderClient::SavePlaycount::Off, save_rating ? TagReaderClient::SaveRating::On : TagReaderClient::SaveRating::Off, savecover_options); + TagReaderClient::SaveTypes save_types; + if (save_tags) { + save_types |= TagReaderClient::SaveType::Tags; + } + if (save_playcount) { + save_types |= TagReaderClient::SaveType::PlayCount; + } + if (save_rating) { + save_types |= TagReaderClient::SaveType::Rating; + } + if (save_embedded_cover) { + save_types |= TagReaderClient::SaveType::Cover; + } + TagReaderReply *reply = TagReaderClient::Instance()->SaveFile(ref.current_.url().toLocalFile(), ref.current_, save_types, savecover_options); QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, ref]() { SongSaveTagsComplete(reply, ref.current_.url().toLocalFile(), ref.current_, ref.cover_action_); }, Qt::QueuedConnection); } // If the cover was changed, but no tags written, make sure to update the collection. @@ -1376,16 +1427,15 @@ void EditTagDialog::SongSaveTagsComplete(TagReaderReply *reply, const QString &f break; case UpdateCoverAction::New: song.clear_art_manual(); - song.set_embedded_cover(); + song.set_art_embedded(true); break; case UpdateCoverAction::Clear: case UpdateCoverAction::Delete: - song.clear_art_automatic(); - song.clear_art_manual(); + song.set_art_embedded(false); break; case UpdateCoverAction::Unset: - song.clear_art_automatic(); - song.set_manually_unset_cover(); + song.set_art_embedded(false); + song.set_art_unset(true); break; } collection_songs_.insert(song.id(), song); diff --git a/src/dialogs/edittagdialog.h b/src/dialogs/edittagdialog.h index 1fb37923..66d4a113 100644 --- a/src/dialogs/edittagdialog.h +++ b/src/dialogs/edittagdialog.h @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2023, 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 @@ -66,10 +66,6 @@ class EditTagDialog : public QDialog { explicit EditTagDialog(Application *app, QWidget *parent = nullptr); ~EditTagDialog() override; - static const char *kSettingsGroup; - static const char *kTagsDifferentHintText; - static const char *kArtDifferentHintText; - void SetSongs(const SongList &songs, const PlaylistItemPtrList &items = PlaylistItemPtrList()); PlaylistItemPtrList playlist_items() const { return playlist_items_; } @@ -85,6 +81,11 @@ class EditTagDialog : public QDialog { void hideEvent(QHideEvent *e) override; private: + static const char kSettingsGroup[]; + static const char kTagsDifferentHintText[]; + static const char kArtDifferentHintText[]; + static const int kSmallImageSize; + enum class UpdateCoverAction { None = 0, Clear, @@ -120,7 +121,7 @@ class EditTagDialog : public QDialog { void FetchTag(); void FetchTagSongChosen(const Song &original_song, const Song &new_metadata); - void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result); + void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &cover_result); void LoadCoverFromFile(); void SaveCoverToFile(); @@ -147,7 +148,7 @@ class EditTagDialog : public QDialog { }; Song *GetFirstSelected(); - void UpdateCover(const UpdateCoverAction action, const AlbumCoverImageResult &result = AlbumCoverImageResult()); + void UpdateCover(const UpdateCoverAction cover_action, const AlbumCoverImageResult &cover_result = AlbumCoverImageResult()); bool DoesValueVary(const QModelIndexList &sel, const QString &id) const; bool IsValueModified(const QModelIndexList &sel, const QString &id) const; @@ -157,10 +158,11 @@ 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, const UpdateCoverAction cover_action); + void UpdateSummaryTab(const Song &song); void UpdateStatisticsTab(const Song &song); - static QString GetArtSummary(const Song &song, const UpdateCoverAction cover_action); + QString GetArtSummary(const Song &song, const AlbumCoverLoaderResult::Type cover_type); + QString GetArtSummary(const UpdateCoverAction cover_action); void UpdateUI(const QModelIndexList &indexes); @@ -194,7 +196,6 @@ class EditTagDialog : public QDialog { bool ignore_edits_; - AlbumCoverLoaderOptions cover_options_; quint64 summary_cover_art_id_; quint64 tags_cover_art_id_; bool cover_art_is_set_; @@ -207,6 +208,8 @@ class EditTagDialog : public QDialog { int save_tag_pending_; QMap collection_songs_; + + AlbumCoverLoaderOptions::Types cover_types_; }; #endif // EDITTAGDIALOG_H diff --git a/src/dialogs/edittagdialog.ui b/src/dialogs/edittagdialog.ui index d5569182..6132e402 100644 --- a/src/dialogs/edittagdialog.ui +++ b/src/dialogs/edittagdialog.ui @@ -48,7 +48,7 @@ - 1 + 0 @@ -162,8 +162,38 @@ QLayout::SetMinimumSize - - + + + + Sample rate + + + true + + + + + + + Art Unset + + + + + + + Art Manual + + + + + + + + 150 + 0 + + true @@ -172,22 +202,17 @@ - - - - Length + + + + QLineEdit { + background: transparent; +} - - true + + false - - - - - - File type - - + true @@ -202,8 +227,28 @@ - - + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + File size + + + true + + + + + true @@ -225,6 +270,26 @@ + + + + Bit rate + + + true + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + @@ -235,8 +300,18 @@ - - + + + + File type + + + true + + + + + true @@ -245,6 +320,16 @@ + + + + Play count + + + true + + + @@ -258,48 +343,8 @@ - - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Skip count - - - true - - - - - - - Date modified - - - true - - - - - - - Play count - - - true - - - - - + + true @@ -325,8 +370,25 @@ - - + + + + Art Automatic + + + + + + + Skip count + + + true + + + + + true @@ -335,18 +397,28 @@ - - + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + - Sample rate + Date modified true - - + + QLineEdit { background: transparent; @@ -360,16 +432,40 @@ - - + + + + QLineEdit { + background: transparent; +} + + + false + + + + + - Last played + Length true + + + + QLineEdit { + background: transparent; +} + + + false + + + @@ -386,82 +482,7 @@ - - - - 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 { @@ -473,8 +494,25 @@ - - + + + + Last played + + + true + + + + + + + Art Embedded + + + + + QLineEdit { background: transparent; diff --git a/src/internet/internetsearchview.cpp b/src/internet/internetsearchview.cpp index dbaa997e..f2f8de03 100644 --- a/src/internet/internetsearchview.cpp +++ b/src/internet/internetsearchview.cpp @@ -139,10 +139,6 @@ InternetSearchView::InternetSearchView(QWidget *parent) ui_->progressbar->hide(); ui_->progressbar->reset(); - cover_loader_options_.desired_height_ = kArtHeight; - cover_loader_options_.pad_output_image_ = true; - cover_loader_options_.scale_output_image_ = true; - } InternetSearchView::~InternetSearchView() { delete ui_; } @@ -851,7 +847,9 @@ void InternetSearchView::LazyLoadAlbumCover(const QModelIndex &proxy_index) { item_album->setData(cached_pixmap, Qt::DecorationRole); } else { - quint64 loader_id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, result.metadata_); + AlbumCoverLoaderOptions cover_loader_options(AlbumCoverLoaderOptions::Option::ScaledImage | AlbumCoverLoaderOptions::Option::PadScaledImage); + cover_loader_options.desired_scaled_size = QSize(kArtHeight, kArtHeight); + quint64 loader_id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options, result.metadata_); cover_loader_tasks_[loader_id] = qMakePair(source_index, result.pixmap_cache_key_); } diff --git a/src/internet/internetsearchview.h b/src/internet/internetsearchview.h index 887fe77c..034dd95a 100644 --- a/src/internet/internetsearchview.h +++ b/src/internet/internetsearchview.h @@ -42,7 +42,6 @@ #include "core/song.h" #include "collection/collectionmodel.h" -#include "covermanager/albumcoverloaderoptions.h" #include "covermanager/albumcoverloaderresult.h" #include "settings/settingsdialog.h" @@ -215,7 +214,6 @@ class InternetSearchView : public QWidget { QMap delayed_searches_; QMap pending_searches_; - AlbumCoverLoaderOptions cover_loader_options_; QMap> cover_loader_tasks_; }; diff --git a/src/organize/organize.cpp b/src/organize/organize.cpp index c47b260e..1557de86 100644 --- a/src/organize/organize.cpp +++ b/src/organize/organize.cpp @@ -222,7 +222,7 @@ void Organize::ProcessSomeFiles() { job.remove_original_ = !copy_; job.playlist_ = playlist_; - if (task.song_info_.song_.art_manual_is_valid() && !task.song_info_.song_.has_manually_unset_cover()) { + if (task.song_info_.song_.art_manual_is_valid() && !task.song_info_.song_.art_unset()) { 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(); } @@ -230,7 +230,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_.has_embedded_cover()) { + else if (task.song_info_.song_.art_automatic_is_valid()) { 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 94ece8bb..15051b29 100644 --- a/src/playlist/playlist.cpp +++ b/src/playlist/playlist.cpp @@ -2319,9 +2319,9 @@ 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.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)) { + if (((result.type == AlbumCoverLoaderResult::Type::Manual && result.album_cover.cover_url.isLocalFile()) || result.type == AlbumCoverLoaderResult::Type::Unset) && (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()))) { + if (item && item->Metadata() == song && (!item->Metadata().art_manual_is_valid() || (result.type == AlbumCoverLoaderResult::Type::Unset && !item->Metadata().art_unset()))) { 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); ScheduleSaveAsync(); diff --git a/src/playlist/playlistview.cpp b/src/playlist/playlistview.cpp index 17b8c29e..95917152 100644 --- a/src/playlist/playlistview.cpp +++ b/src/playlist/playlistview.cpp @@ -1468,12 +1468,7 @@ void PlaylistView::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResu 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()); - } - else { - set_background_image(current_song_cover_art_); - } + set_background_image(result.success && result.type != AlbumCoverLoaderResult::Type::None && result.type != AlbumCoverLoaderResult::Type::Unset ? current_song_cover_art_ : QImage()); force_background_redraw_ = true; update(); } diff --git a/src/playlistparsers/xspfparser.cpp b/src/playlistparsers/xspfparser.cpp index 2ceb5d29..6e3a61d7 100644 --- a/src/playlistparsers/xspfparser.cpp +++ b/src/playlistparsers/xspfparser.cpp @@ -172,13 +172,10 @@ void XSPFParser::Save(const SongList &songs, QIODevice *device, const QDir &dir, writer.writeTextElement("trackNum", QString::number(song.track())); } - QUrl cover_url = song.art_manual().isEmpty() || song.art_manual().path().isEmpty() ? song.art_automatic() : song.art_manual(); + const QUrl cover_url = song.art_manual().isEmpty() || !song.art_manual().isValid() ? song.art_automatic() : song.art_manual(); // Ignore images that are in our resource bundle. - if (!cover_url.isEmpty() && !cover_url.path().isEmpty() && cover_url.path() != Song::kManuallyUnsetCover && cover_url.path() != Song::kEmbeddedCover) { - if (cover_url.scheme().isEmpty()) { - cover_url.setScheme("file"); - } - QString cover_filename = QUrl::toPercentEncoding(URLOrFilename(cover_url, dir, path_type), "/ "); + if (!cover_url.isEmpty() && cover_url.isValid()) { + const QString cover_filename = QUrl::toPercentEncoding(URLOrFilename(cover_url, dir, path_type), "/ "); writer.writeTextElement("image", cover_filename); } } diff --git a/src/radios/radiomodel.cpp b/src/radios/radiomodel.cpp index fbc38673..3f81aba4 100644 --- a/src/radios/radiomodel.cpp +++ b/src/radios/radiomodel.cpp @@ -47,12 +47,6 @@ RadioModel::RadioModel(Application *app, QObject *parent) root_->lazy_loaded = true; - cover_loader_options_.get_image_data_ = false; - cover_loader_options_.get_image_ = true; - cover_loader_options_.scale_output_image_ = true; - cover_loader_options_.pad_output_image_ = true; - cover_loader_options_.desired_height_ = kTreeIconSize; - if (app_) { QObject::connect(app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &RadioModel::AlbumCoverLoaded); } @@ -298,7 +292,7 @@ QPixmap RadioModel::ChannelIcon(const QModelIndex &idx) { if (!songs.isEmpty()) { Song song = songs.first(); song.set_art_automatic(item->channel.thumbnail_url); - const quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, song); + const quint64 id = app_->album_cover_loader()->LoadImageAsync(AlbumCoverLoaderOptions(AlbumCoverLoaderOptions::Option::ScaledImage | AlbumCoverLoaderOptions::Option::PadScaledImage, QSize(kTreeIconSize, kTreeIconSize)), song); pending_art_[id] = ItemAndCacheKey(item, cache_key); pending_cache_keys_.insert(cache_key); } @@ -319,7 +313,7 @@ void RadioModel::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult pending_cache_keys_.remove(cache_key); - if (!result.success || result.image_scaled.isNull() || result.type == AlbumCoverLoaderResult::Type::ManuallyUnset) { + if (!result.success || result.image_scaled.isNull() || result.type == AlbumCoverLoaderResult::Type::Unset) { QPixmapCache::insert(cache_key, ServiceIcon(item)); } else { diff --git a/src/radios/radiomodel.h b/src/radios/radiomodel.h index 6cdb8e97..275eacd6 100644 --- a/src/radios/radiomodel.h +++ b/src/radios/radiomodel.h @@ -33,7 +33,6 @@ #include "core/song.h" #include "core/simpletreemodel.h" -#include "covermanager/albumcoverloaderoptions.h" #include "covermanager/albumcoverloaderresult.h" #include "radioitem.h" #include "radiochannel.h" @@ -91,7 +90,6 @@ class RadioModel : public SimpleTreeModel { using ItemAndCacheKey = QPair; Application *app_; - AlbumCoverLoaderOptions cover_loader_options_; QMap container_nodes_; QList items_; QMap pending_art_; diff --git a/src/settings/collectionsettingspage.cpp b/src/settings/collectionsettingspage.cpp index 0fd2bca8..de48c5f2 100644 --- a/src/settings/collectionsettingspage.cpp +++ b/src/settings/collectionsettingspage.cpp @@ -45,7 +45,6 @@ #include "core/iconloader.h" #include "utilities/strutils.h" #include "utilities/timeutils.h" -#include "utilities/coveroptions.h" #include "collection/collection.h" #include "collection/collectionmodel.h" #include "collection/collectiondirectorymodel.h" @@ -90,10 +89,6 @@ CollectionSettingsPage::CollectionSettingsPage(SettingsDialog *dialog, QWidget * QObject::connect(ui_->song_tracking, &QCheckBox::toggled, this, &CollectionSettingsPage::SongTrackingToggled); #endif - 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); - QObject::connect(ui_->checkbox_disk_cache, &QCheckBox::stateChanged, this, &CollectionSettingsPage::DiskCacheEnable); QObject::connect(ui_->button_clear_disk_cache, &QPushButton::clicked, dialog->app(), &Application::ClearPixmapDiskCache); QObject::connect(ui_->button_clear_disk_cache, &QPushButton::clicked, this, &CollectionSettingsPage::ClearPixmapDiskCache); @@ -189,34 +184,6 @@ void CollectionSettingsPage::Load() { QStringList filters = s.value("cover_art_patterns", QStringList() << "front" << "cover").toStringList(); ui_->cover_art_patterns->setText(filters.join(",")); - const CoverOptions::CoverType save_cover_type = static_cast(s.value("save_cover_type", static_cast(CoverOptions::CoverType::Cache)).toInt()); - switch (save_cover_type) { - case CoverOptions::CoverType::Cache: - ui_->radiobutton_save_albumcover_cache->setChecked(true); - break; - case CoverOptions::CoverType::Album: - ui_->radiobutton_save_albumcover_albumdir->setChecked(true); - break; - case CoverOptions::CoverType::Embedded: - ui_->radiobutton_save_albumcover_embedded->setChecked(true); - break; - } - - const CoverOptions::CoverFilename save_cover_filename = static_cast(s.value("save_cover_filename", static_cast(CoverOptions::CoverFilename::Pattern)).toInt()); - switch (save_cover_filename) { - case CoverOptions::CoverFilename::Hash: - ui_->radiobutton_cover_hash->setChecked(true); - break; - case CoverOptions::CoverFilename::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); - ui_->checkbox_cover_overwrite->setChecked(s.value("cover_overwrite", false).toBool()); - ui_->checkbox_cover_lowercase->setChecked(s.value("cover_lowercase", true).toBool()); - ui_->checkbox_cover_replace_spaces->setChecked(s.value("cover_replace_spaces", true).toBool()); - ui_->spinbox_cache_size->setValue(s.value(kSettingsCacheSize, kSettingsCacheSizeDefault).toInt()); ui_->combobox_cache_size->setCurrentIndex(ui_->combobox_cache_size->findData(s.value(kSettingsCacheSizeUnit, static_cast(CacheSizeUnit::MB)).toInt())); ui_->checkbox_disk_cache->setChecked(s.value(kSettingsDiskCacheEnable, false).toBool()); @@ -270,22 +237,6 @@ void CollectionSettingsPage::Save() { s.setValue("cover_art_patterns", filters); - CoverOptions::CoverType save_cover_type = CoverOptions::CoverType::Cache; - if (ui_->radiobutton_save_albumcover_cache->isChecked()) save_cover_type = CoverOptions::CoverType::Cache; - else if (ui_->radiobutton_save_albumcover_albumdir->isChecked()) save_cover_type = CoverOptions::CoverType::Album; - else if (ui_->radiobutton_save_albumcover_embedded->isChecked()) save_cover_type = CoverOptions::CoverType::Embedded; - s.setValue("save_cover_type", static_cast(save_cover_type)); - - CoverOptions::CoverFilename save_cover_filename = CoverOptions::CoverFilename::Hash; - if (ui_->radiobutton_cover_hash->isChecked()) save_cover_filename = CoverOptions::CoverFilename::Hash; - else if (ui_->radiobutton_cover_pattern->isChecked()) save_cover_filename = CoverOptions::CoverFilename::Pattern; - s.setValue("save_cover_filename", static_cast(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()); - s.setValue("cover_replace_spaces", ui_->checkbox_cover_replace_spaces->isChecked()); - s.setValue(kSettingsCacheSize, ui_->spinbox_cache_size->value()); s.setValue(kSettingsCacheSizeUnit, ui_->combobox_cache_size->currentData().toInt()); s.setValue(kSettingsDiskCacheEnable, ui_->checkbox_disk_cache->isChecked()); @@ -303,33 +254,6 @@ void CollectionSettingsPage::Save() { } -void CollectionSettingsPage::CoverSaveInAlbumDirChanged() { - - if (ui_->radiobutton_save_albumcover_albumdir->isChecked()) { - if (!ui_->groupbox_cover_filename->isEnabled()) { - ui_->groupbox_cover_filename->setEnabled(true); - } - if (ui_->radiobutton_cover_pattern->isChecked()) { - if (!ui_->lineedit_cover_pattern->isEnabled()) ui_->lineedit_cover_pattern->setEnabled(true); - if (!ui_->checkbox_cover_overwrite->isEnabled()) ui_->checkbox_cover_overwrite->setEnabled(true); - if (!ui_->checkbox_cover_lowercase->isEnabled()) ui_->checkbox_cover_lowercase->setEnabled(true); - if (!ui_->checkbox_cover_replace_spaces->isEnabled()) ui_->checkbox_cover_replace_spaces->setEnabled(true); - } - else { - if (ui_->lineedit_cover_pattern->isEnabled()) ui_->lineedit_cover_pattern->setEnabled(false); - if (ui_->checkbox_cover_overwrite->isEnabled()) ui_->checkbox_cover_overwrite->setEnabled(false); - if (ui_->checkbox_cover_lowercase->isEnabled()) ui_->checkbox_cover_lowercase->setEnabled(false); - if (ui_->checkbox_cover_replace_spaces->isEnabled()) ui_->checkbox_cover_replace_spaces->setEnabled(false); - } - } - else { - if (ui_->groupbox_cover_filename->isEnabled()) { - ui_->groupbox_cover_filename->setEnabled(false); - } - } - -} - void CollectionSettingsPage::ClearPixmapDiskCache() { ui_->disk_cache_in_use->setText("empty"); diff --git a/src/settings/collectionsettingspage.h b/src/settings/collectionsettingspage.h index 9960a638..7ce6178d 100644 --- a/src/settings/collectionsettingspage.h +++ b/src/settings/collectionsettingspage.h @@ -67,7 +67,6 @@ class CollectionSettingsPage : public SettingsPage { void CurrentRowChanged(const QModelIndex &idx); void SongTrackingToggled(); void DiskCacheEnable(const int state); - void CoverSaveInAlbumDirChanged(); void ClearPixmapDiskCache(); void CacheSizeUnitChanged(int index); void DiskCacheSizeUnitChanged(int index); diff --git a/src/settings/collectionsettingspage.ui b/src/settings/collectionsettingspage.ui index 2606f33d..cb4e5366 100644 --- a/src/settings/collectionsettingspage.ui +++ b/src/settings/collectionsettingspage.ui @@ -7,7 +7,7 @@ 0 0 519 - 1546 + 920 @@ -221,130 +221,6 @@ If there are no matches then it will use the largest image in the directory. - - - - Saving album covers - - - - 0 - - - - - - - - - - - Save album covers in album directory - - - - - - - Save album covers in cache directory - - - - - - - Save album covers as embedded cover - - - - - - - - - - Filename: - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Pattern - - - - - - - Random - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - %albumartist-%album - - - - - - - Overwrite existing file - - - - - - - Lowercase filename - - - - - - - Replace spaces with dashes - - - - - - - - - @@ -623,15 +499,6 @@ If there are no matches then it will use the largest image in the directory.auto_open pretty_covers show_dividers - radiobutton_save_albumcover_albumdir - radiobutton_save_albumcover_cache - radiobutton_save_albumcover_embedded - radiobutton_cover_pattern - radiobutton_cover_hash - lineedit_cover_pattern - checkbox_cover_overwrite - checkbox_cover_lowercase - checkbox_cover_replace_spaces spinbox_cache_size combobox_cache_size checkbox_disk_cache diff --git a/src/settings/coverssettingspage.cpp b/src/settings/coverssettingspage.cpp index 57d108d7..4afc9111 100644 --- a/src/settings/coverssettingspage.cpp +++ b/src/settings/coverssettingspage.cpp @@ -39,29 +39,49 @@ #include "ui_coverssettingspage.h" #include "core/application.h" #include "core/iconloader.h" +#include "utilities/coveroptions.h" #include "covermanager/coverproviders.h" #include "covermanager/coverprovider.h" #include "widgets/loginstatewidget.h" const char *CoversSettingsPage::kSettingsGroup = "Covers"; +const char *CoversSettingsPage::kProviders = "providers"; +const char *CoversSettingsPage::kTypes = "types"; +const char *CoversSettingsPage::kSaveType = "save_type"; +const char *CoversSettingsPage::kSaveFilename = "save_filename"; +const char *CoversSettingsPage::kSavePattern = "save_pattern"; +const char *CoversSettingsPage::kSaveOverwrite = "save_overwrite"; +const char *CoversSettingsPage::kSaveLowercase = "save_lowercase"; +const char *CoversSettingsPage::kSaveReplaceSpaces = "save_replace_spaces"; CoversSettingsPage::CoversSettingsPage(SettingsDialog *dialog, QWidget *parent) : SettingsPage(dialog, parent), ui_(new Ui::CoversSettingsPage), - provider_selected_(false) { + provider_selected_(false), + types_selected_(false) { ui_->setupUi(this); setWindowIcon(IconLoader::Load("cdcase", true, 0, 32)); QObject::connect(ui_->providers_up, &QPushButton::clicked, this, &CoversSettingsPage::ProvidersMoveUp); QObject::connect(ui_->providers_down, &QPushButton::clicked, this, &CoversSettingsPage::ProvidersMoveDown); - QObject::connect(ui_->providers, &QListWidget::currentItemChanged, this, &CoversSettingsPage::CurrentItemChanged); - QObject::connect(ui_->providers, &QListWidget::itemSelectionChanged, this, &CoversSettingsPage::ItemSelectionChanged); - QObject::connect(ui_->providers, &QListWidget::itemChanged, this, &CoversSettingsPage::ItemChanged); + QObject::connect(ui_->providers, &QListWidget::currentItemChanged, this, &CoversSettingsPage::ProvidersCurrentItemChanged); + QObject::connect(ui_->providers, &QListWidget::itemSelectionChanged, this, &CoversSettingsPage::ProvidersItemSelectionChanged); + QObject::connect(ui_->providers, &QListWidget::itemChanged, this, &CoversSettingsPage::ProvidersItemChanged); QObject::connect(ui_->button_authenticate, &QPushButton::clicked, this, &CoversSettingsPage::AuthenticateClicked); QObject::connect(ui_->login_state, &LoginStateWidget::LogoutClicked, this, &CoversSettingsPage::LogoutClicked); + QObject::connect(ui_->types_up, &QPushButton::clicked, this, &CoversSettingsPage::TypesMoveUp); + QObject::connect(ui_->types_down, &QPushButton::clicked, this, &CoversSettingsPage::TypesMoveDown); + QObject::connect(ui_->types, &QListWidget::currentItemChanged, this, &CoversSettingsPage::TypesCurrentItemChanged); + QObject::connect(ui_->types, &QListWidget::itemSelectionChanged, this, &CoversSettingsPage::TypesItemSelectionChanged); + QObject::connect(ui_->types, &QListWidget::itemChanged, this, &CoversSettingsPage::TypesItemChanged); + + QObject::connect(ui_->radiobutton_save_albumcover_albumdir, &QRadioButton::toggled, this, &CoversSettingsPage::CoverSaveInAlbumDirChanged); + QObject::connect(ui_->radiobutton_cover_hash, &QRadioButton::toggled, this, &CoversSettingsPage::CoverSaveInAlbumDirChanged); + QObject::connect(ui_->radiobutton_cover_pattern, &QRadioButton::toggled, this, &CoversSettingsPage::CoverSaveInAlbumDirChanged); + ui_->login_state->AddCredentialGroup(ui_->widget_authenticate); NoProviderSelected(); @@ -87,6 +107,57 @@ void CoversSettingsPage::Load() { item->setForeground(provider->is_enabled() ? palette().color(QPalette::Active, QPalette::Text) : palette().color(QPalette::Disabled, QPalette::Text)); } + QSettings s; + s.beginGroup(kSettingsGroup); + + const QStringList all_types = QStringList() << "art_unset" + << "art_manual" + << "art_automatic" + << "art_embedded"; + + const QStringList types = s.value(kTypes, all_types).toStringList(); + + ui_->types->clear(); + for (const QString &type : types) { + AddAlbumCoverArtType(type, AlbumCoverArtTypeDescription(type), true); + } + + for (const QString &type : all_types) { + if (!types.contains(type)) { + AddAlbumCoverArtType(type, AlbumCoverArtTypeDescription(type), false); + } + } + + const CoverOptions::CoverType save_cover_type = static_cast(s.value(kSaveType, static_cast(CoverOptions::CoverType::Cache)).toInt()); + switch (save_cover_type) { + case CoverOptions::CoverType::Cache: + ui_->radiobutton_save_albumcover_cache->setChecked(true); + break; + case CoverOptions::CoverType::Album: + ui_->radiobutton_save_albumcover_albumdir->setChecked(true); + break; + case CoverOptions::CoverType::Embedded: + ui_->radiobutton_save_albumcover_embedded->setChecked(true); + break; + } + + const CoverOptions::CoverFilename save_cover_filename = static_cast(s.value(kSaveFilename, static_cast(CoverOptions::CoverFilename::Pattern)).toInt()); + switch (save_cover_filename) { + case CoverOptions::CoverFilename::Hash: + ui_->radiobutton_cover_hash->setChecked(true); + break; + case CoverOptions::CoverFilename::Pattern: + ui_->radiobutton_cover_pattern->setChecked(true); + break; + } + QString cover_pattern = s.value(kSavePattern).toString(); + if (!cover_pattern.isEmpty()) ui_->lineedit_cover_pattern->setText(cover_pattern); + ui_->checkbox_cover_overwrite->setChecked(s.value(kSaveOverwrite, false).toBool()); + ui_->checkbox_cover_lowercase->setChecked(s.value(kSaveLowercase, true).toBool()); + ui_->checkbox_cover_replace_spaces->setChecked(s.value(kSaveReplaceSpaces, true).toBool()); + + s.endGroup(); + Init(ui_->layout_coverssettingspage->parentWidget()); if (!QSettings().childGroups().contains(kSettingsGroup)) set_changed(); @@ -95,20 +166,47 @@ void CoversSettingsPage::Load() { void CoversSettingsPage::Save() { + QSettings s; + s.beginGroup(kSettingsGroup); + QStringList providers; for (int i = 0; i < ui_->providers->count(); ++i) { const QListWidgetItem *item = ui_->providers->item(i); - if (item->checkState() == Qt::Checked) providers << item->text(); // clazy:exclude=reserve-candidates + if (item->checkState() == Qt::Checked) providers << item->text(); + } + s.setValue(kProviders, providers); + + QStringList types; + for (int i = 0; i < ui_->types->count(); ++i) { + const QListWidgetItem *item = ui_->types->item(i); + if (item->checkState() == Qt::Checked) { + types << item->data(Type_Role_Name).toString(); + } } - QSettings s; - s.beginGroup(kSettingsGroup); - s.setValue("providers", providers); + s.setValue(kTypes, types); + + CoverOptions::CoverType save_cover_type = CoverOptions::CoverType::Cache; + if (ui_->radiobutton_save_albumcover_cache->isChecked()) save_cover_type = CoverOptions::CoverType::Cache; + else if (ui_->radiobutton_save_albumcover_albumdir->isChecked()) save_cover_type = CoverOptions::CoverType::Album; + else if (ui_->radiobutton_save_albumcover_embedded->isChecked()) save_cover_type = CoverOptions::CoverType::Embedded; + s.setValue(kSaveType, static_cast(save_cover_type)); + + CoverOptions::CoverFilename save_cover_filename = CoverOptions::CoverFilename::Hash; + if (ui_->radiobutton_cover_hash->isChecked()) save_cover_filename = CoverOptions::CoverFilename::Hash; + else if (ui_->radiobutton_cover_pattern->isChecked()) save_cover_filename = CoverOptions::CoverFilename::Pattern; + s.setValue(kSaveFilename, static_cast(save_cover_filename)); + + s.setValue(kSavePattern, ui_->lineedit_cover_pattern->text()); + s.setValue(kSaveOverwrite, ui_->checkbox_cover_overwrite->isChecked()); + s.setValue(kSaveLowercase, ui_->checkbox_cover_lowercase->isChecked()); + s.setValue(kSaveReplaceSpaces, ui_->checkbox_cover_replace_spaces->isChecked()); + s.endGroup(); } -void CoversSettingsPage::CurrentItemChanged(QListWidgetItem *item_current, QListWidgetItem *item_previous) { +void CoversSettingsPage::ProvidersCurrentItemChanged(QListWidgetItem *item_current, QListWidgetItem *item_previous) { if (item_previous) { CoverProvider *provider = dialog()->app()->cover_providers()->ProviderByName(item_previous->text()); @@ -155,7 +253,7 @@ void CoversSettingsPage::CurrentItemChanged(QListWidgetItem *item_current, QList } -void CoversSettingsPage::ItemSelectionChanged() { +void CoversSettingsPage::ProvidersItemSelectionChanged() { if (ui_->providers->selectedItems().count() == 0) { DisableAuthentication(); @@ -166,7 +264,7 @@ void CoversSettingsPage::ItemSelectionChanged() { } else { if (ui_->providers->currentItem() && !provider_selected_) { - CurrentItemChanged(ui_->providers->currentItem(), nullptr); + ProvidersCurrentItemChanged(ui_->providers->currentItem(), nullptr); } } @@ -187,7 +285,7 @@ void CoversSettingsPage::ProvidersMove(const int d) { } -void CoversSettingsPage::ItemChanged(QListWidgetItem *item) { +void CoversSettingsPage::ProvidersItemChanged(QListWidgetItem *item) { item->setForeground((item->checkState() == Qt::Checked) ? palette().color(QPalette::Active, QPalette::Text) : palette().color(QPalette::Disabled, QPalette::Text)); @@ -281,3 +379,114 @@ void CoversSettingsPage::AuthenticationFailure(const QStringList &errors) { bool CoversSettingsPage::ProviderCompareOrder(CoverProvider *a, CoverProvider *b) { return a->order() < b->order(); } + +void CoversSettingsPage::CoverSaveInAlbumDirChanged() { + + if (ui_->radiobutton_save_albumcover_albumdir->isChecked()) { + if (!ui_->groupbox_cover_filename->isEnabled()) { + ui_->groupbox_cover_filename->setEnabled(true); + } + if (ui_->radiobutton_cover_pattern->isChecked()) { + if (!ui_->lineedit_cover_pattern->isEnabled()) ui_->lineedit_cover_pattern->setEnabled(true); + if (!ui_->checkbox_cover_overwrite->isEnabled()) ui_->checkbox_cover_overwrite->setEnabled(true); + if (!ui_->checkbox_cover_lowercase->isEnabled()) ui_->checkbox_cover_lowercase->setEnabled(true); + if (!ui_->checkbox_cover_replace_spaces->isEnabled()) ui_->checkbox_cover_replace_spaces->setEnabled(true); + } + else { + if (ui_->lineedit_cover_pattern->isEnabled()) ui_->lineedit_cover_pattern->setEnabled(false); + if (ui_->checkbox_cover_overwrite->isEnabled()) ui_->checkbox_cover_overwrite->setEnabled(false); + if (ui_->checkbox_cover_lowercase->isEnabled()) ui_->checkbox_cover_lowercase->setEnabled(false); + if (ui_->checkbox_cover_replace_spaces->isEnabled()) ui_->checkbox_cover_replace_spaces->setEnabled(false); + } + } + else { + if (ui_->groupbox_cover_filename->isEnabled()) { + ui_->groupbox_cover_filename->setEnabled(false); + } + } + +} + +void CoversSettingsPage::AddAlbumCoverArtType(const QString &name, const QString &description, const bool enabled) { + + QListWidgetItem *item = new QListWidgetItem; + item->setData(Type_Role_Name, name); + item->setText(description); + item->setCheckState(enabled ? Qt::Checked : Qt::Unchecked); + ui_->types->addItem(item); + +} + +QString CoversSettingsPage::AlbumCoverArtTypeDescription(const QString &type) const { + + if (type == "art_unset") { + return tr("Manually unset (%1)").arg(type); + } + if (type == "art_manual") { + return tr("Set through album cover search (%1)").arg(type); + } + if (type == "art_automatic") { + return tr("Automatically picked up from album directory (%1)").arg(type); + } + if (type == "art_embedded") { + return tr("Embedded album cover art (%1)").arg(type); + } + + return QString(); + +} + +void CoversSettingsPage::TypesMoveUp() { TypesMove(-1); } + +void CoversSettingsPage::TypesMoveDown() { TypesMove(+1); } + +void CoversSettingsPage::TypesMove(const int d) { + + const int row = ui_->types->currentRow(); + QListWidgetItem *item = ui_->types->takeItem(row); + ui_->types->insertItem(row + d, item); + ui_->types->setCurrentRow(row + d); + + set_changed(); + +} + +void CoversSettingsPage::TypesItemChanged(QListWidgetItem *item) { + + item->setForeground((item->checkState() == Qt::Checked) ? palette().color(QPalette::Active, QPalette::Text) : palette().color(QPalette::Disabled, QPalette::Text)); + + set_changed(); + +} + +void CoversSettingsPage::TypesCurrentItemChanged(QListWidgetItem *item_current, QListWidgetItem *item_previous) { + + Q_UNUSED(item_previous) + + if (item_current) { + const int row = ui_->types->row(item_current); + ui_->types_up->setEnabled(row != 0); + ui_->types_down->setEnabled(row != ui_->types->count() - 1); + types_selected_ = true; + } + else { + ui_->types_up->setEnabled(false); + ui_->types_down->setEnabled(false); + types_selected_ = false; + } + +} + +void CoversSettingsPage::TypesItemSelectionChanged() { + + if (ui_->types->selectedItems().count() == 0) { + ui_->types_up->setEnabled(false); + ui_->types_down->setEnabled(false); + } + else { + if (ui_->providers->currentItem() && !types_selected_) { + TypesCurrentItemChanged(ui_->types->currentItem(), nullptr); + } + } + +} diff --git a/src/settings/coverssettingspage.h b/src/settings/coverssettingspage.h index adec3e67..ff22a283 100644 --- a/src/settings/coverssettingspage.h +++ b/src/settings/coverssettingspage.h @@ -42,6 +42,14 @@ class CoversSettingsPage : public SettingsPage { ~CoversSettingsPage() override; static const char *kSettingsGroup; + static const char *kProviders; + static const char *kTypes; + static const char *kSaveType; + static const char *kSaveFilename; + static const char *kSavePattern; + static const char *kSaveOverwrite; + static const char *kSaveLowercase; + static const char *kSaveReplaceSpaces; void Load() override; void Save() override; @@ -52,21 +60,35 @@ class CoversSettingsPage : public SettingsPage { void DisableAuthentication(); void DisconnectAuthentication(CoverProvider *provider) const; static bool ProviderCompareOrder(CoverProvider *a, CoverProvider *b); + void AddAlbumCoverArtType(const QString &name, const QString &description, const bool enabled); + QString AlbumCoverArtTypeDescription(const QString &type) const; + void TypesMove(const int d); private slots: - void CurrentItemChanged(QListWidgetItem *item_current, QListWidgetItem *item_previous); - void ItemSelectionChanged(); - void ItemChanged(QListWidgetItem *item); + void ProvidersCurrentItemChanged(QListWidgetItem *item_current, QListWidgetItem *item_previous); + void ProvidersItemSelectionChanged(); + void ProvidersItemChanged(QListWidgetItem *item); void ProvidersMoveUp(); void ProvidersMoveDown(); void AuthenticateClicked(); void LogoutClicked(); void AuthenticationSuccess(); void AuthenticationFailure(const QStringList &errors); + void CoverSaveInAlbumDirChanged(); + void TypesCurrentItemChanged(QListWidgetItem *item_current, QListWidgetItem *item_previous); + void TypesItemSelectionChanged(); + void TypesItemChanged(QListWidgetItem *item); + void TypesMoveUp(); + void TypesMoveDown(); private: + enum Type_Role { + Type_Role_Name = Qt::UserRole + 1 + }; + Ui_CoversSettingsPage *ui_; bool provider_selected_; + bool types_selected_; }; #endif // COVERSSETTINGSPAGE_H diff --git a/src/settings/coverssettingspage.ui b/src/settings/coverssettingspage.ui index 1b156854..50f0fc3c 100644 --- a/src/settings/coverssettingspage.ui +++ b/src/settings/coverssettingspage.ui @@ -7,7 +7,7 @@ 0 0 460 - 600 + 897 @@ -141,6 +141,180 @@ + + + + Album cover types + + + + + + + 0 + 0 + + + + + + + + + + Move up + + + + + + + Move down + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + Saving album covers + + + + 0 + + + + + + + + + + + Save album covers in album directory + + + + + + + Save album covers in cache directory + + + + + + + Save album covers as embedded cover + + + + + + + + + + Filename: + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Pattern + + + + + + + Random + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + %albumartist-%album + + + + + + + Overwrite existing file + + + + + + + Lowercase filename + + + + + + + Replace spaces with dashes + + + + + + + + + diff --git a/src/utilities/imageutils.cpp b/src/utilities/imageutils.cpp index de0f2e6b..8c555a6b 100644 --- a/src/utilities/imageutils.cpp +++ b/src/utilities/imageutils.cpp @@ -18,14 +18,11 @@ */ #include -#include #include #include -#include #include #include #include -#include #include #include @@ -61,35 +58,6 @@ QStringList ImageUtils::SupportedImageFormats() { } -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(); @@ -124,28 +92,24 @@ QByteArray ImageUtils::FileToJpegData(const QString &filename) { } -QImage ImageUtils::ScaleAndPad(const QImage &image, const bool scale, const bool pad, const int desired_height, const qreal device_pixel_ratio) { +QImage ImageUtils::ScaleImage(const QImage &image, const QSize desired_size, const qreal device_pixel_ratio, const bool pad) { - if (image.isNull()) return image; - - const int scaled_height = desired_height * device_pixel_ratio; - - // Scale the image down - QImage image_scaled; - if (scale) { - image_scaled = image.scaled(QSize(scaled_height, scaled_height), Qt::KeepAspectRatio, Qt::SmoothTransformation); - } - else { - image_scaled = image; + if (image.isNull() || (image.width() == desired_size.width() && image.height() == desired_size.height())) { + return image; } - // Pad the image to height x height - if (pad) { - QImage image_padded(scaled_height, scaled_height, QImage::Format_ARGB32); + QSize scale_size(desired_size.width() * device_pixel_ratio, desired_size.height() * device_pixel_ratio); + + // Scale the image + QImage image_scaled = image.scaled(scale_size, Qt::KeepAspectRatio, Qt::SmoothTransformation); + + // Pad the image + if (pad && image_scaled.width() != image_scaled.height()) { + QImage image_padded(scale_size, QImage::Format_ARGB32); image_padded.fill(0); QPainter p(&image_padded); - p.drawImage((scaled_height - image_scaled.width()) / 2, (scaled_height - image_scaled.height()) / 2, image_scaled); + p.drawImage((image_padded.width() - image_scaled.width()) / 2, (image_padded.height() - image_scaled.height()) / 2, image_scaled); p.end(); image_scaled = image_padded; @@ -157,44 +121,23 @@ QImage ImageUtils::ScaleAndPad(const QImage &image, const bool scale, const bool } -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 ImageUtils::GenerateNoCoverImage(const QSize size, const qreal device_pixel_ratio) { QImage image(":/pictures/cdcase.png"); + QSize scale_size(size.width() * device_pixel_ratio, size.height() * device_pixel_ratio); // Get a square version of the nocover image with some transparency: - QImage image_scaled = image.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); + QImage image_scaled = image.scaled(scale_size, Qt::KeepAspectRatio, Qt::SmoothTransformation); - QImage image_square(size, QImage::Format_ARGB32); + QImage image_square(scale_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.drawImage((image_square.width() - image_scaled.width()) / 2, (image_square.height() - image_scaled.height()) / 2, image_scaled); p.end(); + image_square.setDevicePixelRatio(device_pixel_ratio); + return image_square; } diff --git a/src/utilities/imageutils.h b/src/utilities/imageutils.h index 17cbbfbe..522f4d93 100644 --- a/src/utilities/imageutils.h +++ b/src/utilities/imageutils.h @@ -39,11 +39,8 @@ class ImageUtils { static QStringList SupportedImageFormats(); static QByteArray SaveImageToJpegData(const QImage &image = QImage()); static QByteArray FileToJpegData(const QString &filename); - 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, const qreal device_pixel_ratio = 1.0F); - static QImage CreateThumbnail(const QImage &image, const bool pad, const QSize size); - static QImage GenerateNoCoverImage(const QSize size = QSize()); - + static QImage ScaleImage(const QImage &image, const QSize desired_size, const qreal device_pixel_ratio = 1.0F, const bool pad = true); + static QImage GenerateNoCoverImage(const QSize size, const qreal device_pixel_ratio); }; #endif // IMAGEUTILS_H diff --git a/src/widgets/playingwidget.cpp b/src/widgets/playingwidget.cpp index 6f622d14..68e9def9 100644 --- a/src/widgets/playingwidget.cpp +++ b/src/widgets/playingwidget.cpp @@ -74,6 +74,7 @@ PlayingWidget::PlayingWidget(QWidget *parent) active_(false), small_ideal_height_(0), total_height_(0), + desired_height_(0), fit_width_(false), timeline_show_hide_(new QTimeLine(500, this)), timeline_fade_(new QTimeLine(1000, this)), @@ -203,7 +204,7 @@ void PlayingWidget::set_ideal_height(const int height) { } QSize PlayingWidget::sizeHint() const { - return QSize(cover_loader_options_.desired_height_, total_height_); + return QSize(desired_height_, total_height_); } void PlayingWidget::CreateModeAction(const Mode mode, const QString &text, QActionGroup *group) { @@ -340,7 +341,7 @@ void PlayingWidget::SetImage(const QImage &image) { void PlayingWidget::ScaleCover() { - QImage image = ImageUtils::ScaleAndPad(image_original_, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_, devicePixelRatioF()); + QImage image = ImageUtils::ScaleImage(image_original_, QSize(desired_height_, desired_height_), devicePixelRatioF(), true); if (image.isNull()) pixmap_cover_ = QPixmap(); else pixmap_cover_ = QPixmap::fromImage(image); update(); @@ -370,13 +371,13 @@ void PlayingWidget::UpdateHeight() { switch (mode_) { case Mode::SmallSongDetails: - cover_loader_options_.desired_height_ = small_ideal_height_; + desired_height_ = small_ideal_height_; total_height_ = small_ideal_height_; break; case Mode::LargeSongDetails: - if (fit_width_) cover_loader_options_.desired_height_ = width(); - else cover_loader_options_.desired_height_ = qMin(kMaxCoverSize, width()); - total_height_ = kTopBorder + cover_loader_options_.desired_height_ + kBottomOffset + static_cast(details_->size().height()); + if (fit_width_) desired_height_ = width(); + else desired_height_ = qMin(kMaxCoverSize, width()); + total_height_ = kTopBorder + desired_height_ + kBottomOffset + static_cast(details_->size().height()); break; } @@ -406,7 +407,7 @@ void PlayingWidget::UpdateDetailsText() { html += "

"; break; case Mode::LargeSongDetails: - details_->setTextWidth(cover_loader_options_.desired_height_); + details_->setTextWidth(desired_height_); html += "

"; break; } @@ -461,7 +462,7 @@ void PlayingWidget::DrawContents(QPainter *p) { // Work out how high the text is going to be const int text_height = static_cast(details_->size().height()); const int cover_size = fit_width_ ? width() : qMin(kMaxCoverSize, width()); - const int x_offset = (width() - cover_loader_options_.desired_height_) / 2; + const int x_offset = (width() - desired_height_) / 2; // Draw the cover p->drawPixmap(x_offset, kTopBorder, cover_size, cover_size, pixmap_cover_); diff --git a/src/widgets/playingwidget.h b/src/widgets/playingwidget.h index 8d020192..90400351 100644 --- a/src/widgets/playingwidget.h +++ b/src/widgets/playingwidget.h @@ -35,7 +35,6 @@ #include #include "core/song.h" -#include "covermanager/albumcoverloaderoptions.h" class QTimeLine; class QTextDocument; @@ -123,8 +122,8 @@ class PlayingWidget : public QWidget { bool playing_; bool active_; int small_ideal_height_; - AlbumCoverLoaderOptions cover_loader_options_; int total_height_; + int desired_height_; bool fit_width_; QTimeLine *timeline_show_hide_; QTimeLine *timeline_fade_;