diff --git a/src/libs/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt
index 87d95d63..29b07d8b 100644
--- a/src/libs/database/CMakeLists.txt
+++ b/src/libs/database/CMakeLists.txt
@@ -1,6 +1,7 @@
add_library(lmsdatabase STATIC
impl/Artist.cpp
impl/ArtistInfo.cpp
+ impl/Artwork.cpp
impl/AuthToken.cpp
impl/Cluster.cpp
impl/Db.cpp
diff --git a/src/libs/database/impl/Artwork.cpp b/src/libs/database/impl/Artwork.cpp
new file mode 100644
index 00000000..d8d03573
--- /dev/null
+++ b/src/libs/database/impl/Artwork.cpp
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2025 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS 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.
+ *
+ * LMS 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 LMS. If not, see .
+ */
+
+#include "database/Artwork.hpp"
+
+#include "database/Image.hpp"
+#include "database/Session.hpp"
+#include "database/TrackEmbeddedImage.hpp"
+
+#include "Utils.hpp"
+#include "traits/IdTypeTraits.hpp"
+
+namespace lms::db
+{
+ Artwork::Artwork(ObjectPtr trackEmbeddedImage)
+ : _trackEmbeddedImage{ getDboPtr(trackEmbeddedImage) }
+ {
+ }
+
+ Artwork::Artwork(ObjectPtr image)
+ : _image{ getDboPtr(image) }
+ {
+ }
+
+ Artwork::pointer Artwork::create(Session& session, ObjectPtr trackEmbeddedImage)
+ {
+ session.checkWriteTransaction();
+ return session.getDboSession()->add(std::unique_ptr{ new Artwork{ trackEmbeddedImage } });
+ }
+
+ Artwork::pointer Artwork::create(Session& session, ObjectPtr image)
+ {
+ session.checkWriteTransaction();
+ return session.getDboSession()->add(std::unique_ptr{ new Artwork{ image } });
+ }
+
+ std::size_t Artwork::getCount(Session& session)
+ {
+ session.checkReadTransaction();
+ return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM artwork"));
+ }
+
+ Artwork::pointer Artwork::find(Session& session, ArtworkId id)
+ {
+ session.checkReadTransaction();
+ return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT a FROM artwork a").where("a.id = ?").bind(id));
+ }
+
+ Artwork::pointer Artwork::find(Session& session, TrackEmbeddedImageId id)
+ {
+ session.checkReadTransaction();
+ return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT a FROM artwork a JOIN track_embedded_image t_e_i ON a.track_embedded_image_id = t_e_i.id").where("t_e_i.id = ?").bind(id));
+ }
+
+ Artwork::pointer Artwork::find(Session& session, ImageId id)
+ {
+ session.checkReadTransaction();
+ return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT a FROM artwork a JOIN image i ON a.image_id = i.id").where("i.id = ?").bind(id));
+ }
+
+} // namespace lms::db
diff --git a/src/libs/database/impl/Image.cpp b/src/libs/database/impl/Image.cpp
index 1b010d09..e0d43c4d 100644
--- a/src/libs/database/impl/Image.cpp
+++ b/src/libs/database/impl/Image.cpp
@@ -23,7 +23,6 @@
#include "database/Artist.hpp"
#include "database/Directory.hpp"
-#include "database/Release.hpp"
#include "database/Session.hpp"
#include "Utils.hpp"
diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp
index f6ba2b4c..e46d1ae1 100644
--- a/src/libs/database/impl/Migration.cpp
+++ b/src/libs/database/impl/Migration.cpp
@@ -35,7 +35,7 @@ namespace lms::db
{
namespace
{
- static constexpr Version LMS_DATABASE_VERSION{ 92 };
+ static constexpr Version LMS_DATABASE_VERSION{ 93 };
}
VersionInfo::VersionInfo()
@@ -411,7 +411,7 @@ SELECT
copyright_url,
track_replay_gain,
release_replay_gain,
- COALESCE(artist_display_name, ""),
+ artist_display_name,
release_id,
1
FROM track)");
@@ -1219,6 +1219,58 @@ FROM tracklist)");
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_embedded_image_link DROP COLUMN is_preferred");
}
+ void migrateFromV92(Session& session)
+ {
+ // Create the new artwork table
+ utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "artwork" (
+ "id" integer primary key autoincrement,
+ "version" integer not null,
+ "track_embedded_image_id" bigint,
+ "image_id" bigint,
+ constraint "fk_artwork_track_embedded_image" foreign key ("track_embedded_image_id") references "track_embedded_image" ("id") on delete cascade deferrable initially deferred,
+ constraint "fk_artwork_image" foreign key ("image_id") references "image" ("id") on delete cascade deferrable initially deferred))");
+
+ // Replaced image by artwork for release
+ // Create the new table, copy the data, drop the old table, rename the new one
+ utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "release_backup" (
+ "id" integer primary key autoincrement,
+ "version" integer not null,
+ "name" text not null,
+ "sort_name" text not null,
+ "mbid" text not null,
+ "group_mbid" text not null,
+ "total_disc" integer,
+ "artist_display_name" text not null,
+ "is_compilation" boolean not null,
+ "barcode" text not null,
+ "comment" text not null,
+ "preferred_artwork_id" bigint,
+ constraint "fk_release_preferred_artwork" foreign key ("preferred_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred))");
+
+ // Migrate data, with the new preferred_artwork_id field set to null
+ utils::executeCommand(*session.getDboSession(), R"(INSERT INTO release_backup
+SELECT
+ id,
+ version,
+ name,
+ sort_name,
+ mbid,
+ group_mbid,
+ total_disc,
+ COALESCE(artist_display_name, ''),
+ is_compilation,
+ barcode,
+ comment,
+ NULL as preferred_artwork_id
+FROM release)");
+
+ utils::executeCommand(*session.getDboSession(), "DROP TABLE release");
+ utils::executeCommand(*session.getDboSession(), "ALTER TABLE release_backup RENAME TO release");
+
+ // Just increment the scan version of the settings to make the next scan rescan everything
+ utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET artist_info_scan_version = artist_info_scan_version + 1");
+ }
+
bool doDbMigration(Session& session)
{
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
@@ -1286,7 +1338,8 @@ FROM tracklist)");
{ 88, migrateFromV88 },
{ 89, migrateFromV89 },
{ 90, migrateFromV90 },
- { 91, migrateFromV91 }
+ { 91, migrateFromV91 },
+ { 92, migrateFromV92 }
};
bool migrationPerformed{};
diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp
index d14174b8..c2a39890 100644
--- a/src/libs/database/impl/Release.cpp
+++ b/src/libs/database/impl/Release.cpp
@@ -23,9 +23,9 @@
#include "core/PartialDateTime.hpp"
#include "database/Artist.hpp"
+#include "database/Artwork.hpp"
#include "database/Cluster.hpp"
#include "database/Directory.hpp"
-#include "database/Image.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/Types.hpp"
@@ -701,14 +701,9 @@ namespace lms::db
return utils::fetchQueryResults(query);
}
- ObjectPtr Release::getImage() const
+ ObjectPtr Release::getPreferredArtwork() const
{
- return ObjectPtr{ _image };
- }
-
- ImageId Release::getImageId() const
- {
- return _image.id();
+ return ObjectPtr{ _preferredArtwork };
}
void Release::clearLabels()
@@ -741,9 +736,9 @@ namespace lms::db
_releaseTypes.insert(getDboPtr(releaseType));
}
- void Release::setImage(ObjectPtr image)
+ void Release::setPreferredArtwork(ObjectPtr artwork)
{
- _image = getDboPtr(image);
+ _preferredArtwork = getDboPtr(artwork);
}
bool Release::hasVariousArtists() const
diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp
index b9e2f3ca..cc6f9dad 100644
--- a/src/libs/database/impl/Session.cpp
+++ b/src/libs/database/impl/Session.cpp
@@ -23,6 +23,7 @@
#include "core/ITraceLogger.hpp"
#include "database/Artist.hpp"
#include "database/ArtistInfo.hpp"
+#include "database/Artwork.hpp"
#include "database/AuthToken.hpp"
#include "database/Cluster.hpp"
#include "database/Db.hpp"
@@ -102,6 +103,7 @@ namespace lms::db
_session.mapClass("artist");
_session.mapClass("artist_info");
+ _session.mapClass("artwork");
_session.mapClass("auth_token");
_session.mapClass("cluster");
_session.mapClass("cluster_type");
@@ -206,6 +208,10 @@ namespace lms::db
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_artist_id_idx ON artist_info(artist_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_mbid_matched_artist_idx ON artist_info(mbid_matched, artist_id)");
+ utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artwork_id_idx ON artwork(id)");
+ utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artwork_image_idx ON artwork(image_id)");
+ utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artwork_track_embedded_image_idx ON artwork(track_embedded_image_id)");
+
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_user_domain_idx ON auth_token(user_id, domain)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_expiry_idx ON auth_token(domain, expiry)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_value_idx ON auth_token(domain, value)");
@@ -256,7 +262,6 @@ namespace lms::db
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS rated_track_user_track_idx ON rated_track(user_id,track_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)");
- utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_image_idx ON release(image_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_group_mbid_idx ON release(group_mbid)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
diff --git a/src/libs/database/include/database/ArtistInfoId.hpp b/src/libs/database/include/database/ArtistInfoId.hpp
index 1056f37c..5a046270 100644
--- a/src/libs/database/include/database/ArtistInfoId.hpp
+++ b/src/libs/database/include/database/ArtistInfoId.hpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 Emeric Poupon
+ * Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
diff --git a/src/libs/database/include/database/Artwork.hpp b/src/libs/database/include/database/Artwork.hpp
new file mode 100644
index 00000000..f373c502
--- /dev/null
+++ b/src/libs/database/include/database/Artwork.hpp
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2025 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS 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.
+ *
+ * LMS 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 LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+
+#include "database/ArtworkId.hpp"
+#include "database/ImageId.hpp"
+#include "database/Object.hpp"
+#include "database/TrackEmbeddedImageId.hpp"
+
+namespace lms::db
+{
+ class Image;
+ class Session;
+ class TrackEmbeddedImage;
+
+ class Artwork final : public Object
+ {
+ public:
+ Artwork() = default;
+
+ // find
+ static std::size_t getCount(Session& session);
+ static pointer find(Session& session, ArtworkId id);
+ static pointer find(Session& session, TrackEmbeddedImageId id);
+ static pointer find(Session& session, ImageId id);
+
+ // getters
+ TrackEmbeddedImageId getTrackEmbeddedImageId() const { return _trackEmbeddedImage.id(); }
+ ImageId getImageId() const { return _image.id(); }
+
+ template
+ void persist(Action& a)
+ {
+ Wt::Dbo::belongsTo(a, _trackEmbeddedImage, "track_embedded_image", Wt::Dbo::OnDeleteCascade);
+ Wt::Dbo::belongsTo(a, _image, "image", Wt::Dbo::OnDeleteCascade);
+ }
+
+ private:
+ friend class Session;
+ Artwork(ObjectPtr trackEmbeddedImage);
+ Artwork(ObjectPtr image);
+ static pointer create(Session& session, ObjectPtr trackEmbeddedImage);
+ static pointer create(Session& session, ObjectPtr image);
+
+ Wt::Dbo::ptr _trackEmbeddedImage;
+ Wt::Dbo::ptr _image;
+ };
+} // namespace lms::db
diff --git a/src/libs/database/include/database/ArtworkId.hpp b/src/libs/database/include/database/ArtworkId.hpp
new file mode 100644
index 00000000..ba667613
--- /dev/null
+++ b/src/libs/database/include/database/ArtworkId.hpp
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2025 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS 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.
+ *
+ * LMS 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 LMS. If not, see .
+ */
+
+#pragma once
+
+#include "database/IdType.hpp"
+
+LMS_DECLARE_IDTYPE(ArtworkId)
diff --git a/src/libs/database/include/database/Image.hpp b/src/libs/database/include/database/Image.hpp
index de6f78b4..816b4785 100644
--- a/src/libs/database/include/database/Image.hpp
+++ b/src/libs/database/include/database/Image.hpp
@@ -34,7 +34,6 @@ namespace lms::db
{
class Artist;
class Directory;
- class Release;
class Session;
class Image final : public Object
@@ -101,7 +100,6 @@ namespace lms::db
Wt::Dbo::field(a, _height, "height");
Wt::Dbo::hasMany(a, _artists, Wt::Dbo::ManyToOne, "image");
- Wt::Dbo::hasMany(a, _releases, Wt::Dbo::ManyToOne, "image");
Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
}
@@ -118,7 +116,6 @@ namespace lms::db
int _height{};
Wt::Dbo::collection> _artists;
- Wt::Dbo::collection> _releases;
Wt::Dbo::ptr _directory;
};
} // namespace lms::db
diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp
index 03ed0b8f..1f55173a 100644
--- a/src/libs/database/include/database/Release.hpp
+++ b/src/libs/database/include/database/Release.hpp
@@ -31,10 +31,10 @@
#include "core/PartialDateTime.hpp"
#include "core/UUID.hpp"
#include "database/ArtistId.hpp"
+#include "database/ArtworkId.hpp"
#include "database/CountryId.hpp"
#include "database/DirectoryId.hpp"
#include "database/Filters.hpp"
-#include "database/ImageId.hpp"
#include "database/LabelId.hpp"
#include "database/MediaLibraryId.hpp"
#include "database/Object.hpp"
@@ -46,9 +46,9 @@
namespace lms::db
{
class Artist;
+ class Artwork;
class Cluster;
class ClusterType;
- class Image;
class Release;
class Session;
class Track;
@@ -292,8 +292,8 @@ namespace lms::db
core::EnumSet getAdvisories() const;
std::string_view getBarcode() const { return _barcode; }
std::string_view getComment() const { return _comment; }
- ObjectPtr getImage() const;
- ImageId getImageId() const;
+ ObjectPtr getPreferredArtwork() const;
+ ArtworkId getPreferredArtworkId() const;
// Setters
void setName(std::string_view name) { _name = name; }
@@ -311,7 +311,7 @@ namespace lms::db
void addReleaseType(ObjectPtr releaseType);
void setBarcode(std::string_view barcode) { _barcode = barcode; }
void setComment(std::string_view comment) { _comment = comment; }
- void setImage(ObjectPtr image);
+ void setPreferredArtwork(ObjectPtr artwork);
// Get the artists of this release
std::vector> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
@@ -333,9 +333,9 @@ namespace lms::db
Wt::Dbo::field(a, _isCompilation, "is_compilation");
Wt::Dbo::field(a, _barcode, "barcode");
Wt::Dbo::field(a, _comment, "comment");
- Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
- Wt::Dbo::belongsTo(a, _image, "image", Wt::Dbo::OnDeleteSetNull);
+ Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
+ Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull);
Wt::Dbo::hasMany(a, _labels, Wt::Dbo::ManyToMany, "release_label", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _releaseTypes, Wt::Dbo::ManyToMany, "release_release_type", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _countries, Wt::Dbo::ManyToMany, "release_country", "", Wt::Dbo::OnDeleteCascade);
@@ -361,8 +361,8 @@ namespace lms::db
std::string _barcode;
std::string _comment;
- Wt::Dbo::ptr _image;
Wt::Dbo::collection> _tracks;
+ Wt::Dbo::ptr _preferredArtwork;
Wt::Dbo::collection> _labels;
Wt::Dbo::collection> _releaseTypes;
Wt::Dbo::collection> _countries;
diff --git a/src/libs/database/test/Release.cpp b/src/libs/database/test/Release.cpp
index c2e48600..995c5b21 100644
--- a/src/libs/database/test/Release.cpp
+++ b/src/libs/database/test/Release.cpp
@@ -20,10 +20,12 @@
#include "Common.hpp"
#include "core/PartialDateTime.hpp"
+#include "database/Artwork.hpp"
#include "database/Image.hpp"
namespace lms::db::tests
{
+ using ScopedArtwork = ScopedEntity;
using ScopedImage = ScopedEntity;
using ScopedLabel = ScopedEntity;
using ScopedCountry = ScopedEntity;
@@ -1195,27 +1197,28 @@ namespace lms::db::tests
}
}
- TEST_F(DatabaseFixture, Release_image)
+ TEST_F(DatabaseFixture, Release_artwork)
{
ScopedRelease release{ session, "MyRelease" };
{
auto transaction{ session.createReadTransaction() };
- EXPECT_FALSE(release.get()->getImage());
+ EXPECT_FALSE(release.get()->getPreferredArtwork());
}
- ScopedImage image{ session, "/myImage" };
+ ScopedImage image{ session, "/image.jpg" };
+ ScopedArtwork artwork{ session, image.lockAndGet() };
{
auto transaction{ session.createWriteTransaction() };
- release.get().modify()->setImage(image.get());
+ release.get().modify()->setPreferredArtwork(artwork.get());
}
{
auto transaction{ session.createReadTransaction() };
- auto releaseImage(release.get()->getImage());
- ASSERT_TRUE(releaseImage);
- EXPECT_EQ(releaseImage->getId(), image.getId());
+ auto releaseArtwork(release.get()->getPreferredArtwork());
+ ASSERT_TRUE(releaseArtwork);
+ EXPECT_EQ(releaseArtwork->getId(), artwork.getId());
}
}
diff --git a/src/libs/services/artwork/impl/ArtworkService.cpp b/src/libs/services/artwork/impl/ArtworkService.cpp
index 3f1784c6..11426470 100644
--- a/src/libs/services/artwork/impl/ArtworkService.cpp
+++ b/src/libs/services/artwork/impl/ArtworkService.cpp
@@ -23,6 +23,7 @@
#include "core/ILogger.hpp"
#include "core/Utils.hpp"
#include "database/Artist.hpp"
+#include "database/Artwork.hpp"
#include "database/Db.hpp"
#include "database/Image.hpp"
#include "database/Release.hpp"
@@ -215,14 +216,14 @@ namespace lms::artwork
if (isImageFound(res))
return res;
-
+#if 0
// Fallback on external cover of the release
if (const db::Release::pointer release{ db::Release::find(session, releaseId) })
{
if (const db::ImageId imageId{ release->getImageId() }; imageId.isValid())
res = imageId;
}
-
+#endif
if (isImageFound(res))
return res;
@@ -286,36 +287,11 @@ namespace lms::artwork
if (!release)
return res;
- if (const db::ImageId imageId{ release->getImageId() }; imageId.isValid())
- res = imageId;
-
- if (isImageFound(res))
- return res;
-
- // Fallback on embedded Front image
- {
- db::TrackEmbeddedImage::FindParameters params;
- params.setRelease(releaseId);
- params.setImageTypes({ db::ImageType::FrontCover });
- params.setSortMethod(db::TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc);
- params.setRange(db::Range{ .offset = 0, .size = 1 });
-
- db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { res = image->getId(); });
- }
-
- if (isImageFound(res))
- return res;
-
- // Fallback on embedded media image
- {
- db::TrackEmbeddedImage::FindParameters params;
- params.setRelease(releaseId);
- params.setImageTypes({ db::ImageType::Media });
- params.setSortMethod(db::TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc);
- params.setRange(db::Range{ .offset = 0, .size = 1 });
-
- db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { res = image->getId(); });
- }
+ const db::Artwork::pointer artwork{ release->getPreferredArtwork() };
+ if (artwork && artwork->getImageId().isValid())
+ res = artwork->getImageId();
+ else if (artwork && artwork->getTrackEmbeddedImageId().isValid())
+ res = artwork->getTrackEmbeddedImageId();
return res;
}
diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp
index d758e192..7939a401 100644
--- a/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp
+++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp
@@ -23,16 +23,20 @@
#include
#include
#include
+#include
#include "core/IConfig.hpp"
#include "core/ILogger.hpp"
#include "core/Path.hpp"
+#include "database/Artwork.hpp"
#include "database/Db.hpp"
#include "database/Directory.hpp"
#include "database/Image.hpp"
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
+#include "database/TrackEmbeddedImage.hpp"
+#include "database/TrackEmbeddedImageId.hpp"
#include "ScanContext.hpp"
@@ -40,10 +44,45 @@ namespace lms::scanner
{
namespace
{
+ using PreferredArtwork = std::variant;
+ bool isSameArtwork(PreferredArtwork preferredArtwork, const db::Artwork::pointer& artwork)
+ {
+ if (std::holds_alternative(preferredArtwork))
+ return !artwork;
+
+ if (const db::TrackEmbeddedImageId* trackEmbeddedImageId = std::get_if(&preferredArtwork))
+ return artwork && *trackEmbeddedImageId == artwork->getTrackEmbeddedImageId();
+
+ if (const db::ImageId* imageId = std::get_if(&preferredArtwork))
+ return artwork && *imageId == artwork->getImageId();
+
+ return false;
+ }
+
+ std::filesystem::path toPath(db::Session& session, const db::TrackEmbeddedImageId trackEmbeddedImageId)
+ {
+ std::filesystem::path res;
+
+ db::Track::FindParameters params;
+ params.setEmbeddedImage(trackEmbeddedImageId);
+ params.setRange(db::Range{ .offset = 0, .size = 1 });
+ db::Track::find(session, params, [&](const db::Track::pointer& track) {
+ res = track->getAbsoluteFilePath();
+ });
+
+ return res;
+ }
+
+ std::filesystem::path toPath(db::Session& session, const db::ImageId imageId)
+ {
+ db::Image::pointer image{ db::Image::find(session, imageId) };
+ return image ? image->getAbsoluteFilePath() : std::filesystem::path{};
+ }
+
struct ReleaseImageAssociation
{
db::ReleaseId releaseId;
- db::ImageId imageId;
+ PreferredArtwork preferredArtwork;
};
using ReleaseImageAssociationContainer = std::deque;
@@ -52,7 +91,7 @@ namespace lms::scanner
db::Session& session;
db::ReleaseId lastRetrievedReleaseId;
std::size_t processedReleaseCount{};
- const std::vector& releaseFileNames;
+ const std::vector& releaseImageFileNames;
};
db::Image::pointer findImageInDirectory(SearchReleaseImageContext& searchContext, const std::filesystem::path& directoryPath)
@@ -62,7 +101,7 @@ namespace lms::scanner
const db::Directory::pointer directory{ db::Directory::find(searchContext.session, directoryPath) };
if (directory) // may not exist for releases that are split on different media libraries
{
- for (std::string_view fileStem : searchContext.releaseFileNames)
+ for (std::string_view fileStem : searchContext.releaseImageFileNames)
{
db::Image::FindParameters params;
params.setDirectory(directory->getId());
@@ -129,7 +168,45 @@ namespace lms::scanner
return image;
}
- bool fetchNextReleaseImagesToUpdate(SearchReleaseImageContext& searchContext, ReleaseImageAssociationContainer& releaseImageAssociations)
+ PreferredArtwork computeBestReleaseArtwork(SearchReleaseImageContext& searchContext, const db::Release::pointer& release)
+ {
+ const db::Image::pointer image{ computeBestReleaseImage(searchContext, release) };
+ if (image)
+ return PreferredArtwork{ image->getId() };
+
+ // Fallback on embedded Front image
+ db::TrackEmbeddedImageId trackEmbeddedImageId;
+ {
+ db::TrackEmbeddedImage::FindParameters params;
+ params.setRelease(release->getId());
+ params.setImageTypes({ db::ImageType::FrontCover });
+ params.setSortMethod(db::TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc);
+ params.setRange(db::Range{ .offset = 0, .size = 1 });
+
+ db::TrackEmbeddedImage::find(searchContext.session, params, [&](const db::TrackEmbeddedImage::pointer& image) { trackEmbeddedImageId = image->getId(); });
+ }
+
+ if (trackEmbeddedImageId.isValid())
+ return PreferredArtwork{ trackEmbeddedImageId };
+
+ // Fallback on embedded media image
+ {
+ db::TrackEmbeddedImage::FindParameters params;
+ params.setRelease(release->getId());
+ params.setImageTypes({ db::ImageType::Media });
+ params.setSortMethod(db::TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc);
+ params.setRange(db::Range{ .offset = 0, .size = 1 });
+
+ db::TrackEmbeddedImage::find(searchContext.session, params, [&](const db::TrackEmbeddedImage::pointer& image) { trackEmbeddedImageId = image->getId(); });
+ }
+
+ if (trackEmbeddedImageId.isValid())
+ return PreferredArtwork{ trackEmbeddedImageId };
+
+ return PreferredArtwork{};
+ }
+
+ bool fetchNextReleaseArtworksToUpdate(SearchReleaseImageContext& searchContext, ReleaseImageAssociationContainer& releaseImageAssociations)
{
const db::ReleaseId releaseId{ searchContext.lastRetrievedReleaseId };
@@ -139,13 +216,11 @@ namespace lms::scanner
auto transaction{ searchContext.session.createReadTransaction() };
db::Release::find(searchContext.session, searchContext.lastRetrievedReleaseId, readBatchSize, [&](const db::Release::pointer& release) {
- db::Image::pointer image{ computeBestReleaseImage(searchContext, release) };
+ const PreferredArtwork preferredArtwork{ computeBestReleaseArtwork(searchContext, release) };
+ const db::Artwork::pointer currentPreferredArtwork{ release->getPreferredArtwork() };
- if (image != release->getImage())
- {
- LMS_LOG(DBUPDATER, DEBUG, "Updating release image for release '" << release->getName() << "', using '" << (image ? image->getAbsoluteFilePath().c_str() : "") << "'");
- releaseImageAssociations.push_back(ReleaseImageAssociation{ release->getId(), image ? image->getId() : db::ImageId{} });
- }
+ if (!isSameArtwork(preferredArtwork, currentPreferredArtwork))
+ releaseImageAssociations.push_back(ReleaseImageAssociation{ release->getId(), preferredArtwork });
searchContext.processedReleaseCount++;
});
}
@@ -153,16 +228,40 @@ namespace lms::scanner
return releaseId != searchContext.lastRetrievedReleaseId;
}
- void updateReleaseImage(db::Session& session, const ReleaseImageAssociation& releaseImageAssociation)
+ void updateReleaseArtwork(db::Session& session, const ReleaseImageAssociation& releaseImageAssociation)
{
db::Release::pointer release{ db::Release::find(session, releaseImageAssociation.releaseId) };
assert(release);
- db::Image::pointer image;
- if (releaseImageAssociation.imageId.isValid())
- image = db::Image::find(session, releaseImageAssociation.imageId);
+ db::Artwork::pointer artwork;
+ if (const db::TrackEmbeddedImageId * trackEmbeddedImageId{ std::get_if(&releaseImageAssociation.preferredArtwork) })
+ {
+ artwork = db::Artwork::find(session, *trackEmbeddedImageId);
+ if (!artwork)
+ {
+ db::TrackEmbeddedImage::pointer trackEmbeddedImage{ db::TrackEmbeddedImage::find(session, *trackEmbeddedImageId) };
+ assert(trackEmbeddedImage);
+ artwork = session.create(trackEmbeddedImage);
+ }
- release.modify()->setImage(image);
+ LMS_LOG(DBUPDATER, DEBUG, "Updating preferred artwork in release '" << release->getName() << "' with embedded image in track " << toPath(session, *trackEmbeddedImageId));
+ }
+ else if (const db::ImageId * imageId{ std::get_if(&releaseImageAssociation.preferredArtwork) })
+ {
+ artwork = db::Artwork::find(session, *imageId);
+ if (!artwork)
+ {
+ db::Image::pointer image{ db::Image::find(session, *imageId) };
+ assert(image);
+ artwork = session.create(image);
+ }
+
+ LMS_LOG(DBUPDATER, DEBUG, "Updating preferred artwork in release '" << release->getName() << "' with image " << toPath(session, *imageId));
+ }
+ else
+ LMS_LOG(DBUPDATER, DEBUG, "Removing preferred artwork from release '" << release->getName() << "'");
+
+ release.modify()->setPreferredArtwork(artwork);
}
void updateReleaseImages(db::Session& session, ReleaseImageAssociationContainer& imageAssociations)
@@ -175,13 +274,13 @@ namespace lms::scanner
for (std::size_t i{}; !imageAssociations.empty() && i < writeBatchSize; ++i)
{
- updateReleaseImage(session, imageAssociations.front());
+ updateReleaseArtwork(session, imageAssociations.front());
imageAssociations.pop_front();
}
}
}
- std::vector constructReleaseFileNames()
+ std::vector constructReleaseImageFileNames()
{
std::vector res;
@@ -198,16 +297,13 @@ namespace lms::scanner
ScanStepAssociateReleaseImages::ScanStepAssociateReleaseImages(InitParams& initParams)
: ScanStepBase{ initParams }
- , _releaseFileNames{ constructReleaseFileNames() }
+ , _releaseImageFileNames{ constructReleaseImageFileNames() }
{
}
bool ScanStepAssociateReleaseImages::needProcess(const ScanContext& context) const
{
- if (context.stats.nbChanges() > 0)
- return true;
-
- return false;
+ return context.stats.nbChanges() > 0;
}
void ScanStepAssociateReleaseImages::process(ScanContext& context)
@@ -222,11 +318,11 @@ namespace lms::scanner
SearchReleaseImageContext searchContext{
.session = session,
.lastRetrievedReleaseId = {},
- .releaseFileNames = _releaseFileNames,
+ .releaseImageFileNames = _releaseImageFileNames,
};
ReleaseImageAssociationContainer releaseImageAssociations;
- while (fetchNextReleaseImagesToUpdate(searchContext, releaseImageAssociations))
+ while (fetchNextReleaseArtworksToUpdate(searchContext, releaseImageAssociations))
{
if (_abortScan)
return;
diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.hpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.hpp
index 70376a09..07c59862 100644
--- a/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.hpp
+++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.hpp
@@ -40,6 +40,6 @@ namespace lms::scanner
bool needProcess(const ScanContext& context) const override;
void process(ScanContext& context) override;
- const std::vector _releaseFileNames;
+ const std::vector _releaseImageFileNames;
};
} // namespace lms::scanner