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
+41
View File
@@ -0,0 +1,41 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
namespace lms::core
{
template<typename Tag, typename T>
class TaggedType
{
public:
using underlying_type = T;
explicit constexpr TaggedType() = default;
explicit constexpr TaggedType(T value)
: _value{ value } {}
constexpr T value() const { return _value; }
auto operator<=>(const TaggedType&) const = default;
private:
T _value{};
};
} // namespace lms::core
+2
View File
@@ -25,6 +25,8 @@ add_library(lmsdatabase STATIC
impl/SqlQuery.cpp
impl/Track.cpp
impl/TrackBookmark.cpp
impl/TrackEmbeddedImage.cpp
impl/TrackEmbeddedImageLink.cpp
impl/TrackLyrics.cpp
impl/Types.cpp
impl/UIState.cpp
+1 -1
View File
@@ -29,9 +29,9 @@
#include "database/Track.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "SqlQuery.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
+2 -2
View File
@@ -24,9 +24,9 @@
#include "database/Session.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/StringViewTraits.hpp"
namespace lms::db
{
+2 -2
View File
@@ -27,10 +27,10 @@
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "IdTypeTraits.hpp"
#include "SqlQuery.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/StringViewTraits.hpp"
namespace lms::db
{
+2 -2
View File
@@ -23,9 +23,9 @@
#include "database/Session.hpp"
#include "database/Types.hpp"
#include "IdTypeTraits.hpp"
#include "PathTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/PathTraits.hpp"
namespace lms::db
{
+2 -2
View File
@@ -26,9 +26,9 @@
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "IdTypeTraits.hpp"
#include "PathTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/PathTraits.hpp"
namespace lms::db
{
+1 -1
View File
@@ -22,9 +22,9 @@
#include "database/Track.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "SqlQuery.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
+3 -3
View File
@@ -23,10 +23,10 @@
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "IdTypeTraits.hpp"
#include "PathTraits.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/PathTraits.hpp"
#include "traits/StringViewTraits.hpp"
namespace lms::db
{
+34 -2
View File
@@ -35,7 +35,7 @@ namespace lms::db
{
namespace
{
static constexpr Version LMS_DATABASE_VERSION{ 83 };
static constexpr Version LMS_DATABASE_VERSION{ 84 };
}
VersionInfo::VersionInfo()
@@ -1101,11 +1101,42 @@ FROM tracklist)");
void migrateFromV82(Session& session)
{
// new setting to display inline artist relationships in the release view
// New setting to display inline artist relationships in the release view
utils::executeCommand(*session.getDboSession(), "ALTER TABLE user ADD COLUMN ui_enable_inline_artist_relationships BOOLEAN NOT NULL DEFAULT(false)");
utils::executeCommand(*session.getDboSession(), "ALTER TABLE user ADD COLUMN ui_inline_artist_relationships BIGINT NOT NULL DEFAULT(68)"); // Composer + Performer
}
void migrateFromV83(Session& session)
{
// New embedded track image handling
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track DROP COLUMN has_cover");
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_embedded_image" (
"id" integer primary key autoincrement,
"version" integer not null,
"hash" text not null,
"size" integer not null,
"width" integer not null,
"height" integer not null,
"mime_type" text not null
))");
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_embedded_image_link" (
"id" integer primary key autoincrement,
"version" integer not null,
"index" integer not null,
"is_preferred" boolean not null,
"type" integer not null,
"description" text not null,
"track_id" bigint,
"track_embedded_image_id" bigint,
constraint "fk_track_embedded_image_link_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
constraint "fk_track_embedded_image_link_track_embedded_image" foreign key ("track_embedded_image_id") references "track_embedded_image" ("id") on delete cascade deferrable initially deferred
))");
// Just increment the scan version of the settings to make the next scan rescan everything
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
}
bool doDbMigration(Session& session)
{
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
@@ -1165,6 +1196,7 @@ FROM tracklist)");
{ 80, migrateFromV80 },
{ 81, migrateFromV81 },
{ 82, migrateFromV82 },
{ 83, migrateFromV83 },
};
bool migrationPerformed{};
+3 -3
View File
@@ -28,10 +28,10 @@
#include "database/TrackList.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "PathTraits.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/PathTraits.hpp"
#include "traits/StringViewTraits.hpp"
namespace lms::db
{
+2 -2
View File
@@ -28,9 +28,9 @@
#include "database/Track.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/StringViewTraits.hpp"
namespace lms::db
{
+1 -1
View File
@@ -25,8 +25,8 @@
#include "database/Session.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
+1 -1
View File
@@ -25,8 +25,8 @@
#include "database/Session.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
+1 -1
View File
@@ -25,8 +25,8 @@
#include "database/Track.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
+4 -4
View File
@@ -31,12 +31,12 @@
#include "database/Types.hpp"
#include "database/User.hpp"
#include "EnumSetTraits.hpp"
#include "IdTypeTraits.hpp"
#include "PartialDateTimeTraits.hpp"
#include "SqlQuery.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
#include "traits/EnumSetTraits.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/PartialDateTimeTraits.hpp"
#include "traits/StringViewTraits.hpp"
namespace lms::db
{
+15 -3
View File
@@ -43,6 +43,8 @@
#include "database/Track.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/TrackBookmark.hpp"
#include "database/TrackEmbeddedImage.hpp"
#include "database/TrackEmbeddedImageLink.hpp"
#include "database/TrackFeatures.hpp"
#include "database/TrackList.hpp"
#include "database/TrackLyrics.hpp"
@@ -50,11 +52,12 @@
#include "database/UIState.hpp"
#include "database/User.hpp"
#include "EnumSetTraits.hpp"
#include "Migration.hpp"
#include "PartialDateTimeTraits.hpp"
#include "PathTraits.hpp"
#include "Utils.hpp"
#include "traits/EnumSetTraits.hpp"
#include "traits/ImageHashTypeTraits.hpp"
#include "traits/PartialDateTimeTraits.hpp"
#include "traits/PathTraits.hpp"
namespace lms::db
{
@@ -122,6 +125,8 @@ namespace lms::db
_session.mapClass<Track>("track");
_session.mapClass<TrackBookmark>("track_bookmark");
_session.mapClass<TrackArtistLink>("track_artist_link");
_session.mapClass<TrackEmbeddedImage>("track_embedded_image");
_session.mapClass<TrackEmbeddedImageLink>("track_embedded_image_link");
_session.mapClass<TrackFeatures>("track_features");
_session.mapClass<TrackList>("tracklist");
_session.mapClass<TrackListEntry>("tracklist_entry");
@@ -209,6 +214,13 @@ namespace lms::db
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_media_library_idx ON directory(media_library_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_name_idx ON directory(name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_id_idx ON track_embedded_image(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_hash_idx ON track_embedded_image(hash)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_id_idx ON track_embedded_image_link(track_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_embedded_image_id_track_id_idx ON track_embedded_image_link(track_embedded_image_id, track_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_is_preferred_track_id_track_embedded_image_id_idx ON track_embedded_image_link(is_preferred, track_id, track_embedded_image_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_directory_stem_idx ON image(directory_id, stem COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_id_idx ON image(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_path_idx ON image(absolute_file_path)");
+1 -1
View File
@@ -25,8 +25,8 @@
#include "database/Session.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
+1 -1
View File
@@ -25,8 +25,8 @@
#include "database/Session.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
+1 -1
View File
@@ -25,8 +25,8 @@
#include "database/Track.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
+22 -7
View File
@@ -29,16 +29,18 @@
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/TrackEmbeddedImage.hpp"
#include "database/TrackEmbeddedImageLink.hpp"
#include "database/TrackFeatures.hpp"
#include "database/TrackLyrics.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "PartialDateTimeTraits.hpp"
#include "PathTraits.hpp"
#include "SqlQuery.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/PartialDateTimeTraits.hpp"
#include "traits/PathTraits.hpp"
#include "traits/StringViewTraits.hpp"
namespace lms::db
{
@@ -177,12 +179,15 @@ namespace lms::db
if (params.directory.isValid())
query.where("t.directory_id = ?").bind(params.directory);
if (params.hasEmbeddedImage.has_value())
query.where("t.has_cover = ?").bind(params.hasEmbeddedImage.value());
if (params.fileSize.has_value())
query.where("t.file_size = ?").bind(static_cast<long long>(params.fileSize.value()));
if (params.embeddedImageId.isValid())
{
query.join("track_embedded_image_link t_e_i_l ON t_e_i_l.track_id = t.id");
query.where("t_e_i_l.track_embedded_image_id = ?").bind(params.embeddedImageId);
}
switch (params.sortMethod)
{
case TrackSortMethod::None:
@@ -469,6 +474,16 @@ namespace lms::db
_trackLyrics.insert(getDboPtr(lyrics));
}
void Track::clearEmbeddedImageLinks()
{
_embeddedImageLinks.clear();
}
void Track::addEmbeddedImageLink(const ObjectPtr<TrackEmbeddedImageLink>& image)
{
_embeddedImageLinks.insert(getDboPtr(image));
}
std::optional<int> Track::getYear() const
{
return _date.getYear();
+1 -1
View File
@@ -23,8 +23,8 @@
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
+1 -1
View File
@@ -23,8 +23,8 @@
#include "database/Track.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
@@ -0,0 +1,124 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "database/Directory.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/TrackEmbeddedImage.hpp"
#include "database/Types.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/ImageHashTypeTraits.hpp"
namespace lms::db
{
namespace
{
Wt::Dbo::Query<Wt::Dbo::ptr<TrackEmbeddedImage>> createQuery(Session& session, const TrackEmbeddedImage::FindParameters& params)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<TrackEmbeddedImage>>("SELECT t_e_i FROM track_embedded_image t_e_i") };
if (params.isPreferred
|| params.track.isValid()
|| params.release.isValid()
|| params.trackList.isValid()
|| params.sortMethod == TrackEmbeddedImageSortMethod::FrontCoverAndSize)
{
query.join("track_embedded_image_link t_e_i_l ON t_e_i_l.track_embedded_image_id = t_e_i.id");
if (params.isPreferred)
query.where("t_e_i_l.is_preferred = ?").bind(params.isPreferred.value());
if (params.track.isValid())
query.where("t_e_i_l.track_id = ?").bind(params.track);
if (params.release.isValid())
{
query.join("track t ON t_e_i_l.track_id = t.id");
query.where("t.release_id = ?").bind(params.release);
}
if (params.trackList.isValid())
{
query.join("tracklist_entry t_l_e ON t_l_e.track_id = t_e_i_l.track_id");
query.where("t_l_e.tracklist_id = ?").bind(params.trackList);
}
}
switch (params.sortMethod)
{
case TrackEmbeddedImageSortMethod::None:
break;
case TrackEmbeddedImageSortMethod::FrontCoverAndSize:
query.orderBy("CASE WHEN t_e_i_l.type = ? THEN 0 ELSE 1 END, t_e_i.size").bind(ImageType::FrontCover);
break;
}
return query;
}
} // namespace
TrackEmbeddedImage::pointer TrackEmbeddedImage::create(Session& session)
{
return session.getDboSession()->add(std::unique_ptr<TrackEmbeddedImage>{ new TrackEmbeddedImage{} });
}
std::size_t TrackEmbeddedImage::getCount(Session& session)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM track_embedded_image"));
}
TrackEmbeddedImage::pointer TrackEmbeddedImage::find(Session& session, TrackEmbeddedImageId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackEmbeddedImage>().where("id = ?").bind(id));
}
void TrackEmbeddedImage::find(Session& session, const FindParameters& params, const std::function<void(const pointer&)>& func)
{
session.checkReadTransaction();
auto query{ createQuery(session, params) };
utils::forEachQueryRangeResult(query, params.range, func);
}
TrackEmbeddedImage::pointer TrackEmbeddedImage::find(Session& session, std::size_t size, ImageHashType hash)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->find<TrackEmbeddedImage>().where("size = ?").bind(static_cast<int>(size)).where("hash = ?").bind(hash) };
return utils::fetchQuerySingleResult(query);
}
RangeResults<TrackEmbeddedImageId> TrackEmbeddedImage::findOrphanIds(Session& session, std::optional<Range> range)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<TrackEmbeddedImageId>("SELECT t_e_i.id FROM track_embedded_image t_e_i LEFT JOIN track_embedded_image_link t_e_i_l ON t_e_i.id = t_e_i_l.track_embedded_image_id WHERE t_e_i_l.track_embedded_image_id IS NULL") };
return utils::execRangeQuery<TrackEmbeddedImageId>(query, range);
}
} // namespace lms::db
@@ -0,0 +1,66 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "database/TrackEmbeddedImageLink.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/TrackEmbeddedImage.hpp"
#include "database/Types.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
TrackEmbeddedImageLink::TrackEmbeddedImageLink(ObjectPtr<Track> track, ObjectPtr<TrackEmbeddedImage> image)
: _track{ getDboPtr(track) }
, _image{ getDboPtr(image) }
{
}
TrackEmbeddedImageLink::pointer TrackEmbeddedImageLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackEmbeddedImage> image)
{
return session.getDboSession()->add(std::unique_ptr<TrackEmbeddedImageLink>{ new TrackEmbeddedImageLink{ track, image } });
}
std::size_t TrackEmbeddedImageLink::getCount(Session& session)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM track_embedded_image_link"));
}
TrackEmbeddedImageLink::pointer TrackEmbeddedImageLink::find(Session& session, TrackEmbeddedImageLinkId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackEmbeddedImageLink>().where("id = ?").bind(id));
}
ObjectPtr<Track> TrackEmbeddedImageLink::getTrack() const
{
return _track;
}
ObjectPtr<TrackEmbeddedImage> TrackEmbeddedImageLink::getImage() const
{
return _image;
}
} // namespace lms::db
+1 -1
View File
@@ -27,8 +27,8 @@
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
+2 -2
View File
@@ -26,10 +26,10 @@
#include "database/Track.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "SqlQuery.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/StringViewTraits.hpp"
namespace lms::db
{
+2 -2
View File
@@ -28,9 +28,9 @@
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "IdTypeTraits.hpp"
#include "PathTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/PathTraits.hpp"
namespace lms::db
{
+2 -3
View File
@@ -19,13 +19,12 @@
#include "database/UIState.hpp"
#include "core/ILogger.hpp"
#include "database/Session.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/StringViewTraits.hpp"
namespace lms::db
{
+3 -3
View File
@@ -27,10 +27,10 @@
#include "database/Track.hpp"
#include "database/UIState.hpp"
#include "EnumSetTraits.hpp"
#include "IdTypeTraits.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
#include "traits/EnumSetTraits.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/StringViewTraits.hpp"
namespace lms::db
{
@@ -0,0 +1,62 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/Dbo/StdSqlTraits.h>
#include "core/String.hpp"
#include "database/Types.hpp"
namespace Wt::Dbo
{
template<>
struct sql_value_traits<lms::db::ImageHashType>
{
static const bool specialized = true;
// Uses an underlying string to encode this big value
static std::string type(SqlConnection* conn, int size)
{
return sql_value_traits<std::string, void>::type(conn, size);
}
static void bind(const lms::db::ImageHashType& v, SqlStatement* statement, int column, int size)
{
std::string valueAsStr{ std::to_string(v.value()) };
sql_value_traits<std::string>::bind(valueAsStr, statement, column, size);
}
static bool read(lms::db::ImageHashType& v, SqlStatement* statement, int column, int size)
{
std::string valueAsStr;
if (sql_value_traits<std::string>::read(valueAsStr, statement, column, size))
{
if (const auto parsedValue{ lms::core::stringUtils::readAs<lms::db::ImageHashType::underlying_type>(valueAsStr) })
{
v = lms::db::ImageHashType{ *parsedValue };
return true;
}
}
v = lms::db::ImageHashType{};
return false;
}
};
} // namespace Wt::Dbo
+12 -10
View File
@@ -41,6 +41,7 @@
#include "database/MediaLibraryId.hpp"
#include "database/Object.hpp"
#include "database/ReleaseId.hpp"
#include "database/TrackEmbeddedImageId.hpp"
#include "database/TrackId.hpp"
#include "database/TrackListId.hpp"
#include "database/Types.hpp"
@@ -52,6 +53,7 @@ namespace lms::db
class Cluster;
class ClusterType;
class Directory;
class TrackEmbeddedImageLink;
class MediaLibrary;
class Release;
class Session;
@@ -85,8 +87,8 @@ namespace lms::db
std::optional<int> trackNumber; // matching this track number
std::optional<int> discNumber; // matching this disc number
DirectoryId directory; // if set, tracks in this directory
std::optional<bool> hasEmbeddedImage; // if set, tracks that have or not embedded images
std::optional<std::size_t> fileSize; // if set, tracks that match this file size
TrackEmbeddedImageId embeddedImageId; // if set, tracks that have this embedded image
FindParameters& setFilters(const Filters& _filters)
{
@@ -182,16 +184,16 @@ namespace lms::db
directory = _directory;
return *this;
}
FindParameters& setHasEmbeddedImage(std::optional<bool> _hasEmbeddedImage)
{
hasEmbeddedImage = _hasEmbeddedImage;
return *this;
}
FindParameters& setFileSize(std::optional<std::size_t> _fileSize)
{
fileSize = _fileSize;
return *this;
}
FindParameters& setEmbeddedImage(TrackEmbeddedImageId _embeddedImageId)
{
embeddedImageId = _embeddedImageId;
return *this;
}
};
Track() = default;
@@ -232,7 +234,6 @@ namespace lms::db
void setSampleRate(std::size_t channelCount) { _sampleRate = channelCount; }
void setDate(const core::PartialDateTime& date) { _date = date; }
void setOriginalDate(const core::PartialDateTime& date) { _originalDate = date; }
void setHasCover(bool hasCover) { _hasCover = hasCover; }
void setTrackMBID(const std::optional<core::UUID>& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; }
void setRecordingMBID(const std::optional<core::UUID>& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; }
void setCopyright(std::string_view copyright);
@@ -249,6 +250,8 @@ namespace lms::db
void clearLyrics();
void clearEmbeddedLyrics();
void addLyrics(const ObjectPtr<TrackLyrics>& lyrics);
void clearEmbeddedImageLinks();
void addEmbeddedImageLink(const ObjectPtr<TrackEmbeddedImageLink>& link);
void setMediaLibrary(ObjectPtr<MediaLibrary> mediaLibrary) { _mediaLibrary = getDboPtr(mediaLibrary); }
void setDirectory(ObjectPtr<Directory> directory) { _directory = getDboPtr(directory); }
@@ -273,7 +276,6 @@ namespace lms::db
std::optional<int> getOriginalYear() const;
const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; }
const Wt::WDateTime& getAddedTime() const { return _fileAdded; }
bool hasCover() const { return _hasCover; }
bool hasLyrics() const;
std::optional<core::UUID> getTrackMBID() const { return core::UUID::fromString(_trackMBID); }
std::optional<core::UUID> getRecordingMBID() const { return core::UUID::fromString(_recordingMBID); }
@@ -321,7 +323,6 @@ namespace lms::db
Wt::Dbo::field(a, _fileSize, "file_size");
Wt::Dbo::field(a, _fileLastWrite, "file_last_write");
Wt::Dbo::field(a, _fileAdded, "file_added");
Wt::Dbo::field(a, _hasCover, "has_cover");
Wt::Dbo::field(a, _trackMBID, "mbid");
Wt::Dbo::field(a, _recordingMBID, "recording_mbid");
Wt::Dbo::field(a, _copyright, "copyright");
@@ -338,6 +339,7 @@ namespace lms::db
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _trackLyrics, Wt::Dbo::ManyToOne, "track");
Wt::Dbo::hasMany(a, _embeddedImageLinks, Wt::Dbo::ManyToOne, "track");
}
private:
@@ -368,7 +370,6 @@ namespace lms::db
long long _fileSize{};
Wt::WDateTime _fileLastWrite;
Wt::WDateTime _fileAdded;
bool _hasCover{};
std::string _trackMBID;
std::string _recordingMBID;
std::string _copyright;
@@ -384,6 +385,7 @@ namespace lms::db
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackLyrics>> _trackLyrics;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackEmbeddedImageLink>> _embeddedImageLinks;
};
namespace Debug
@@ -0,0 +1,133 @@
/*
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include <string>
#include <string_view>
#include <Wt/Dbo/Dbo.h>
#include "database/Object.hpp"
#include "database/ReleaseId.hpp"
#include "database/TrackEmbeddedImageId.hpp"
#include "database/TrackId.hpp"
#include "database/TrackListId.hpp"
#include "database/Types.hpp"
namespace lms::db
{
class TrackEmbeddedImageLink;
class Session;
class Track;
class TrackEmbeddedImage final : public Object<TrackEmbeddedImage, TrackEmbeddedImageId>
{
public:
TrackEmbeddedImage() = default;
struct FindParameters
{
std::optional<Range> range;
TrackId track;
ReleaseId release;
TrackListId trackList;
std::optional<bool> isPreferred;
TrackEmbeddedImageSortMethod sortMethod{ TrackEmbeddedImageSortMethod::None };
FindParameters& setRange(std::optional<Range> _range)
{
range = _range;
return *this;
}
FindParameters& setTrack(TrackId _track)
{
track = _track;
return *this;
}
FindParameters& setRelease(ReleaseId _release)
{
release = _release;
return *this;
}
FindParameters& setTrackList(TrackListId _trackList)
{
trackList = _trackList;
return *this;
}
FindParameters& setIsPreferred(std::optional<bool> _isPreferred)
{
isPreferred = _isPreferred;
return *this;
}
FindParameters& setSortMethod(TrackEmbeddedImageSortMethod _sortMethod)
{
sortMethod = _sortMethod;
return *this;
}
};
// find
static std::size_t getCount(Session& session);
static pointer find(Session& session, TrackEmbeddedImageId id);
static void find(Session& session, const FindParameters& params, const std::function<void(const pointer&)>& func);
static pointer find(Session& session, std::size_t size, ImageHashType hash);
static RangeResults<TrackEmbeddedImageId> findOrphanIds(Session& session, std::optional<Range> range);
// getters
ImageHashType getHash() const { return _hash; }
std::size_t getSize() const { return _size; }
std::size_t getWidth() const { return _width; }
std::size_t getHeight() const { return _height; }
std::string_view getMimeType() const { return _mimeType; }
// setters
void setHash(ImageHashType hash) { _hash = hash; }
void setSize(std::size_t size) { _size = static_cast<int>(size); }
void setWidth(std::size_t width) { _width = static_cast<int>(width); }
void setHeight(std::size_t height) { _height = static_cast<int>(height); }
void setMimeType(std::string_view mimeType) { _mimeType = mimeType; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _hash, "hash");
Wt::Dbo::field(a, _size, "size");
Wt::Dbo::field(a, _width, "width");
Wt::Dbo::field(a, _height, "height");
Wt::Dbo::field(a, _mimeType, "mime_type");
Wt::Dbo::hasMany(a, _links, Wt::Dbo::ManyToOne, "track_embedded_image");
}
private:
friend class Session;
static pointer create(Session& session);
ImageHashType _hash;
int _size{};
int _width{};
int _height{};
std::string _mimeType;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackEmbeddedImageLink>> _links;
};
} // namespace lms::db
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2022 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "database/IdType.hpp"
LMS_DECLARE_IDTYPE(TrackEmbeddedImageId)
@@ -0,0 +1,88 @@
/*
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string>
#include <string_view>
#include <Wt/Dbo/Dbo.h>
#include "database/Object.hpp"
#include "database/TrackEmbeddedImageLinkId.hpp"
#include "database/Types.hpp"
namespace lms::db
{
class TrackEmbeddedImage;
class Session;
class Track;
class TrackEmbeddedImageLink final : public Object<TrackEmbeddedImageLink, TrackEmbeddedImageLinkId>
{
public:
TrackEmbeddedImageLink() = default;
using HashType = std::uint64_t;
// find
static std::size_t getCount(Session& session);
static pointer find(Session& session, TrackEmbeddedImageLinkId id);
// getters
ObjectPtr<Track> getTrack() const;
ObjectPtr<TrackEmbeddedImage> getImage() const;
std::size_t getIndex() const { return _index; }
bool isPreferred() const { return _isPreferred; }
ImageType getType() const { return _type; }
std::string_view getDescription() const { return _description; }
// setters
void setIndex(std::size_t index) { _index = static_cast<int>(index); }
void setIsPreferred(bool isPreferred) { _isPreferred = isPreferred; }
void setType(ImageType type) { _type = type; }
void setDescription(std::string_view description) { _description = description; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _index, "index");
Wt::Dbo::field(a, _isPreferred, "is_preferred");
Wt::Dbo::field(a, _type, "type");
Wt::Dbo::field(a, _description, "description");
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _image, "track_embedded_image", Wt::Dbo::OnDeleteCascade);
}
private:
TrackEmbeddedImageLink(ObjectPtr<Track> track, ObjectPtr<TrackEmbeddedImage> image);
friend class Session;
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackEmbeddedImage> image);
int _index{}; // index within the track
bool _isPreferred{};
ImageType _type{ ImageType::Unknown };
std::string _description;
Wt::Dbo::ptr<Track> _track;
Wt::Dbo::ptr<TrackEmbeddedImage> _image;
};
} // namespace lms::db
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2022 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "database/IdType.hpp"
LMS_DECLARE_IDTYPE(TrackEmbeddedImageLinkId)
@@ -26,6 +26,7 @@
#include <Wt/WDate.h>
#include "core/Exception.hpp"
#include "core/TaggedType.hpp"
namespace lms::db
{
@@ -136,6 +137,8 @@ namespace lms::db
Name,
};
using ImageHashType = core::TaggedType<class ImageHash, std::uint64_t>;
enum class LabelSortMethod
{
None,
@@ -165,6 +168,12 @@ namespace lms::db
Name,
};
enum class TrackEmbeddedImageSortMethod
{
None,
FrontCoverAndSize,
};
enum class TrackListSortMethod
{
None,
@@ -194,6 +203,32 @@ namespace lms::db
EmbeddedFirst,
};
enum class ImageType
{
Unknown = 0,
Other = 1,
FileIcon = 2,
OtherFileIcon = 3,
FrontCover = 4,
BackCover = 5,
LeafletPage = 6,
Media = 7,
LeadArtist = 8,
Artist = 9,
Conductor = 10,
Band = 11,
Composer = 12,
Lyricist = 13,
RecordingLocation = 14,
DuringRecording = 15,
DuringPerformance = 16,
MovieScreenCapture = 17,
ColouredFish = 18,
Illustration = 19,
BandLogo = 20,
PublisherLogo = 21
};
enum class TrackArtistLinkType
{
Artist = 0, // regular track artist
+1
View File
@@ -19,6 +19,7 @@ add_executable(test-database
StarredTrack.cpp
Track.cpp
TrackBookmark.cpp
TrackEmbeddedImage.cpp
TrackFeatures.cpp
TrackList.cpp
TrackLyrics.cpp
+4
View File
@@ -32,6 +32,8 @@
#include "database/StarredArtist.hpp"
#include "database/StarredRelease.hpp"
#include "database/StarredTrack.hpp"
#include "database/TrackEmbeddedImage.hpp"
#include "database/TrackEmbeddedImageLink.hpp"
#include "database/TrackLyrics.hpp"
#include "database/UIState.hpp"
#include "database/User.hpp"
@@ -344,6 +346,8 @@ VALUES
EXPECT_FALSE(Cluster::find(session, ClusterId{}));
EXPECT_FALSE(ClusterType::find(session, ClusterTypeId{}));
EXPECT_FALSE(Directory::find(session, DirectoryId{}));
EXPECT_FALSE(TrackEmbeddedImage::find(session, TrackEmbeddedImageId{}));
EXPECT_FALSE(TrackEmbeddedImageLink::find(session, TrackEmbeddedImageLinkId{}));
EXPECT_FALSE(Image::find(session, ImageId{}));
EXPECT_FALSE(Label::find(session, LabelId{}));
EXPECT_FALSE(Listen::find(session, ListenId{}));
@@ -0,0 +1,283 @@
/*
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
*/
#include <limits>
#include "database/TrackEmbeddedImage.hpp"
#include "database/TrackEmbeddedImageLink.hpp"
#include "database/Types.hpp"
#include "Common.hpp"
namespace lms::db::tests
{
using ScopedTrackEmbeddedImage = ScopedEntity<db::TrackEmbeddedImage>;
using ScopedTrackEmbeddedImageLink = ScopedEntity<db::TrackEmbeddedImageLink>;
TEST_F(DatabaseFixture, TrackEmbeddedImage)
{
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(TrackEmbeddedImage::getCount(session), 0);
}
ScopedTrackEmbeddedImage image{ session };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(TrackEmbeddedImage::getCount(session), 1);
const TrackEmbeddedImage::pointer img{ TrackEmbeddedImage::find(session, image.getId()) };
ASSERT_NE(img, TrackEmbeddedImage::pointer{});
EXPECT_EQ(img->getHash(), db::ImageHashType{});
EXPECT_EQ(img->getSize(), 0);
EXPECT_EQ(img->getWidth(), 0);
EXPECT_EQ(img->getHeight(), 0);
EXPECT_EQ(img->getMimeType(), "");
}
{
auto transaction{ session.createWriteTransaction() };
TrackEmbeddedImage::pointer img{ TrackEmbeddedImage::find(session, image.getId()) };
ASSERT_NE(img, TrackEmbeddedImage::pointer{});
img.modify()->setHash(db::ImageHashType{ std::numeric_limits<std::uint64_t>::max() });
img.modify()->setSize(1024 * 1024);
img.modify()->setWidth(640);
img.modify()->setHeight(480);
img.modify()->setMimeType("image/jpeg");
}
{
auto transaction{ session.createReadTransaction() };
const TrackEmbeddedImage::pointer img{ TrackEmbeddedImage::find(session, image.getId()) };
ASSERT_NE(img, TrackEmbeddedImage::pointer{});
EXPECT_EQ(img->getHash(), db::ImageHashType{ std::numeric_limits<std::uint64_t>::max() });
EXPECT_EQ(img->getSize(), 1024 * 1024);
EXPECT_EQ(img->getWidth(), 640);
EXPECT_EQ(img->getHeight(), 480);
EXPECT_EQ(img->getMimeType(), "image/jpeg");
}
}
TEST_F(DatabaseFixture, TrackEmbeddedImage_findByHash)
{
ScopedTrackEmbeddedImage image{ session };
constexpr std::size_t size{ 1024 };
constexpr db::ImageHashType hash{ 42 };
{
auto transaction{ session.createWriteTransaction() };
TrackEmbeddedImage::pointer img{ TrackEmbeddedImage::find(session, image.getId()) };
ASSERT_NE(img, TrackEmbeddedImage::pointer{});
img.modify()->setHash(hash);
img.modify()->setSize(size);
}
{
auto transaction{ session.createReadTransaction() };
const TrackEmbeddedImage::pointer img{ TrackEmbeddedImage::find(session, size, hash) };
ASSERT_NE(img, TrackEmbeddedImage::pointer{});
EXPECT_EQ(image.getId(), img->getId());
}
{
auto transaction{ session.createReadTransaction() };
const TrackEmbeddedImage::pointer img{ TrackEmbeddedImage::find(session, size + 1, hash) };
EXPECT_EQ(img, TrackEmbeddedImage::pointer{});
}
}
TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams)
{
ScopedTrackEmbeddedImage image{ session };
ScopedTrack track{ session };
ScopedRelease release{ session, "MyRelease" };
ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params;
params.setIsPreferred(true);
bool visited{};
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
EXPECT_FALSE(visited);
}
{
auto transaction{ session.createWriteTransaction() };
link.get().modify()->setIsPreferred(true);
}
{
auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params;
params.setIsPreferred(true);
bool visited{};
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
EXPECT_TRUE(visited);
}
{
auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params;
params.setIsPreferred(true);
params.setRelease(release.getId());
bool visited{};
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
EXPECT_FALSE(visited);
}
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setRelease(release.get());
}
{
auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params;
params.setIsPreferred(true);
params.setRelease(release.getId());
params.setSortMethod(TrackEmbeddedImageSortMethod::FrontCoverAndSize);
bool visited{};
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
EXPECT_TRUE(visited);
}
{
auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params;
params.setIsPreferred(true);
params.setTrack(track.getId());
params.setSortMethod(TrackEmbeddedImageSortMethod::FrontCoverAndSize);
bool visited{};
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
EXPECT_TRUE(visited);
}
}
TEST_F(DatabaseFixture, Track_findByEmbeddedImage)
{
ScopedTrackEmbeddedImage image{ session };
ScopedTrack track{ session };
ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
Track::FindParameters params;
params.setEmbeddedImage(image->getId());
bool visited{};
Track::find(session, params, [&](const auto&) { visited = true; });
EXPECT_TRUE(visited);
}
}
TEST_F(DatabaseFixture, TrackEmbeddedImage_findOrphans)
{
ScopedTrackEmbeddedImage image{ session };
{
auto transaction{ session.createReadTransaction() };
auto orphans{ TrackEmbeddedImage::findOrphanIds(session, std::nullopt) };
ASSERT_EQ(orphans.results.size(), 1);
EXPECT_EQ(orphans.results[0], image.getId());
}
{
ScopedTrack track{ session };
ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
auto orphans{ TrackEmbeddedImage::findOrphanIds(session, std::nullopt) };
ASSERT_EQ(orphans.results.size(), 0);
}
}
{
auto transaction{ session.createReadTransaction() };
auto orphans{ TrackEmbeddedImage::findOrphanIds(session, std::nullopt) };
ASSERT_EQ(orphans.results.size(), 1);
EXPECT_EQ(orphans.results[0], image.getId());
}
}
TEST_F(DatabaseFixture, TrackEmbeddedImageLink)
{
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(TrackEmbeddedImage::getCount(session), 0);
}
ScopedTrack track{ session };
ScopedTrackEmbeddedImage image{ session };
ScopedTrackEmbeddedImageLink imageLink{ session, track.lockAndGet(), image.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(TrackEmbeddedImageLink::getCount(session), 1);
const TrackEmbeddedImageLink::pointer link{ TrackEmbeddedImageLink::find(session, imageLink.getId()) };
ASSERT_NE(link, TrackEmbeddedImageLink::pointer{});
EXPECT_EQ(link->getIndex(), 0);
EXPECT_EQ(link->getType(), ImageType::Unknown);
EXPECT_EQ(link->getDescription(), "");
EXPECT_EQ(link->getTrack(), track.get());
EXPECT_EQ(link->getImage(), image.get());
}
{
auto transaction{ session.createWriteTransaction() };
TrackEmbeddedImageLink::pointer link{ TrackEmbeddedImageLink::find(session, imageLink.getId()) };
ASSERT_NE(link, TrackEmbeddedImage::pointer{});
link.modify()->setIndex(2);
link.modify()->setType(ImageType::FrontCover);
link.modify()->setDescription("MyDesc");
}
{
auto transaction{ session.createReadTransaction() };
const TrackEmbeddedImageLink::pointer img{ TrackEmbeddedImageLink::find(session, imageLink.getId()) };
ASSERT_NE(img, TrackEmbeddedImage::pointer{});
EXPECT_EQ(img->getIndex(), 2);
EXPECT_EQ(img->getType(), ImageType::FrontCover);
EXPECT_EQ(img->getDescription(), "MyDesc");
}
}
} // namespace lms::db::tests
@@ -24,7 +24,6 @@
#include "core/IConfig.hpp"
#include "core/ILogger.hpp"
#include "core/String.hpp"
#include "core/Utils.hpp"
#include "database/Db.hpp"
#include "database/Image.hpp"
@@ -204,9 +203,9 @@ namespace lms::cover
return cover;
}
std::shared_ptr<image::IEncodedImage> ArtworkService::getTrackImage(db::TrackId trackId, std::optional<image::ImageSize> width)
std::shared_ptr<image::IEncodedImage> ArtworkService::getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional<image::ImageSize> width)
{
const ImageCache::EntryDesc cacheEntryDesc{ trackId, width };
const ImageCache::EntryDesc cacheEntryDesc{ trackEmbeddedImageId, width };
std::shared_ptr<image::IEncodedImage> cover{ _cache.getImage(cacheEntryDesc) };
if (cover)
@@ -217,12 +216,14 @@ namespace lms::cover
db::Session& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() };
const db::Track::pointer track{ db::Track::find(session, trackId) };
if (track && track->hasCover())
trackFile = track->getAbsoluteFilePath();
db::Track::FindParameters params;
params.setEmbeddedImage(trackEmbeddedImageId);
db::Track::find(session, params, [&](const db::Track::pointer& track) {
if (!cover)
cover = getTrackImage(track->getAbsoluteFilePath(), width);
});
}
cover = getTrackImage(trackFile, width);
if (cover)
_cache.addImage(cacheEntryDesc, cover);
@@ -22,7 +22,6 @@
#include <filesystem>
#include <vector>
#include "image/IEncodedImage.hpp"
#include "services/artwork/IArtworkService.hpp"
#include "ImageCache.hpp"
@@ -49,7 +48,8 @@ namespace lms::cover
private:
std::shared_ptr<image::IEncodedImage> getImage(db::ImageId imageId, std::optional<image::ImageSize> width) override;
std::shared_ptr<image::IEncodedImage> getTrackImage(db::TrackId trackId, 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> getDefaultReleaseCover() override;
std::shared_ptr<image::IEncodedImage> getDefaultArtistImage() override;
@@ -26,6 +26,7 @@
#include <variant>
#include "database/ImageId.hpp"
#include "database/TrackEmbeddedImageId.hpp"
#include "database/TrackId.hpp"
#include "image/IEncodedImage.hpp"
@@ -38,7 +39,7 @@ namespace lms::cover
struct EntryDesc
{
using VariantType = std::variant<db::TrackId, db::ImageId>;
using VariantType = std::variant<db::TrackEmbeddedImageId, db::ImageId>;
VariantType id;
std::optional<std::size_t> size;
@@ -24,7 +24,7 @@
#include <optional>
#include "database/ImageId.hpp"
#include "database/TrackId.hpp"
#include "database/TrackEmbeddedImageId.hpp"
#include "image/IEncodedImage.hpp"
namespace lms::db
@@ -40,9 +40,7 @@ namespace lms::cover
virtual ~IArtworkService() = default;
virtual std::shared_ptr<image::IEncodedImage> getImage(db::ImageId imageId, std::optional<image::ImageSize> width) = 0;
// no logic to fallback to release here
virtual std::shared_ptr<image::IEncodedImage> getTrackImage(db::TrackId trackId, std::optional<image::ImageSize> width) = 0;
virtual std::shared_ptr<image::IEncodedImage> getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional<image::ImageSize> width) = 0;
// Svg images dont have image "size"
virtual std::shared_ptr<image::IEncodedImage> getDefaultReleaseCover() = 0;
@@ -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)
{
@@ -27,6 +27,7 @@
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/TrackEmbeddedImage.hpp"
namespace lms::scanner
{
@@ -40,6 +41,7 @@ namespace lms::scanner
removeOrphanedLabels(context);
removeOrphanedCountries(context);
removeOrphanedDirectories(context);
removeOrphanedTrackEmbeddedImages(context);
}
void ScanStepRemoveOrphanedDbEntries::removeOrphanedClusters(ScanContext& context)
@@ -90,6 +92,12 @@ namespace lms::scanner
removeOrphanedEntries<db::Directory>(context);
}
void ScanStepRemoveOrphanedDbEntries::removeOrphanedTrackEmbeddedImages(ScanContext& context)
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned embedded images in tracks...");
removeOrphanedEntries<db::TrackEmbeddedImage>(context);
}
template<typename T>
void ScanStepRemoveOrphanedDbEntries::removeOrphanedEntries(ScanContext& context)
{
@@ -41,6 +41,7 @@ namespace lms::scanner
void removeOrphanedLabels(ScanContext& context);
void removeOrphanedCountries(ScanContext& context);
void removeOrphanedDirectories(ScanContext& context);
void removeOrphanedTrackEmbeddedImages(ScanContext& context);
template<typename T>
void removeOrphanedEntries(ScanContext& context);
+53 -48
View File
@@ -19,32 +19,46 @@
#include "CoverArtId.hpp"
#include "SubsonicId.hpp"
#include "core/String.hpp"
#include "database/ImageId.hpp"
#include "database/TrackEmbeddedImageId.hpp"
namespace lms::api::subsonic
{
namespace
{
constexpr char timestampSeparatorChar{ ':' };
}
constexpr char timestampSeparatorChar{ '-' };
std::string idToString(db::ImageId id)
{
return "im-" + id.toString();
}
std::string idToString(db::ImageId id)
{
return "im-" + id.toString();
}
std::string idToString(db::TrackEmbeddedImageId id)
{
return "trim-" + id.toString();
}
} // namespace
std::string idToString(CoverArtId coverId)
{
// produce "id:timestamp"
std::string res{ std::visit([](auto&& id) {
return idToString(id);
},
coverId.id) };
std::string res;
res += timestampSeparatorChar;
res += std::to_string(coverId.timestamp);
// 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);
}
assert(!res.empty());
return res;
}
} // namespace lms::api::subsonic
@@ -52,46 +66,37 @@ namespace lms::api::subsonic
// Used to parse parameters
namespace lms::core::stringUtils
{
template<>
std::optional<db::ImageId> readAs(std::string_view str)
{
std::vector<std::string_view> values{ core::stringUtils::splitString(str, '-') };
if (values.size() != 2)
return std::nullopt;
if (values[0] != "im")
return std::nullopt;
if (const auto value{ core::stringUtils::readAs<db::ReleaseId::ValueType>(values[1]) })
return db::ImageId{ *value };
return std::nullopt;
}
template<>
std::optional<api::subsonic::CoverArtId> readAs(std::string_view str)
{
// expect "id:timestamp"
auto timeStampSeparator{ str.find_last_of(api::subsonic::timestampSeparatorChar) };
if (timeStampSeparator == std::string_view::npos)
std::optional<api::subsonic::CoverArtId> res;
std::vector<std::string_view> values{ core::stringUtils::splitString(str, '-') };
if (values.size() <= 1)
return std::nullopt;
std::string_view strId{ str.substr(0, timeStampSeparator) };
std::string_view strTimestamp{ str.substr(timeStampSeparator + 1) };
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]) };
api::subsonic::CoverArtId cover;
if (const auto imagetId{ readAs<db::ImageId>(strId) })
cover.id = *imagetId;
else if (const auto trackId{ readAs<db::TrackId>(strId) })
cover.id = *trackId;
else
return std::nullopt;
if (imageId && timestamp)
res.emplace(db::ImageId{ *imageId }, *timestamp);
}
}
if (const auto timestamp{ readAs<std::time_t>(strTimestamp) })
cover.timestamp = *timestamp;
else
return std::nullopt;
return cover;
return res;
}
} // namespace lms::core::stringUtils
+9 -4
View File
@@ -24,18 +24,23 @@
#include "core/String.hpp"
#include "database/ImageId.hpp"
#include "database/TrackId.hpp"
#include "database/TrackEmbeddedImageId.hpp"
namespace lms::api::subsonic
{
struct CoverArtId
{
std::variant<db::ImageId, db::TrackId> id;
std::time_t timestamp;
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 } {}
};
std::string idToString(CoverArtId coverId);
std::string idToString(db::ImageId imageId);
} // namespace lms::api::subsonic
// Used to parse parameters
@@ -30,6 +30,7 @@
#include "core/Utils.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/TrackEmbeddedImageId.hpp"
#include "database/TrackLyrics.hpp"
#include "database/User.hpp"
#include "services/artwork/IArtworkService.hpp"
@@ -336,8 +337,8 @@ namespace lms::api::subsonic
*size = core::utils::clamp(*size, std::size_t{ 32 }, std::size_t{ 2048 });
std::shared_ptr<image::IEncodedImage> image;
if (const db::TrackId * trackId{ std::get_if<db::TrackId>(&coverArtId.id) })
image = core::Service<cover::IArtworkService>::get()->getTrackImage(*trackId, size);
if (const db::TrackEmbeddedImageId * trackEmbeddedImageId{ std::get_if<db::TrackEmbeddedImageId>(&coverArtId.id) })
image = core::Service<cover::IArtworkService>::get()->getTrackEmbeddedImage(*trackEmbeddedImageId, size);
else if (const db::ImageId * imageId{ std::get_if<db::ImageId>(&coverArtId.id) })
image = core::Service<cover::IArtworkService>::get()->getImage(*imageId, size);
+6 -4
View File
@@ -28,6 +28,7 @@
#include "database/Image.hpp"
#include "database/Release.hpp"
#include "database/Track.hpp"
#include "database/TrackEmbeddedImage.hpp"
#include "database/Types.hpp"
#include "database/User.hpp"
#include "services/feedback/IFeedbackService.hpp"
@@ -93,13 +94,14 @@ namespace lms::api::subsonic
}
else
{
db::Track::FindParameters params;
db::TrackEmbeddedImage::FindParameters params;
params.setRelease(release->getId());
params.setHasEmbeddedImage(true);
params.setIsPreferred(true);
params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontCoverAndSize);
params.setRange(db::Range{ 0, 1 });
db::Track::find(context.dbSession, params, [&](const db::Track::pointer& track) {
const CoverArtId coverArtId{ track->getId(), track->getLastWriteTime().toTime_t() };
db::TrackEmbeddedImage::find(context.dbSession, params, [&](const db::TrackEmbeddedImage::pointer& image) {
const CoverArtId coverArtId{ image->getId() };
albumNode.setAttribute("coverArt", idToString(coverArtId));
});
}
@@ -20,7 +20,7 @@
#include "Playlist.hpp"
#include "core/String.hpp"
#include "database/Track.hpp"
#include "database/TrackEmbeddedImage.hpp"
#include "database/TrackList.hpp"
#include "database/User.hpp"
@@ -43,14 +43,14 @@ namespace lms::api::subsonic
if (const db::User::pointer user{ tracklist->getUser() })
playlistNode.setAttribute("owner", user->getLoginName());
db::Track::FindParameters params;
db::TrackEmbeddedImage::FindParameters params;
params.setTrackList(tracklist->getId());
params.setHasEmbeddedImage(true);
params.setRange(db::Range{ 0, 1 });
params.setSortMethod(db::TrackSortMethod::TrackList);
params.setIsPreferred(true);
params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontCoverAndSize);
params.setRange(db::Range{ .offset = 0, .size = 1 });
db::Track::find(session, params, [&](const db::Track::pointer& track) {
const CoverArtId coverArtId{ track->getId(), track->getLastWriteTime().toTime_t() };
db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) {
const CoverArtId coverArtId{ image->getId() };
playlistNode.setAttribute("coverArt", idToString(coverArtId));
});
+18 -9
View File
@@ -32,6 +32,7 @@
#include "database/Release.hpp"
#include "database/Track.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/TrackEmbeddedImage.hpp"
#include "database/Types.hpp"
#include "database/User.hpp"
#include "services/feedback/IFeedbackService.hpp"
@@ -110,17 +111,25 @@ namespace lms::api::subsonic
const Release::pointer release{ track->getRelease() };
if (track->hasCover())
{
const CoverArtId coverArtId{ track->getId(), track->getLastWriteTime().toTime_t() };
trackResponse.setAttribute("coverArt", idToString(coverArtId));
}
else if (release)
{
if (const db::Image::pointer image{ release->getImage() })
{
const CoverArtId coverArtId{ image->getId(), image->getLastWriteTime().toTime_t() };
TrackEmbeddedImage::FindParameters params;
params.setTrack(track->getId());
params.setIsPreferred(true);
params.setRange(Range{ .offset = 0, .size = 1 });
bool hasEmbeddedImage{};
TrackEmbeddedImage::find(context.dbSession, params, [&](const TrackEmbeddedImage::pointer& image) {
const CoverArtId coverArtId{ image->getId() };
trackResponse.setAttribute("coverArt", idToString(coverArtId));
});
if (!hasEmbeddedImage && release)
{
if (const db::Image::pointer image{ release->getImage() })
{
const CoverArtId coverArtId{ image->getId(), image->getLastWriteTime().toTime_t() };
trackResponse.setAttribute("coverArt", idToString(coverArtId));
}
}
}
+2 -2
View File
@@ -253,8 +253,8 @@ namespace lms::ui
<< " artist: \"" << (!artists.empty() ? core::stringUtils::jsEscape(track->getArtistDisplayName()) : "") << "\","
<< " release: \"" << (track->getRelease() ? core::stringUtils::jsEscape(track->getRelease()->getName()) : "") << "\","
<< " artwork: ["
<< " { src: \"" << LmsApp->getArtworkResource()->getTrackImageUrl(trackId, ArtworkResource::Size::Small) << "\", sizes: \"128x128\", type: \"image/jpeg\" },"
<< " { src: \"" << LmsApp->getArtworkResource()->getTrackImageUrl(trackId, ArtworkResource::Size::Large) << "\", sizes: \"512x512\", type: \"image/jpeg\" },"
<< " { src: \"" << LmsApp->getArtworkResource()->getPreferredTrackImageUrl(trackId, ArtworkResource::Size::Small) << "\", sizes: \"128x128\", type: \"image/jpeg\" },"
<< " { src: \"" << LmsApp->getArtworkResource()->getPreferredTrackImageUrl(trackId, ArtworkResource::Size::Large) << "\", sizes: \"512x512\", type: \"image/jpeg\" },"
<< " ]"
<< "};";
// Update 'sizes' above to match this:
+1 -1
View File
@@ -84,7 +84,7 @@ namespace lms::ui::utils
std::unique_ptr<Wt::WImage> createTrackImage(db::TrackId trackId, ArtworkResource::Size size)
{
auto cover{ std::make_unique<Wt::WImage>() };
cover->setImageLink(LmsApp->getArtworkResource()->getTrackImageUrl(trackId, size));
cover->setImageLink(LmsApp->getArtworkResource()->getPreferredTrackImageUrl(trackId, size));
cover->setStyleClass("Lms-cover img-fluid"); // HACK
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK
+34 -32
View File
@@ -31,6 +31,8 @@
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/TrackEmbeddedImage.hpp"
#include "database/Types.hpp"
#include "services/artwork/IArtworkService.hpp"
#include "LmsApplication.hpp"
@@ -89,13 +91,14 @@ namespace lms::ui
}
else
{
db::Track::FindParameters params;
db::TrackEmbeddedImage::FindParameters params;
params.setRelease(releaseId);
params.setHasEmbeddedImage(true);
params.setIsPreferred(true);
params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontCoverAndSize);
params.setRange(db::Range{ 0, 1 });
db::Track::find(LmsApp->getDbSession(), params, [&](const db::Track::pointer& track) {
url = getImageUrl(track->getId(), size, "release");
db::TrackEmbeddedImage::find(LmsApp->getDbSession(), params, [&](const db::TrackEmbeddedImage::pointer& image) {
url = getImageUrl(image->getId(), size, "release");
});
}
}
@@ -107,24 +110,32 @@ namespace lms::ui
return url;
}
std::string ArtworkResource::getTrackImageUrl(db::TrackId trackId, std::optional<Size> size) const
std::string ArtworkResource::getPreferredTrackImageUrl(db::TrackId trackId, std::optional<Size> size) const
{
std::string url;
{
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
const db::Track::pointer track{ db::Track::find(LmsApp->getDbSession(), trackId) };
if (track)
db::TrackEmbeddedImage::FindParameters params;
params.setTrack(trackId);
params.setIsPreferred(true);
params.setRange(db::Range{ .offset = 0, .size = 1 });
db::TrackEmbeddedImage::find(LmsApp->getDbSession(), params, [&](const db::TrackEmbeddedImage::pointer& image) {
url = getImageUrl(image->getId(), size, "release");
});
if (url.empty())
{
if (track->hasCover())
db::Track::pointer track{ db::Track::find(LmsApp->getDbSession(), trackId) };
if (track)
{
url = getImageUrl(trackId, size, "release");
}
else if (const db::Release::pointer release{ track->getRelease() })
{
if (const db::Image::pointer image{ release->getImage() })
url = getImageUrl(image->getId(), size, "release");
if (const db::Release::pointer release{ track->getRelease() })
{
if (const db::Image::pointer image{ release->getImage() })
url = getImageUrl(image->getId(), size, "release");
}
}
}
}
@@ -144,9 +155,9 @@ namespace lms::ui
return res;
}
std::string ArtworkResource::getImageUrl(db::TrackId trackId, std::optional<Size> size, std::string_view type) const
std::string ArtworkResource::getImageUrl(db::TrackEmbeddedImageId trackId, std::optional<Size> size, std::string_view type) const
{
std::string res{ url() + "&trackid=" + trackId.toString() + "&type=" + std::string{ type } };
std::string res{ url() + "&trimageid=" + trackId.toString() + "&type=" + std::string{ type } };
if (size)
res += "&size=" + std::to_string(static_cast<std::size_t>(*size));
return res;
@@ -168,13 +179,13 @@ namespace lms::ui
// Retrieve parameters
const std::string* imageIdStr = request.getParameter("imageid");
const std::string* trackIdStr = request.getParameter("trackid");
const std::string* trackEmbeddedImageIdStr = request.getParameter("trimageid");
const std::string* sizeStr = request.getParameter("size");
const std::string* typeStr = request.getParameter("type");
std::shared_ptr<image::IEncodedImage> image;
if ((imageIdStr || trackIdStr))
if ((imageIdStr || trackEmbeddedImageIdStr))
{
const auto size{ sizeStr ? core::stringUtils::readAs<std::size_t>(*sizeStr) : std::nullopt };
if (size && *size > maxSize)
@@ -185,22 +196,13 @@ namespace lms::ui
if (imageIdStr)
{
const std::optional<db::ImageId> imageId{ core::stringUtils::readAs<db::ImageId::ValueType>(*imageIdStr) };
if (!imageId)
return;
image = core::Service<cover::IArtworkService>::get()->getImage(*imageId, size);
if (const auto imageId{ core::stringUtils::readAs<db::ImageId::ValueType>(*imageIdStr) })
image = core::Service<cover::IArtworkService>::get()->getImage(*imageId, size);
}
else if (trackIdStr)
else if (trackEmbeddedImageIdStr)
{
const std::optional<db::TrackId> trackId{ core::stringUtils::readAs<db::TrackId::ValueType>(*trackIdStr) };
if (!trackId)
{
LOG(DEBUG, "track not found");
return;
}
image = core::Service<cover::IArtworkService>::get()->getTrackImage(*trackId, size);
if (const auto imageId{ core::stringUtils::readAs<db::TrackEmbeddedImageId::ValueType>(*trackEmbeddedImageIdStr) })
image = core::Service<cover::IArtworkService>::get()->getTrackEmbeddedImage(*imageId, size);
}
}
+3 -2
View File
@@ -26,6 +26,7 @@
#include "database/ArtistId.hpp"
#include "database/ImageId.hpp"
#include "database/ReleaseId.hpp"
#include "database/TrackEmbeddedImageId.hpp"
#include "database/TrackId.hpp"
namespace lms::ui
@@ -46,11 +47,11 @@ namespace lms::ui
std::string getArtistImageUrl(db::ArtistId artistId, std::optional<Size> size = std::nullopt) const;
std::string getReleaseCoverUrl(db::ReleaseId releaseId, std::optional<Size> size = std::nullopt) const;
std::string getTrackImageUrl(db::TrackId trackId, std::optional<Size> size = std::nullopt) const;
std::string getPreferredTrackImageUrl(db::TrackId trackId, std::optional<Size> size = std::nullopt) const;
private:
std::string getImageUrl(db::ImageId imageId, std::optional<Size> size, std::string_view type) const;
std::string getImageUrl(db::TrackId trackId, std::optional<Size> size, std::string_view type) const;
std::string getImageUrl(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional<Size> size, std::string_view type) const;
std::string getDefaultArtistImageUrl() const;
std::string getDefaultReleaseCoverUrl() const;
-1
View File
@@ -1,4 +1,3 @@
add_subdirectory(db-generator)
add_subdirectory(cover)
add_subdirectory(metadata)
add_subdirectory(recommendation)
-10
View File
@@ -1,10 +0,0 @@
add_executable(lms-cover
LmsCover.cpp
)
target_link_libraries(lms-cover PRIVATE
lmsartwork
Boost::program_options
)
-109
View File
@@ -1,109 +0,0 @@
/*
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
*/
#include <filesystem>
#include <iostream>
#include <stdlib.h>
#include <boost/program_options.hpp>
#include "core/IConfig.hpp"
#include "core/ILogger.hpp"
#include "core/Service.hpp"
#include "core/StreamLogger.hpp"
#include "core/SystemPaths.hpp"
#include "database/Db.hpp"
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "image/Image.hpp"
#include "services/artwork/IArtworkService.hpp"
namespace lms
{
void dumpTrackCovers(db::Session& session, image::ImageSize width)
{
using namespace db;
RangeResults<db::TrackId> trackIds;
{
auto transaction{ session.createReadTransaction() };
trackIds = db::Track::findIds(session, db::Track::FindParameters{});
}
for (const db::TrackId trackId : trackIds.results)
{
std::cout << "Getting cover for track id " << trackId.toString() << std::endl;
core::Service<cover::IArtworkService>::get()->getTrackImage(trackId, width);
}
}
} // namespace lms
int main(int argc, char* argv[])
{
try
{
using namespace lms;
namespace po = boost::program_options;
// log to stdout
core::Service<core::logging::ILogger> logger{ std::make_unique<core::logging::StreamLogger>(std::cout) };
po::options_description desc{ "Allowed options" };
// clang-format off
desc.add_options()
("help,h", "print usage message")
("conf,c", po::value<std::string>()->default_value(core::sysconfDirectory / "lms.conf"), "LMS config file")
("default-release-cover,d", po::value<std::string>(), "Default release cover path")
("default-artist-image,d", po::value<std::string>(), "Default artist image")
("tracks,t", "dump covers for tracks")
("size,s", po::value<unsigned>()->default_value(512), "Requested cover size")
("quality,q", po::value<unsigned>()->default_value(75), "JPEG quality (1-100)");
// clang-format on
po::variables_map vm;
po::store(po::parse_command_line(argc, argv, desc), vm);
if (vm.count("help"))
{
std::cout << desc << std::endl;
return EXIT_SUCCESS;
}
image::init(argv[0]);
core::Service<core::IConfig> config{ core::createConfig(vm["conf"].as<std::string>()) };
db::Db db{ config->getPath("working-dir", "/var/lms") / "lms.db" };
core::Service<cover::IArtworkService> coverArtService{ cover::createArtworkService(db, vm["default-release-cover,"].as<std::string>(), vm["default-artist-image"].as<std::string>()) };
coverArtService->setJpegQuality(config->getULong("cover-jpeg-quality", vm["quality"].as<unsigned>()));
db::Session session{ db };
if (vm.count("tracks"))
dumpTrackCovers(session, vm["size"].as<unsigned>());
}
catch (std::exception& e)
{
std::cerr << "Caught exception: " << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}