Deduplicate embedded images, ref #628

This commit is contained in:
emeric
2025-03-15 14:48:37 +01:00
parent 744abefc8b
commit 05000d8a1a
65 changed files with 1307 additions and 319 deletions
@@ -25,6 +25,7 @@
#include "core/PartialDateTime.hpp"
#include "core/Path.hpp"
#include "core/Service.hpp"
#include "core/XxHash3.hpp"
#include "database/Artist.hpp"
#include "database/Cluster.hpp"
#include "database/Db.hpp"
@@ -34,6 +35,8 @@
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/TrackEmbeddedImage.hpp"
#include "database/TrackEmbeddedImageLink.hpp"
#include "database/TrackFeatures.hpp"
#include "database/TrackLyrics.hpp"
#include "database/Types.hpp"
@@ -330,6 +333,117 @@ namespace lms::scanner
return lyrics;
}
struct ImageInfo
{
std::size_t index;
metadata::Image::Type type{ metadata::Image::Type::Unknown };
std::uint64_t hash{};
std::size_t size{};
image::ImageProperties properties;
std::string mimeType;
std::string description;
};
db::ImageType convertImageType(metadata::Image::Type type)
{
switch (type)
{
case metadata::Image::Type::Unknown:
return db::ImageType::Unknown;
case metadata::Image::Type::Other:
return db::ImageType::Other;
case metadata::Image::Type::FileIcon:
return db::ImageType::FileIcon;
case metadata::Image::Type::OtherFileIcon:
return db::ImageType::OtherFileIcon;
case metadata::Image::Type::FrontCover:
return db::ImageType::FrontCover;
case metadata::Image::Type::BackCover:
return db::ImageType::BackCover;
case metadata::Image::Type::LeafletPage:
return db::ImageType::LeafletPage;
case metadata::Image::Type::Media:
return db::ImageType::Media;
case metadata::Image::Type::LeadArtist:
return db::ImageType::LeadArtist;
case metadata::Image::Type::Artist:
return db::ImageType::Artist;
case metadata::Image::Type::Conductor:
return db::ImageType::Conductor;
case metadata::Image::Type::Band:
return db::ImageType::Band;
case metadata::Image::Type::Composer:
return db::ImageType::Composer;
case metadata::Image::Type::Lyricist:
return db::ImageType::Lyricist;
case metadata::Image::Type::RecordingLocation:
return db::ImageType::RecordingLocation;
case metadata::Image::Type::DuringRecording:
return db::ImageType::DuringRecording;
case metadata::Image::Type::DuringPerformance:
return db::ImageType::DuringPerformance;
case metadata::Image::Type::MovieScreenCapture:
return db::ImageType::MovieScreenCapture;
case metadata::Image::Type::ColouredFish:
return db::ImageType::ColouredFish;
case metadata::Image::Type::Illustration:
return db::ImageType::Illustration;
case metadata::Image::Type::BandLogo:
return db::ImageType::BandLogo;
case metadata::Image::Type::PublisherLogo:
return db::ImageType::PublisherLogo;
}
}
db::TrackEmbeddedImage::pointer getOrCreateTrackEmbeddedImage(db::Session& session, const ImageInfo& imageInfo)
{
db::TrackEmbeddedImage::pointer image{ db::TrackEmbeddedImage::find(session, imageInfo.size, db::ImageHashType{ imageInfo.hash }) };
if (!image)
{
image = session.create<db::TrackEmbeddedImage>();
image.modify()->setSize(imageInfo.size);
image.modify()->setHash(db::ImageHashType{ imageInfo.hash });
image.modify()->setWidth(imageInfo.properties.width);
image.modify()->setHeight(imageInfo.properties.height);
image.modify()->setMimeType(imageInfo.mimeType);
}
return image;
}
db::TrackEmbeddedImageLink::pointer createTrackEmbeddedImageLink(db::Session& session, const db::Track::pointer& track, const ImageInfo& imageInfo)
{
const db::TrackEmbeddedImage::pointer image{ getOrCreateTrackEmbeddedImage(session, imageInfo) };
db::TrackEmbeddedImageLink::pointer imageLink{ session.create<db::TrackEmbeddedImageLink>(track, image) };
imageLink.modify()->setIndex(imageInfo.index);
imageLink.modify()->setType(convertImageType(imageInfo.type));
imageLink.modify()->setDescription(imageInfo.description);
return imageLink;
}
void updateEmbeddedImages(db::Session& session, db::Track::pointer& track, std::span<const ImageInfo> images)
{
db::TrackEmbeddedImageLink::pointer preferredImageLink;
track.modify()->clearEmbeddedImageLinks();
for (const ImageInfo& imageInfo : images)
{
db::TrackEmbeddedImageLink::pointer link{ createTrackEmbeddedImageLink(session, track, imageInfo) };
track.modify()->addEmbeddedImageLink(link);
if (!preferredImageLink
|| (preferredImageLink->getType() != db::ImageType::FrontCover && link->getType() == db::ImageType::FrontCover)
|| (preferredImageLink->getImage()->getSize() < link->getImage()->getSize()))
{
preferredImageLink = link;
}
}
if (preferredImageLink)
preferredImageLink.modify()->setIsPreferred(true);
}
db::Advisory getAdvisory(std::optional<metadata::Track::Advisory> advisory)
{
if (!advisory)
@@ -348,7 +462,7 @@ namespace lms::scanner
return db::Advisory::UnSet;
}
db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const metadata::Track& parsedTrack, bool hasEmbeddedCover, const FileInfo& fileInfo)
db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const metadata::Track& parsedTrack, const FileInfo& fileInfo)
{
db::Track::FindParameters params;
// Add as many fields as possible to limit errors
@@ -362,7 +476,6 @@ namespace lms::scanner
}
if (parsedTrack.position)
params.setTrackNumber(*parsedTrack.position);
params.setHasEmbeddedImage(hasEmbeddedCover);
params.setFileSize(fileInfo.fileSize);
bool error{};
@@ -414,7 +527,7 @@ namespace lms::scanner
metadata::IAudioFileParser& _parser;
const ScannerSettings& _settings;
std::unique_ptr<metadata::Track> _parsedTrack;
bool _hasEmbeddedCover{};
std::vector<ImageInfo> _parsedImages;
};
void AudioFileScanOperation::scan()
@@ -426,16 +539,32 @@ namespace lms::scanner
{
_parsedTrack = _parser.parseMetaData(_file);
std::size_t index{};
_parser.parseImages(_file, [&](const metadata::Image& image) {
try
{
image::probeImage(image.data);
_hasEmbeddedCover = true;
image::ImageProperties properties{ image::probeImage(image.data) };
ImageInfo info;
info.index = index;
info.type = image.type;
{
LMS_SCOPED_TRACE_DETAILED("Scanner", "ImageHash");
info.hash = core::xxHash3_64(image.data);
}
info.size = image.data.size();
info.mimeType = image.mimeType;
info.description = image.description;
info.properties = properties;
_parsedImages.push_back(std::move(info));
}
catch (const image::Exception& e)
{
LMS_LOG(DBUPDATER, ERROR, "Failed to parse image in track file " << _file);
}
index++;
});
}
catch (const metadata::Exception& e)
@@ -521,7 +650,7 @@ namespace lms::scanner
if (!track)
{
// maybe the file just moved?
track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, _hasEmbeddedCover, *fileInfo);
track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, *fileInfo);
if (track)
{
LMS_LOG(DBUPDATER, DEBUG, "Considering track " << _file << " moved from " << track->getAbsoluteFilePath());
@@ -662,7 +791,6 @@ namespace lms::scanner
track.modify()->setTrackMBID(_parsedTrack->mbid);
if (auto trackFeatures{ db::TrackFeatures::find(dbSession, track->getId()) })
trackFeatures.remove(); // TODO: only if MBID changed?
track.modify()->setHasCover(_hasEmbeddedCover);
track.modify()->setCopyright(_parsedTrack->copyright);
track.modify()->setCopyrightURL(_parsedTrack->copyrightURL);
track.modify()->setAdvisory(getAdvisory(_parsedTrack->advisory));
@@ -672,10 +800,9 @@ namespace lms::scanner
track.modify()->clearEmbeddedLyrics();
for (const metadata::Lyrics& lyricsInfo : _parsedTrack->lyrics)
{
db::TrackLyrics::pointer lyrics{ createLyrics(dbSession, lyricsInfo) };
track.modify()->addLyrics(lyrics);
}
track.modify()->addLyrics(createLyrics(dbSession, lyricsInfo));
updateEmbeddedImages(dbSession, track, _parsedImages);
if (added)
{