Deduplicate embedded images, ref #628
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user