Now using new artwork table everywhere in app and subsonic API to refer to artworks

This commit is contained in:
emeric
2025-06-22 16:49:54 +02:00
parent 93b446e124
commit c251c2a924
34 changed files with 475 additions and 478 deletions
+13
View File
@@ -19,6 +19,8 @@
#include "database/Artwork.hpp"
#include <Wt/Dbo/WtSqlTraits.h>
#include "database/Image.hpp"
#include "database/Session.hpp"
#include "database/TrackEmbeddedImage.hpp"
@@ -74,4 +76,15 @@ namespace lms::db
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Artwork>>("SELECT a FROM artwork a").where("a.image_id = ?").bind(id));
}
Wt::WDateTime Artwork::getLastWrittenTime() const
{
auto query{ session()->query<Wt::WDateTime>("SELECT MAX(COALESCE(image.file_last_write, track.file_last_write)) AS last_written_datetime FROM artwork") };
query.leftJoin("image ON artwork.image_id = image.id");
query.leftJoin("track_embedded_image ON artwork.track_embedded_image_id = track_embedded_image.id");
query.leftJoin("track_embedded_image_link ON track_embedded_image.id = track_embedded_image_link.track_embedded_image_id");
query.leftJoin("track ON track.id = track_embedded_image_link.track_id");
query.where("artwork.id = ?").bind(getId());
return utils::fetchQuerySingleResult(query);
}
} // namespace lms::db
+10
View File
@@ -374,11 +374,21 @@ namespace lms::db
return _preferredArtwork;
}
ArtworkId Track::getPreferredArtworkId() const
{
return _preferredArtwork.id();
}
ObjectPtr<Artwork> Track::getPreferredMediaArtwork() const
{
return _preferredMediaArtwork;
}
ArtworkId Track::getPreferredMediaArtworkId() const
{
return _preferredMediaArtwork.id();
}
RangeResults<TrackId> Track::findIds(Session& session, const FindParameters& parameters)
{
session.checkReadTransaction();
@@ -20,6 +20,7 @@
#pragma once
#include <Wt/Dbo/Dbo.h>
#include <Wt/WDateTime.h>
#include "database/ArtworkId.hpp"
#include "database/ImageId.hpp"
@@ -46,6 +47,7 @@ namespace lms::db
// getters
TrackEmbeddedImageId getTrackEmbeddedImageId() const { return _trackEmbeddedImage.id(); }
ImageId getImageId() const { return _image.id(); }
Wt::WDateTime getLastWrittenTime() const;
template<class Action>
void persist(Action& a)
@@ -306,7 +306,9 @@ namespace lms::db
ObjectPtr<MediaLibrary> getMediaLibrary() const;
ObjectPtr<Directory> getDirectory() const;
ObjectPtr<Artwork> getPreferredArtwork() const;
ArtworkId getPreferredArtworkId() const;
ObjectPtr<Artwork> getPreferredMediaArtwork() const;
ArtworkId getPreferredMediaArtworkId() const;
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypes, std::size_t size) const;
+83
View File
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2025of 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 <http://www.gnu.org/licenses/>.
*/
#include "Common.hpp"
#include "database/Artwork.hpp"
#include "database/Image.hpp"
#include "database/TrackEmbeddedImage.hpp"
#include "database/TrackEmbeddedImageLink.hpp"
namespace lms::db::tests
{
using ScopedArtwork = ScopedEntity<db::Artwork>;
using ScopedImage = ScopedEntity<db::Image>;
using ScopedTrackEmbeddedImage = ScopedEntity<db::TrackEmbeddedImage>;
TEST_F(DatabaseFixture, Artwork_image)
{
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Artwork::getCount(session), 0);
}
ScopedImage image{ session, "/MyImage" };
ScopedArtwork artwork{ session, image.lockAndGet() };
const Wt::WDateTime dateTime{ Wt::WDate{ 2025, 1, 1 } };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Artwork::getCount(session), 1);
}
{
auto transaction{ session.createWriteTransaction() };
image.get().modify()->setLastWriteTime(dateTime);
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(artwork.get()->getLastWrittenTime(), dateTime);
}
}
TEST_F(DatabaseFixture, Artwork_trackEmbeddedImage)
{
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Artwork::getCount(session), 0);
}
ScopedTrackEmbeddedImage image{ session };
ScopedTrack track{ session };
ScopedArtwork artwork{ session, image.lockAndGet() };
const Wt::WDateTime dateTime{ Wt::WDate{ 2025, 1, 1 } };
{
auto transaction{ session.createWriteTransaction() };
session.create<db::TrackEmbeddedImageLink>(track.get(), image.get());
track.get().modify()->setLastWriteTime(dateTime);
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(artwork.get()->getLastWrittenTime(), dateTime);
}
}
} // namespace lms::db::tests
+1
View File
@@ -2,6 +2,7 @@
add_executable(test-database
Artist.cpp
ArtistInfo.cpp
Artwork.cpp
AuthToken.cpp
Cluster.cpp
Common.cpp
+58 -103
View File
@@ -26,12 +26,13 @@
#include "database/Artwork.hpp"
#include "database/Db.hpp"
#include "database/Image.hpp"
#include "database/ImageId.hpp"
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/TrackEmbeddedImage.hpp"
#include "database/TrackEmbeddedImageLink.hpp"
#include "database/Types.hpp"
#include "database/TrackList.hpp"
#include "image/Exception.hpp"
#include "image/IEncodedImage.hpp"
#include "image/Image.hpp"
@@ -87,12 +88,12 @@ namespace lms::artwork
return image;
}
std::shared_ptr<image::IEncodedImage> ArtworkService::getDefaultReleaseCover()
std::shared_ptr<image::IEncodedImage> ArtworkService::getDefaultReleaseArtwork()
{
return _defaultReleaseCover;
}
std::shared_ptr<image::IEncodedImage> ArtworkService::getDefaultArtistImage()
std::shared_ptr<image::IEncodedImage> ArtworkService::getDefaultArtistArtwork()
{
return _defaultArtistImage;
}
@@ -136,150 +137,104 @@ namespace lms::artwork
return image;
}
ArtworkService::ImageFindResult ArtworkService::findArtistImage(db::ArtistId artistId)
db::ArtworkId ArtworkService::findTrackListImage(db::TrackListId trackListId)
{
db::ArtworkId artworkId;
// Iterate over all tracks and stop when we find an artwork
db::Session& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() };
ImageFindResult res;
db::TrackList::pointer trackList{ db::TrackList::find(session, trackListId) };
if (!trackList)
return artworkId;
const db::Artist::pointer artist{ db::Artist::find(session, artistId) };
if (!artist)
return res;
const auto entries{ trackList->getEntries(db::Range{ 0, 10 }) };
for (const auto& entry : entries.results)
{
const auto track{ entry->getTrack() };
if (track->getPreferredMediaArtworkId().isValid())
{
artworkId = track->getPreferredMediaArtworkId();
break; // stop iteration
}
const db::Artwork::pointer artwork{ artist->getPreferredArtwork() };
if (artwork && artwork->getImageId().isValid())
res = artwork->getImageId();
else if (artwork && artwork->getTrackEmbeddedImageId().isValid())
res = artwork->getTrackEmbeddedImageId();
if (track->getPreferredArtworkId().isValid())
{
artworkId = track->getPreferredArtworkId();
break; // stop iteration
}
}
return res;
return artworkId;
}
ArtworkService::ImageFindResult ArtworkService::findTrackImage(db::TrackId trackId)
std::shared_ptr<image::IEncodedImage> ArtworkService::getImage(db::ArtworkId artworkId, std::optional<image::ImageSize> width)
{
ImageFindResult res;
const ImageCache::EntryDesc cacheEntryDesc{ artworkId, width };
db::Session& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() };
std::shared_ptr<image::IEncodedImage> image{ _cache.getImage(cacheEntryDesc) };
if (image)
return image;
const db::Track::pointer track{ db::Track::find(session, trackId) };
if (!track)
return res;
db::TrackEmbeddedImageId trackEmbeddedImageId;
db::ImageId imageId;
const db::Artwork::pointer artwork{ track->getPreferredArtwork() };
if (artwork && artwork->getImageId().isValid())
res = artwork->getImageId();
else if (artwork && artwork->getTrackEmbeddedImageId().isValid())
res = artwork->getTrackEmbeddedImageId();
{
db::Session& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() };
return res;
}
db::Artwork::pointer artwork{ db::Artwork::find(session, artworkId) };
if (artwork)
{
trackEmbeddedImageId = artwork->getTrackEmbeddedImageId();
imageId = artwork->getImageId();
}
}
ArtworkService::ImageFindResult ArtworkService::findTrackMediaImage(db::TrackId trackId)
{
db::Session& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() };
ImageFindResult res;
if (trackEmbeddedImageId.isValid())
image = getTrackEmbeddedImage(trackEmbeddedImageId, width);
else if (imageId.isValid())
image = getImage(imageId, width);
const db::Track::pointer track{ db::Track::find(session, trackId) };
if (!track)
return res;
if (image)
_cache.addImage(cacheEntryDesc, image);
const db::Artwork::pointer artwork{ track->getPreferredMediaArtwork() };
if (artwork && artwork->getImageId().isValid())
res = artwork->getImageId();
else if (artwork && artwork->getTrackEmbeddedImageId().isValid())
res = artwork->getTrackEmbeddedImageId();
return res;
}
ArtworkService::ImageFindResult ArtworkService::findReleaseImage(db::ReleaseId releaseId)
{
db::Session& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() };
ImageFindResult res;
const db::Release::pointer release{ db::Release::find(session, releaseId) };
if (!release)
return res;
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;
}
ArtworkService::ImageFindResult ArtworkService::findTrackListImage(db::TrackListId trackListId)
{
db::Session& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() };
ImageFindResult res;
db::TrackEmbeddedImage::FindParameters params;
params.setTrackList(trackListId);
params.setSortMethod(db::TrackEmbeddedImageSortMethod::TrackListIndexAscThenSizeDesc);
params.setRange(db::Range{ .offset = 0, .size = 1 });
db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) {
res = image->getId();
});
// TODO fallback on release image if not found
return res;
return image;
}
std::shared_ptr<image::IEncodedImage> ArtworkService::getImage(db::ImageId imageId, std::optional<image::ImageSize> width)
{
const ImageCache::EntryDesc cacheEntryDesc{ imageId, width };
std::shared_ptr<image::IEncodedImage> cover{ _cache.getImage(cacheEntryDesc) };
if (cover)
return cover;
std::filesystem::path imageFile;
{
db::Session& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() };
const db::Image::pointer image{ db::Image::find(session, imageId) };
if (image)
imageFile = image->getAbsoluteFilePath();
if (!image)
return nullptr;
imageFile = image->getAbsoluteFilePath();
}
cover = getFromImageFile(imageFile, width);
if (cover)
_cache.addImage(cacheEntryDesc, cover);
return cover;
return getFromImageFile(imageFile, width);
}
std::shared_ptr<image::IEncodedImage> ArtworkService::getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional<image::ImageSize> width)
{
const ImageCache::EntryDesc cacheEntryDesc{ trackEmbeddedImageId, width };
std::shared_ptr<image::IEncodedImage> image{ _cache.getImage(cacheEntryDesc) };
if (image)
return image;
std::shared_ptr<image::IEncodedImage> image;
{
db::Session& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() };
// TODO: could be put outside transaction
db::TrackEmbeddedImageLink::find(session, trackEmbeddedImageId, [&](const db::TrackEmbeddedImageLink::pointer& link) {
if (!image)
image = getTrackImage(link->getTrack()->getAbsoluteFilePath(), link->getIndex(), width);
});
}
if (image)
_cache.addImage(cacheEntryDesc, image);
return image;
}
@@ -22,6 +22,8 @@
#include <filesystem>
#include <vector>
#include "database/ImageId.hpp"
#include "database/TrackEmbeddedImageId.hpp"
#include "services/artwork/IArtworkService.hpp"
#include "ImageCache.hpp"
@@ -47,21 +49,19 @@ namespace lms::artwork
ArtworkService& operator=(const ArtworkService&) = delete;
private:
ImageFindResult findArtistImage(db::ArtistId artistId) override;
ImageFindResult findTrackImage(db::TrackId trackId) override;
ImageFindResult findTrackMediaImage(db::TrackId trackId) override;
ImageFindResult findReleaseImage(db::ReleaseId releaseId) override;
ImageFindResult findTrackListImage(db::TrackListId trackListId) override;
db::ArtworkId findTrackListImage(db::TrackListId trackListId) override;
std::shared_ptr<image::IEncodedImage> getImage(db::ImageId imageId, std::optional<image::ImageSize> width) override;
std::shared_ptr<image::IEncodedImage> getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional<image::ImageSize> width) override;
std::shared_ptr<image::IEncodedImage> getImage(db::ArtworkId artworkId, std::optional<image::ImageSize> width) override;
std::shared_ptr<image::IEncodedImage> getDefaultReleaseCover() override;
std::shared_ptr<image::IEncodedImage> getDefaultArtistImage() override;
std::shared_ptr<image::IEncodedImage> getDefaultReleaseArtwork() override;
std::shared_ptr<image::IEncodedImage> getDefaultArtistArtwork() override;
void flushCache() override;
void setJpegQuality(unsigned quality) override;
std::shared_ptr<image::IEncodedImage> getImage(db::ImageId imageId, std::optional<image::ImageSize> width);
std::shared_ptr<image::IEncodedImage> getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional<image::ImageSize> width);
std::unique_ptr<image::IEncodedImage> getFromImageFile(const std::filesystem::path& p, std::optional<image::ImageSize> width) const;
std::unique_ptr<image::IEncodedImage> getTrackImage(const std::filesystem::path& path, std::size_t index, std::optional<image::ImageSize> width) const;
@@ -25,10 +25,8 @@
#include <optional>
#include <shared_mutex>
#include <unordered_map>
#include <variant>
#include "database/ImageId.hpp"
#include "database/TrackEmbeddedImageId.hpp"
#include "database/ArtworkId.hpp"
#include "image/IEncodedImage.hpp"
namespace lms::artwork
@@ -40,8 +38,7 @@ namespace lms::artwork
struct EntryDesc
{
using VariantType = std::variant<db::TrackEmbeddedImageId, db::ImageId>;
VariantType id;
db::ArtworkId id;
std::optional<std::size_t> size;
bool operator==(const EntryDesc& other) const = default;
@@ -63,13 +60,13 @@ namespace lms::artwork
std::size_t operator()(const EntryDesc& entry) const
{
assert(entry.size); // should not cache unresized images
return std::hash<EntryDesc::VariantType>{}(entry.id) ^ std::hash<std::size_t>{}(*entry.size);
return std::hash<db::ArtworkId>{}(entry.id) ^ std::hash<std::size_t>{}(*entry.size);
}
};
std::unordered_map<EntryDesc, std::shared_ptr<image::IEncodedImage>, EntryHasher> _cache;
std::size_t _cacheSize{};
mutable std::atomic<std::size_t> _cacheMisses{};
mutable std::atomic<std::size_t> _cacheHits{};
mutable std::atomic<std::size_t> _cacheMisses;
mutable std::atomic<std::size_t> _cacheHits;
};
} // namespace lms::artwork
@@ -22,13 +22,8 @@
#include <filesystem>
#include <memory>
#include <optional>
#include <variant>
#include "database/ArtistId.hpp"
#include "database/ImageId.hpp"
#include "database/ReleaseId.hpp"
#include "database/TrackEmbeddedImageId.hpp"
#include "database/TrackId.hpp"
#include "database/ArtworkId.hpp"
#include "database/TrackListId.hpp"
#include "image/IEncodedImage.hpp"
@@ -44,27 +39,15 @@ namespace lms::artwork
public:
virtual ~IArtworkService() = default;
// Helpers to find artworks
using ImageFindResult = std::variant<std::monostate, db::ImageId, db::TrackEmbeddedImageId>;
virtual ImageFindResult findArtistImage(db::ArtistId artistId) = 0;
// Helpers to get preferred artworks
virtual db::ArtworkId findTrackListImage(db::TrackListId trackListId) = 0;
// Will get Disc/Media artwork if available, otherwise, will fallback on release artwork
virtual ImageFindResult findTrackImage(db::TrackId trackId) = 0;
// Will get Disc/Media artwork if available, no fallback
virtual ImageFindResult findTrackMediaImage(db::TrackId trackId) = 0;
// Will get Release if available, otherwise, will fallback on embedded artworks
virtual ImageFindResult findReleaseImage(db::ReleaseId releaseId) = 0;
virtual ImageFindResult findTrackListImage(db::TrackListId trackListId) = 0;
// Image retrieval
virtual std::shared_ptr<image::IEncodedImage> getImage(db::ImageId imageId, std::optional<image::ImageSize> width) = 0;
virtual std::shared_ptr<image::IEncodedImage> getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional<image::ImageSize> width) = 0;
// Image retrieval, no width means original size
virtual std::shared_ptr<image::IEncodedImage> getImage(db::ArtworkId artworkId, std::optional<image::ImageSize> width) = 0;
// Svg images don't have image "size"
virtual std::shared_ptr<image::IEncodedImage> getDefaultReleaseCover() = 0;
virtual std::shared_ptr<image::IEncodedImage> getDefaultArtistImage() = 0;
virtual std::shared_ptr<image::IEncodedImage> getDefaultReleaseArtwork() = 0;
virtual std::shared_ptr<image::IEncodedImage> getDefaultArtistArtwork() = 0;
virtual void flushCache() = 0;
@@ -60,14 +60,14 @@ namespace lms::scanner
return false;
}
struct ReleaseImageAssociation
struct ReleaseArtworkAssociation
{
db::ReleaseId releaseId;
db::Release::pointer release;
ReleaseArtwork preferredArtwork;
};
using ReleaseImageAssociationContainer = std::deque<ReleaseImageAssociation>;
using ReleaseArtworkAssociationContainer = std::deque<ReleaseArtworkAssociation>;
struct SearchReleaseImageContext
struct SearchReleaseArtworkContext
{
db::Session& session;
db::ReleaseId lastRetrievedReleaseId;
@@ -75,7 +75,7 @@ namespace lms::scanner
const std::vector<std::string>& releaseImageFileNames;
};
db::Image::pointer findImageInDirectory(SearchReleaseImageContext& searchContext, const std::filesystem::path& directoryPath)
db::Image::pointer findImageInDirectory(SearchReleaseArtworkContext& searchContext, const std::filesystem::path& directoryPath)
{
db::Image::pointer image;
@@ -101,7 +101,7 @@ namespace lms::scanner
return image;
}
db::Image::pointer computePreferredReleaseImage(SearchReleaseImageContext& searchContext, const db::Release::pointer& release)
db::Image::pointer computePreferredReleaseImage(SearchReleaseArtworkContext& searchContext, const db::Release::pointer& release)
{
db::Image::pointer image;
@@ -149,7 +149,7 @@ namespace lms::scanner
return image;
}
ReleaseArtwork computePreferredReleaseArtwork(SearchReleaseImageContext& searchContext, const db::Release::pointer& release)
ReleaseArtwork computePreferredReleaseArtwork(SearchReleaseArtworkContext& searchContext, const db::Release::pointer& release)
{
const db::Image::pointer image{ computePreferredReleaseImage(searchContext, release) };
if (image)
@@ -187,7 +187,7 @@ namespace lms::scanner
return ReleaseArtwork{};
}
bool fetchNextReleaseArtworksToUpdate(SearchReleaseImageContext& searchContext, ReleaseImageAssociationContainer& releaseImageAssociations)
bool fetchNextReleaseArtworksToUpdate(SearchReleaseArtworkContext& searchContext, ReleaseArtworkAssociationContainer& ReleaseArtworkAssociations)
{
const db::ReleaseId releaseId{ searchContext.lastRetrievedReleaseId };
@@ -201,7 +201,7 @@ namespace lms::scanner
const db::Artwork::pointer currentPreferredArtwork{ release->getPreferredArtwork() };
if (!isSameArtwork(preferredArtwork, currentPreferredArtwork))
releaseImageAssociations.push_back(ReleaseImageAssociation{ release->getId(), preferredArtwork });
ReleaseArtworkAssociations.push_back(ReleaseArtworkAssociation{ release, preferredArtwork });
searchContext.processedReleaseCount++;
});
}
@@ -209,16 +209,14 @@ namespace lms::scanner
return releaseId != searchContext.lastRetrievedReleaseId;
}
void updateReleaseArtwork(db::Session& session, const ReleaseImageAssociation& releaseImageAssociation)
void updateReleaseArtwork(db::Session& session, const ReleaseArtworkAssociation& ReleaseArtworkAssociation)
{
db::Release::pointer release{ db::Release::find(session, releaseImageAssociation.releaseId) };
assert(release);
db::Release::pointer release{ ReleaseArtworkAssociation.release };
db::Artwork::pointer artwork;
if (const db::TrackEmbeddedImageId * trackEmbeddedImageId{ std::get_if<db::TrackEmbeddedImageId>(&releaseImageAssociation.preferredArtwork) })
if (const db::TrackEmbeddedImageId * trackEmbeddedImageId{ std::get_if<db::TrackEmbeddedImageId>(&ReleaseArtworkAssociation.preferredArtwork) })
artwork = utils::getOrCreateArtworkFromTrackEmbeddedImage(session, *trackEmbeddedImageId);
else if (const db::ImageId * imageId{ std::get_if<db::ImageId>(&releaseImageAssociation.preferredArtwork) })
else if (const db::ImageId * imageId{ std::get_if<db::ImageId>(&ReleaseArtworkAssociation.preferredArtwork) })
artwork = utils::getOrCreateArtworkFromImage(session, *imageId);
release.modify()->setPreferredArtwork(artwork);
@@ -229,7 +227,7 @@ namespace lms::scanner
LMS_LOG(DBUPDATER, DEBUG, "Removed preferred artwork from release '" << release->getName() << "'");
}
void updateReleaseImages(db::Session& session, ReleaseImageAssociationContainer& imageAssociations)
void updateReleaseArtworks(db::Session& session, ReleaseArtworkAssociationContainer& imageAssociations)
{
constexpr std::size_t writeBatchSize{ 50 };
@@ -280,19 +278,19 @@ namespace lms::scanner
context.currentStepStats.totalElems = db::Release::getCount(session);
}
SearchReleaseImageContext searchContext{
SearchReleaseArtworkContext searchContext{
.session = session,
.lastRetrievedReleaseId = {},
.releaseImageFileNames = _releaseImageFileNames,
};
ReleaseImageAssociationContainer releaseImageAssociations;
while (fetchNextReleaseArtworksToUpdate(searchContext, releaseImageAssociations))
ReleaseArtworkAssociationContainer ReleaseArtworkAssociations;
while (fetchNextReleaseArtworksToUpdate(searchContext, ReleaseArtworkAssociations))
{
if (_abortScan)
return;
updateReleaseImages(session, releaseImageAssociations);
updateReleaseArtworks(session, ReleaseArtworkAssociations);
context.currentStepStats.processedElems = searchContext.processedReleaseCount;
_progressCallback(context.currentStepStats);
}
@@ -62,22 +62,22 @@ namespace lms::scanner
return !std::holds_alternative<std::monostate>(res);
}
struct TrackImageAssociation
struct TrackArtworksAssociation
{
db::Track::pointer track;
TrackArtwork preferredArtwork;
TrackArtwork preferredMediaArtwork;
};
using TrackImageAssociationContainer = std::deque<TrackImageAssociation>;
using TrackArtworksAssociationContainer = std::deque<TrackArtworksAssociation>;
struct SearchTrackImageContext
struct SearchTrackArtworkContext
{
db::Session& session;
db::TrackId lastRetrievedTrackId;
std::size_t processedTrackCount{};
};
TrackArtwork computePreferredTrackArtwork(SearchTrackImageContext& searchContext, const db::Track::pointer& track)
TrackArtwork computePreferredTrackArtwork(SearchTrackArtworkContext& searchContext, const db::Track::pointer& track)
{
// Try to get a media image
TrackArtwork res;
@@ -135,7 +135,7 @@ namespace lms::scanner
return res;
}
TrackArtwork computePreferredTrackMediaArtwork(SearchTrackImageContext& searchContext, const db::Track::pointer& track)
TrackArtwork computePreferredTrackMediaArtwork(SearchTrackArtworkContext& searchContext, const db::Track::pointer& track)
{
TrackArtwork res;
{
@@ -165,7 +165,7 @@ namespace lms::scanner
return res;
}
bool fetchNextTrackArtworksToUpdate(SearchTrackImageContext& searchContext, TrackImageAssociationContainer& trackImageAssociations)
bool fetchNextTrackArtworksToUpdate(SearchTrackArtworkContext& searchContext, TrackArtworksAssociationContainer& TrackArtworksAssociations)
{
const db::TrackId trackId{ searchContext.lastRetrievedTrackId };
@@ -184,7 +184,7 @@ namespace lms::scanner
if (!isSameArtwork(preferredArtwork, currentPreferredArtwork)
|| !isSameArtwork(preferredMediaArtwork, currentPreferredMediaArtwork))
{
trackImageAssociations.push_back(TrackImageAssociation{ track, preferredArtwork, preferredMediaArtwork });
TrackArtworksAssociations.push_back(TrackArtworksAssociation{ track, preferredArtwork, preferredMediaArtwork });
}
searchContext.processedTrackCount++;
@@ -226,24 +226,24 @@ namespace lms::scanner
LMS_LOG(DBUPDATER, DEBUG, "Removed preferred media artwork from track '" << track->getAbsoluteFilePath() << "'");
}
void updateTrackPreferredArtworks(db::Session& session, const TrackImageAssociation& trackImageAssociation)
void updateTrackPreferredArtworks(db::Session& session, const TrackArtworksAssociation& TrackArtworksAssociation)
{
db::Track::pointer track{ trackImageAssociation.track };
db::Track::pointer track{ TrackArtworksAssociation.track };
{
const db::Artwork::pointer currentPreferredArtwork{ track->getPreferredArtwork() };
if (!isSameArtwork(trackImageAssociation.preferredArtwork, currentPreferredArtwork))
updateTrackPreferredArtwork(session, track, trackImageAssociation.preferredArtwork);
if (!isSameArtwork(TrackArtworksAssociation.preferredArtwork, currentPreferredArtwork))
updateTrackPreferredArtwork(session, track, TrackArtworksAssociation.preferredArtwork);
}
{
const db::Artwork::pointer currentPreferredMediaArtwork{ track->getPreferredMediaArtwork() };
if (!isSameArtwork(trackImageAssociation.preferredMediaArtwork, currentPreferredMediaArtwork))
updateTrackPreferredMediaArtwork(session, track, trackImageAssociation.preferredMediaArtwork);
if (!isSameArtwork(TrackArtworksAssociation.preferredMediaArtwork, currentPreferredMediaArtwork))
updateTrackPreferredMediaArtwork(session, track, TrackArtworksAssociation.preferredMediaArtwork);
}
}
void updateTrackPreferredArtworks(db::Session& session, TrackImageAssociationContainer& imageAssociations)
void updateTrackPreferredArtworks(db::Session& session, TrackArtworksAssociationContainer& imageAssociations)
{
constexpr std::size_t writeBatchSize{ 50 };
@@ -279,18 +279,18 @@ namespace lms::scanner
context.currentStepStats.totalElems = db::Track::getCount(session);
}
SearchTrackImageContext searchContext{
SearchTrackArtworkContext searchContext{
.session = session,
.lastRetrievedTrackId = {},
};
TrackImageAssociationContainer trackImageAssociations;
while (fetchNextTrackArtworksToUpdate(searchContext, trackImageAssociations))
TrackArtworksAssociationContainer TrackArtworksAssociations;
while (fetchNextTrackArtworksToUpdate(searchContext, TrackArtworksAssociations))
{
if (_abortScan)
return;
updateTrackPreferredArtworks(session, trackImageAssociations);
updateTrackPreferredArtworks(session, TrackArtworksAssociations);
context.currentStepStats.processedElems = searchContext.processedTrackCount;
_progressCallback(context.currentStepStats);
}
+19 -44
View File
@@ -22,8 +22,7 @@
#include <cassert>
#include "core/String.hpp"
#include "database/ImageId.hpp"
#include "database/TrackEmbeddedImageId.hpp"
#include "database/ArtworkId.hpp"
namespace lms::api::subsonic
{
@@ -31,14 +30,9 @@ namespace lms::api::subsonic
{
constexpr char timestampSeparatorChar{ '-' };
std::string idToString(db::ImageId id)
std::string idToString(db::ArtworkId id)
{
return "im-" + id.toString();
}
std::string idToString(db::TrackEmbeddedImageId id)
{
return "trim-" + id.toString();
return "art-" + id.toString();
}
} // namespace
@@ -46,19 +40,10 @@ namespace lms::api::subsonic
{
std::string res;
// produce "type-id[-timestamp]"
if (db::TrackEmbeddedImageId * imageId{ std::get_if<db::TrackEmbeddedImageId>(&coverId.id) })
{
res = idToString(*imageId);
assert(!coverId.timestamp.has_value());
}
else if (db::ImageId * imageId{ std::get_if<db::ImageId>(&coverId.id) })
{
res = idToString(*imageId);
res += timestampSeparatorChar;
assert(coverId.timestamp.has_value());
res += std::to_string(*coverId.timestamp);
}
// produce "art-id-timestamp"
res = idToString(coverId.id);
res += timestampSeparatorChar;
res += std::to_string(coverId.timestamp);
assert(!res.empty());
return res;
@@ -74,30 +59,20 @@ namespace lms::core::stringUtils
std::optional<api::subsonic::CoverArtId> res;
std::vector<std::string_view> values{ core::stringUtils::splitString(str, '-') };
if (values.size() <= 1)
// expect "art-id-timestamp"
if (values.size() != 3)
return res;
if (values[0] != "art")
return res;
const auto value{ core::stringUtils::readAs<db::ArtworkId::ValueType>(values[1]) };
const auto timestamp{ core::stringUtils::readAs<std::time_t>(values[2]) };
if (!value || !timestamp)
return std::nullopt;
if (values[0] == "trim")
{
// expect "trim-id"
if (values.size() == 2)
{
if (const auto value{ core::stringUtils::readAs<db::TrackEmbeddedImageId::ValueType>(values[1]) })
res.emplace(db::TrackEmbeddedImageId{ *value });
}
}
else if (values[0] == "im")
{
// expect "im-id-timestamp"
if (values.size() == 3)
{
const auto imageId{ core::stringUtils::readAs<db::ImageId::ValueType>(values[1]) };
const auto timestamp{ core::stringUtils::readAs<std::time_t>(values[2]) };
if (imageId && timestamp)
res.emplace(db::ImageId{ *imageId }, *timestamp);
}
}
res.emplace(api::subsonic::CoverArtId{ *value, *timestamp });
return res;
}
+3 -14
View File
@@ -20,24 +20,16 @@
#pragma once
#include <ctime>
#include <variant>
#include "core/String.hpp"
#include "database/ImageId.hpp"
#include "database/TrackEmbeddedImageId.hpp"
#include "database/ArtworkId.hpp"
namespace lms::api::subsonic
{
struct CoverArtId
{
std::variant<db::ImageId, db::TrackEmbeddedImageId> id;
std::optional<std::time_t> timestamp;
CoverArtId(db::ImageId _id, std::time_t _timestamp)
: id{ _id }
, timestamp{ _timestamp } {}
CoverArtId(db::TrackEmbeddedImageId _id)
: id{ _id } {}
db::ArtworkId id;
std::time_t timestamp;
};
std::string idToString(CoverArtId coverId);
@@ -46,9 +38,6 @@ namespace lms::api::subsonic
// Used to parse parameters
namespace lms::core::stringUtils
{
template<>
std::optional<db::ImageId> readAs(std::string_view str);
template<>
std::optional<api::subsonic::CoverArtId> readAs(std::string_view str);
} // namespace lms::core::stringUtils
@@ -336,12 +336,7 @@ namespace lms::api::subsonic
if (size)
*size = core::utils::clamp(*size, std::size_t{ 32 }, std::size_t{ 2048 });
std::shared_ptr<image::IEncodedImage> image;
if (const db::TrackEmbeddedImageId * trackEmbeddedImageId{ std::get_if<db::TrackEmbeddedImageId>(&coverArtId.id) })
image = core::Service<artwork::IArtworkService>::get()->getTrackEmbeddedImage(*trackEmbeddedImageId, size);
else if (const db::ImageId * imageId{ std::get_if<db::ImageId>(&coverArtId.id) })
image = core::Service<artwork::IArtworkService>::get()->getImage(*imageId, size);
std::shared_ptr<image::IEncodedImage> image{ core::Service<artwork::IArtworkService>::get()->getImage(coverArtId.id, size) };
if (!image)
{
response.setStatus(404);
+5 -13
View File
@@ -23,14 +23,13 @@
#include "core/Service.hpp"
#include "core/String.hpp"
#include "database/Artist.hpp"
#include "database/Artwork.hpp"
#include "database/Cluster.hpp"
#include "database/Directory.hpp"
#include "database/Image.hpp"
#include "database/Release.hpp"
#include "database/Track.hpp"
#include "database/Types.hpp"
#include "database/User.hpp"
#include "services/artwork/IArtworkService.hpp"
#include "services/feedback/IFeedbackService.hpp"
#include "services/scrobbling/IScrobblingService.hpp"
@@ -88,19 +87,12 @@ namespace lms::api::subsonic
albumNode.setAttribute("created", core::stringUtils::toISO8601String(release->getAddedTime()));
if (const auto artwork{ release->getPreferredArtwork() })
{
const auto imageResult{ core::Service<artwork::IArtworkService>::get()->findReleaseImage(release->getId()) };
if (const db::ImageId * imageId{ std::get_if<db::ImageId>(&imageResult) })
{
if (const db::Image::pointer image{ db::Image::find(context.dbSession, *imageId) })
{
const CoverArtId coverArtId{ *imageId, image->getLastWriteTime().toTime_t() };
albumNode.setAttribute("coverArt", idToString(coverArtId));
}
}
else if (const db::TrackEmbeddedImageId * embeddedImageId{ std::get_if<db::TrackEmbeddedImageId>(&imageResult) })
albumNode.setAttribute("coverArt", idToString(*embeddedImageId));
CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() };
albumNode.setAttribute("coverArt", idToString(coverArtId));
}
if (const auto originalYear{ release->getOriginalYear() })
albumNode.setAttribute("year", *originalYear);
else if (const auto year{ release->getYear() })
+4 -13
View File
@@ -23,11 +23,10 @@
#include "core/Service.hpp"
#include "core/String.hpp"
#include "database/Artist.hpp"
#include "database/Image.hpp"
#include "database/Artwork.hpp"
#include "database/Release.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/User.hpp"
#include "services/artwork/IArtworkService.hpp"
#include "services/feedback/IFeedbackService.hpp"
#include "CoverArtId.hpp"
@@ -96,18 +95,10 @@ namespace lms::api::subsonic
artistNode.setAttribute("id", idToString(artist->getId()));
artistNode.setAttribute("name", artist->getName());
if (const auto artwork{ artist->getPreferredArtwork() })
{
const auto imageResult{ core::Service<artwork::IArtworkService>::get()->findArtistImage(artist->getId()) };
if (const db::ImageId * imageId{ std::get_if<db::ImageId>(&imageResult) })
{
if (const db::Image::pointer image{ db::Image::find(context.dbSession, *imageId) })
{
const CoverArtId coverArtId{ *imageId, image->getLastWriteTime().toTime_t() };
artistNode.setAttribute("coverArt", idToString(coverArtId));
}
}
else if (const db::TrackEmbeddedImageId * embeddedImageId{ std::get_if<db::TrackEmbeddedImageId>(&imageResult) })
artistNode.setAttribute("coverArt", idToString(*embeddedImageId));
CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() };
artistNode.setAttribute("coverArt", idToString(coverArtId));
}
const std::size_t count{ Release::getCount(context.dbSession, Release::FindParameters{}.setArtist(artist->getId())) };
+5 -10
View File
@@ -21,7 +21,7 @@
#include "core/Service.hpp"
#include "core/String.hpp"
#include "database/Image.hpp"
#include "database/Artwork.hpp"
#include "database/TrackList.hpp"
#include "database/User.hpp"
#include "services/artwork/IArtworkService.hpp"
@@ -46,18 +46,13 @@ namespace lms::api::subsonic
if (const db::User::pointer user{ tracklist->getUser() })
playlistNode.setAttribute("owner", user->getLoginName());
if (const db::ArtworkId artworkId{ core::Service<artwork::IArtworkService>::get()->findTrackListImage(tracklist->getId()) }; artworkId.isValid())
{
const auto imageResult{ core::Service<artwork::IArtworkService>::get()->findTrackListImage(tracklist->getId()) };
if (const db::ImageId * imageId{ std::get_if<db::ImageId>(&imageResult) })
if (const auto artwork{ db::Artwork::find(context.dbSession, artworkId) })
{
if (const db::Image::pointer image{ db::Image::find(context.dbSession, *imageId) })
{
const CoverArtId coverArtId{ *imageId, image->getLastWriteTime().toTime_t() };
playlistNode.setAttribute("coverArt", idToString(coverArtId));
}
CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() };
playlistNode.setAttribute("coverArt", idToString(coverArtId));
}
else if (const db::TrackEmbeddedImageId * embeddedImageId{ std::get_if<db::TrackEmbeddedImageId>(&imageResult) })
playlistNode.setAttribute("coverArt", idToString(*embeddedImageId));
}
return playlistNode;
+8 -14
View File
@@ -21,21 +21,19 @@
#include <string_view>
#include "av/IAudioFile.hpp"
#include "core/ITraceLogger.hpp"
#include "core/MimeTypes.hpp"
#include "core/Service.hpp"
#include "core/String.hpp"
#include "database/Artist.hpp"
#include "database/Artwork.hpp"
#include "database/Cluster.hpp"
#include "database/Directory.hpp"
#include "database/Image.hpp"
#include "database/Release.hpp"
#include "database/Track.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/Types.hpp"
#include "database/User.hpp"
#include "services/artwork/IArtworkService.hpp"
#include "services/feedback/IFeedbackService.hpp"
#include "services/scrobbling/IScrobblingService.hpp"
@@ -111,18 +109,14 @@ namespace lms::api::subsonic
trackResponse.setAttribute("transcodedContentType", core::getMimeType(std::filesystem::path{ "." + fileSuffix }));
}
auto artwork{ track->getPreferredMediaArtwork() };
if (!artwork)
artwork = track->getPreferredArtwork();
if (artwork)
{
const auto imageResult{ core::Service<artwork::IArtworkService>::get()->findTrackImage(track->getId()) };
if (const db::ImageId * imageId{ std::get_if<db::ImageId>(&imageResult) })
{
if (const db::Image::pointer image{ db::Image::find(context.dbSession, *imageId) })
{
const CoverArtId coverArtId{ *imageId, image->getLastWriteTime().toTime_t() };
trackResponse.setAttribute("coverArt", idToString(coverArtId));
}
}
else if (const db::TrackEmbeddedImageId * embeddedImageId{ std::get_if<db::TrackEmbeddedImageId>(&imageResult) })
trackResponse.setAttribute("coverArt", idToString(*embeddedImageId));
CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() };
trackResponse.setAttribute("coverArt", idToString(coverArtId));
}
const std::vector<Artist::pointer>& artists{ track->getArtists({ TrackArtistLinkType::Artist }) };