Restored recommendations based on acoustic similarities (using musicnn), fixes #301
This commit is contained in:
@@ -28,7 +28,7 @@ add_library(lmsdatabase STATIC
|
||||
impl/objects/TrackBookmark.cpp
|
||||
impl/objects/TrackEmbeddedImage.cpp
|
||||
impl/objects/TrackEmbeddedImageLink.cpp
|
||||
impl/objects/TrackFeatures.cpp
|
||||
impl/objects/TrackMusicNNEmbeddings.cpp
|
||||
impl/objects/TrackList.cpp
|
||||
impl/objects/TrackLyrics.cpp
|
||||
impl/objects/Types.cpp
|
||||
@@ -38,7 +38,7 @@ add_library(lmsdatabase STATIC
|
||||
impl/IdType.cpp
|
||||
impl/Migration.cpp
|
||||
impl/Object.cpp
|
||||
impl/QueryPlanRecorder.cpp
|
||||
impl/profiling/QueryProfiler.cpp
|
||||
impl/Session.cpp
|
||||
impl/SqlQuery.cpp
|
||||
impl/Transaction.cpp
|
||||
@@ -59,6 +59,7 @@ target_include_directories(lmsdatabase PRIVATE
|
||||
)
|
||||
|
||||
target_link_libraries(lmsdatabase PRIVATE
|
||||
lmsmath
|
||||
Wt::DboSqlite3
|
||||
)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace lms::db
|
||||
{
|
||||
namespace
|
||||
{
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 103 };
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 104 };
|
||||
}
|
||||
|
||||
VersionInfo::VersionInfo()
|
||||
@@ -1706,6 +1706,23 @@ FROM track)");
|
||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET audio_scan_version = audio_scan_version + 1");
|
||||
}
|
||||
|
||||
void migrateFromV103(Session& session)
|
||||
{
|
||||
// Drop previous track_audio_features with a brand new table dedicated to embeddings
|
||||
utils::executeCommand(*session.getDboSession(), R"(DROP TABLE track_features)");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_musicnn_embeddings" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"data" blob not null,
|
||||
"track_id" bigint,
|
||||
constraint "fk_track_musicnn_embeddings_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred
|
||||
))");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE scan_settings RENAME COLUMN similarity_engine_type TO recommendation_engine_type");
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE scan_settings ADD COLUMN musicnn_model_identifier TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
|
||||
bool doDbMigration(Session& session)
|
||||
{
|
||||
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||
@@ -1785,6 +1802,7 @@ FROM track)");
|
||||
{ 100, migrateFromV100 },
|
||||
{ 101, migrateFromV101 },
|
||||
{ 102, migrateFromV102 },
|
||||
{ 103, migrateFromV103 },
|
||||
};
|
||||
|
||||
bool migrationPerformed{};
|
||||
|
||||
@@ -52,9 +52,9 @@
|
||||
#include "database/objects/TrackBookmark.hpp"
|
||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||
#include "database/objects/TrackFeatures.hpp"
|
||||
#include "database/objects/TrackList.hpp"
|
||||
#include "database/objects/TrackLyrics.hpp"
|
||||
#include "database/objects/TrackMusicNNEmbeddings.hpp"
|
||||
#include "database/objects/UIState.hpp"
|
||||
#include "database/objects/User.hpp"
|
||||
|
||||
@@ -106,7 +106,7 @@ namespace lms::db
|
||||
_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<TrackMusicNNEmbeddings>("track_musicnn_embeddings");
|
||||
_session.mapClass<TrackList>("tracklist");
|
||||
_session.mapClass<TrackListEntry>("tracklist_entry");
|
||||
_session.mapClass<TrackLyrics>("track_lyrics");
|
||||
@@ -302,7 +302,7 @@ namespace lms::db
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_track_artist_idx ON track_artist_link(track_id, artist_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_track_type_idx ON track_artist_link(track_id, type)");
|
||||
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_musicnn_embeddings_track_idx ON track_musicnn_embeddings(track_id)");
|
||||
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_lyrics_id_idx ON track_lyrics(id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_lyrics_absolute_file_path_idx ON track_lyrics(absolute_file_path)");
|
||||
|
||||
@@ -29,10 +29,9 @@
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "core/ITraceLogger.hpp"
|
||||
#include "core/Service.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
#include "QueryPlanRecorder.hpp"
|
||||
#include "profiling/ScopedQueryProfiler.hpp"
|
||||
|
||||
namespace lms::db::utils
|
||||
{
|
||||
@@ -42,16 +41,6 @@ namespace lms::db::utils
|
||||
|
||||
Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime);
|
||||
|
||||
namespace detail
|
||||
{
|
||||
template<typename Query>
|
||||
void recordQueryPlanIfNeeded(const Query& query)
|
||||
{
|
||||
if (IQueryPlanRecorder * recorder{ core::Service<IQueryPlanRecorder>::get() })
|
||||
static_cast<QueryPlanRecorder*>(recorder)->recordQueryPlanIfNeeded(query.session(), query.asString());
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
template<typename Query>
|
||||
void applyRange(Query& query, std::optional<Range> range)
|
||||
{
|
||||
@@ -100,20 +89,22 @@ namespace lms::db::utils
|
||||
template<typename Query, typename UnaryFunc>
|
||||
void forEachQueryResult(const Query& query, UnaryFunc&& func)
|
||||
{
|
||||
detail::recordQueryPlanIfNeeded(query);
|
||||
|
||||
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "ForEachQueryResult", "Query", query.asString());
|
||||
|
||||
forEachResult(query.resultList(), std::forward<UnaryFunc>(func));
|
||||
ScopedQueryProfiler queryProfiler{ query };
|
||||
forEachResult(query.resultList(), [&](const auto& result) {
|
||||
queryProfiler.suspend();
|
||||
func(result);
|
||||
queryProfiler.resume();
|
||||
});
|
||||
}
|
||||
|
||||
template<typename T, typename Query>
|
||||
std::vector<T> fetchQueryResults(const Query& query)
|
||||
{
|
||||
detail::recordQueryPlanIfNeeded(query);
|
||||
|
||||
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQueryResults", "Query", query.asString());
|
||||
|
||||
ScopedQueryProfiler queryProfiler{ query };
|
||||
auto collection{ query.resultList() };
|
||||
return std::vector<T>(collection.begin(), collection.end());
|
||||
}
|
||||
@@ -121,10 +112,9 @@ namespace lms::db::utils
|
||||
template<typename Query>
|
||||
std::vector<typename QueryResultType<Query>::type> fetchQueryResults(const Query& query)
|
||||
{
|
||||
detail::recordQueryPlanIfNeeded(query);
|
||||
|
||||
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQueryResults", "Query", query.asString());
|
||||
|
||||
ScopedQueryProfiler queryProfiler{ query };
|
||||
auto collection{ query.resultList() };
|
||||
return std::vector<typename QueryResultType<Query>::type>(collection.begin(), collection.end());
|
||||
}
|
||||
@@ -132,9 +122,8 @@ namespace lms::db::utils
|
||||
template<typename Query>
|
||||
auto fetchQuerySingleResult(const Query& query)
|
||||
{
|
||||
detail::recordQueryPlanIfNeeded(query);
|
||||
|
||||
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQuerySingleResult", "Query", query.asString());
|
||||
ScopedQueryProfiler queryProfiler{ query };
|
||||
return query.resultValue();
|
||||
}
|
||||
|
||||
@@ -184,6 +173,7 @@ namespace lms::db::utils
|
||||
moreResults = false;
|
||||
|
||||
std::size_t count{};
|
||||
ScopedQueryProfiler queryProfiler{ query };
|
||||
const auto collection{ query.resultList() };
|
||||
auto it{ fetchFirstResult(collection) };
|
||||
while (it != collection.end())
|
||||
@@ -194,7 +184,9 @@ namespace lms::db::utils
|
||||
break;
|
||||
}
|
||||
|
||||
queryProfiler.suspend();
|
||||
func(*it);
|
||||
queryProfiler.resume();
|
||||
fetchNextResult<ResultType>(it);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,48 +403,6 @@ AND NOT EXISTS (
|
||||
return _preferredArtwork.id();
|
||||
}
|
||||
|
||||
RangeResults<ArtistId> Artist::findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << "SELECT a.id FROM artist a"
|
||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
|
||||
" INNER JOIN track t ON t.id = t_a_l.track_id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||
" WHERE "
|
||||
" t_c.cluster_id IN (SELECT DISTINCT c.id from cluster c"
|
||||
" INNER JOIN track t ON c.id = t_c.cluster_id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||
" INNER JOIN artist a ON a.id = t_a_l.artist_id"
|
||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
|
||||
" WHERE a.id = ?)"
|
||||
" AND a.id <> ?";
|
||||
|
||||
if (!artistLinkTypes.empty())
|
||||
{
|
||||
oss << " AND t_a_l.type IN (";
|
||||
|
||||
bool first{ true };
|
||||
for (TrackArtistLinkType type : artistLinkTypes)
|
||||
{
|
||||
(void)type;
|
||||
if (!first)
|
||||
oss << ", ";
|
||||
oss << "?";
|
||||
first = false;
|
||||
}
|
||||
oss << ")";
|
||||
}
|
||||
|
||||
auto query{ session()->query<ArtistId>(oss.str()).bind(getId()).bind(getId()).groupBy("a.id").orderBy("COUNT(*) DESC, RANDOM()") };
|
||||
|
||||
for (const TrackArtistLinkType type : artistLinkTypes)
|
||||
query.bind(type);
|
||||
|
||||
return utils::execRangeQuery<ArtistId>(query, range);
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> Artist::getClusterGroups(std::span<const ClusterTypeId> clusterTypeIds, std::size_t size) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
@@ -749,33 +749,6 @@ namespace lms::db
|
||||
return utils::fetchQueryResults(query);
|
||||
}
|
||||
|
||||
std::vector<Release::pointer> Release::getSimilarReleases(std::optional<std::size_t> offset, std::optional<std::size_t> count) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
// Select the similar releases using the 5 most used clusters of the release
|
||||
auto query{ session()->query<Wt::Dbo::ptr<Release>>(
|
||||
"SELECT r FROM release r"
|
||||
" INNER JOIN track t ON t.release_id = r.id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||
" WHERE "
|
||||
" t_c.cluster_id IN "
|
||||
"(SELECT DISTINCT c.id FROM cluster c"
|
||||
" INNER JOIN track t ON c.id = t_c.cluster_id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||
" INNER JOIN release r ON r.id = t.release_id"
|
||||
" WHERE r.id = ?)"
|
||||
" AND r.id <> ?")
|
||||
.bind(getId())
|
||||
.bind(getId())
|
||||
.groupBy("r.id")
|
||||
.orderBy("COUNT(*) DESC, RANDOM()")
|
||||
.limit(count ? static_cast<int>(*count) : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1) };
|
||||
|
||||
return utils::fetchQueryResults<Release::pointer>(query);
|
||||
}
|
||||
|
||||
ObjectPtr<Artwork> Release::getPreferredArtwork() const
|
||||
{
|
||||
return ObjectPtr<Artwork>{ _preferredArtwork };
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||
#include "database/objects/TrackFeatures.hpp"
|
||||
#include "database/objects/TrackLyrics.hpp"
|
||||
#include "database/objects/User.hpp"
|
||||
|
||||
@@ -191,6 +190,20 @@ namespace lms::db
|
||||
if (params.fileSize.has_value())
|
||||
query.where("t.file_size = ?").bind(static_cast<long long>(params.fileSize.value()));
|
||||
|
||||
if (params.hasMusicNNEmbeddings.has_value())
|
||||
{
|
||||
if (*params.hasMusicNNEmbeddings)
|
||||
query.where("EXISTS (SELECT t_m_e.track_id FROM track_musicnn_embeddings t_m_e WHERE t_m_e.track_id = t.id)");
|
||||
else
|
||||
query.where("NOT EXISTS (SELECT t_m_e.track_id FROM track_musicnn_embeddings t_m_e WHERE t_m_e.track_id = t.id)");
|
||||
}
|
||||
|
||||
if (params.lastTrackId.isValid())
|
||||
{
|
||||
assert(params.sortMethod == TrackSortMethod::Id);
|
||||
query.where("t.id > ?").bind(params.lastTrackId);
|
||||
}
|
||||
|
||||
if (params.embeddedImageId.isValid())
|
||||
{
|
||||
query.join("track_embedded_image_link t_e_i_l ON t_e_i_l.track_id = t.id");
|
||||
@@ -322,7 +335,7 @@ namespace lms::db
|
||||
});
|
||||
}
|
||||
|
||||
void Track::findAbsoluteFilePath(Session& session, TrackId& lastRetrievedId, std::size_t count, const std::function<void(TrackId trackId, const std::filesystem::path& absoluteFilePath)>& func)
|
||||
void Track::findAbsoluteFilePath(Session& session, TrackId& lastRetrievedId, std::size_t count, const TrackLocationVisitor& func)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
@@ -334,6 +347,19 @@ namespace lms::db
|
||||
});
|
||||
}
|
||||
|
||||
void Track::findAbsoluteFilePath(Session& session, const FindParameters& params, const TrackLocationVisitor& func)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
std::string_view itemToSelect{ "t.id, t.absolute_file_path" };
|
||||
|
||||
auto query{ createQuery<std::tuple<TrackId, std::filesystem::path>>(session, itemToSelect, params) };
|
||||
|
||||
utils::forEachQueryRangeResult(query, params.range, [&](const auto& res) {
|
||||
func(std::get<0>(res), std::get<1>(res));
|
||||
});
|
||||
}
|
||||
|
||||
void Track::find(Session& session, const IdRange<TrackId>& idRange, const std::function<void(const Track::pointer&)>& func)
|
||||
{
|
||||
assert(idRange.isValid());
|
||||
@@ -385,15 +411,6 @@ namespace lms::db
|
||||
return utils::execRangeQuery<TrackId>(query, range);
|
||||
}
|
||||
|
||||
RangeResults<TrackId> Track::findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession()->query<TrackId>("SELECT t.id FROM track t").where("LENGTH(t.recording_mbid) > 0").where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)") };
|
||||
|
||||
return utils::execRangeQuery<TrackId>(query, range);
|
||||
}
|
||||
|
||||
void Track::updatePreferredArtwork(Session& session, TrackId trackId, ArtworkId artworkId)
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
@@ -490,36 +507,11 @@ namespace lms::db
|
||||
utils::forEachQueryRangeResult(query, params.range, moreResults, func);
|
||||
}
|
||||
|
||||
RangeResults<TrackId> Track::findSimilarTrackIds(Session& session, const std::vector<TrackId>& tracks, std::optional<Range> range)
|
||||
std::size_t Track::getCount(Session& session, const FindParameters& params)
|
||||
{
|
||||
assert(!tracks.empty());
|
||||
session.checkReadTransaction();
|
||||
|
||||
std::ostringstream oss;
|
||||
for (std::size_t i{}; i < tracks.size(); ++i)
|
||||
{
|
||||
if (!oss.str().empty())
|
||||
oss << ", ";
|
||||
oss << "?";
|
||||
}
|
||||
|
||||
auto query{ session.getDboSession()->query<TrackId>(
|
||||
"SELECT t.id FROM track t"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||
" AND t_c.cluster_id IN (SELECT DISTINCT c.id FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id WHERE t_c.track_id IN ("
|
||||
+ oss.str() + "))"
|
||||
" AND t.id NOT IN ("
|
||||
+ oss.str() + ")")
|
||||
.groupBy("t.id")
|
||||
.orderBy("COUNT(*) DESC, RANDOM()") };
|
||||
|
||||
for (TrackId trackId : tracks)
|
||||
query.bind(trackId);
|
||||
|
||||
for (TrackId trackId : tracks)
|
||||
query.bind(trackId);
|
||||
|
||||
return utils::execRangeQuery<TrackId>(query, range);
|
||||
return utils::fetchQuerySingleResult(createQuery<int>(session, "COUNT(*)", params));
|
||||
}
|
||||
|
||||
void Track::setAbsoluteFilePath(const std::filesystem::path& filePath)
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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/objects/TrackFeatures.hpp"
|
||||
|
||||
#include <Wt/Dbo/Impl.h>
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
#include "traits/IdTypeTraits.hpp"
|
||||
|
||||
DBO_INSTANTIATE_TEMPLATES(lms::db::TrackFeatures)
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
|
||||
TrackFeatures::TrackFeatures(ObjectPtr<Track> track, const std::string& jsonEncodedFeatures)
|
||||
: _data{ jsonEncodedFeatures }
|
||||
, _track{ getDboPtr(track) }
|
||||
{
|
||||
}
|
||||
|
||||
TrackFeatures::pointer TrackFeatures::create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures)
|
||||
{
|
||||
return session.getDboSession()->add(std::unique_ptr<TrackFeatures>{ new TrackFeatures{ track, jsonEncodedFeatures } });
|
||||
}
|
||||
|
||||
std::size_t TrackFeatures::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM track_features"));
|
||||
}
|
||||
|
||||
TrackFeatures::pointer TrackFeatures::find(Session& session, TrackFeaturesId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackFeatures>().where("id = ?").bind(id));
|
||||
}
|
||||
|
||||
TrackFeatures::pointer TrackFeatures::find(Session& session, TrackId trackId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackFeatures>().where("track_id = ?").bind(trackId));
|
||||
}
|
||||
|
||||
RangeResults<TrackFeaturesId> TrackFeatures::find(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession()->query<TrackFeaturesId>("SELECT id from track_features") };
|
||||
|
||||
return utils::execRangeQuery<TrackFeaturesId>(query, range);
|
||||
}
|
||||
|
||||
FeatureValues TrackFeatures::getFeatureValues(const FeatureName& featureNode) const
|
||||
{
|
||||
FeatureValuesMap featuresValuesMap{ getFeatureValuesMap({ featureNode }) };
|
||||
return std::move(featuresValuesMap[featureNode]);
|
||||
}
|
||||
|
||||
FeatureValuesMap TrackFeatures::getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const
|
||||
{
|
||||
FeatureValuesMap res;
|
||||
|
||||
try
|
||||
{
|
||||
std::istringstream iss{ _data };
|
||||
boost::property_tree::ptree root;
|
||||
|
||||
boost::property_tree::read_json(iss, root);
|
||||
|
||||
for (const FeatureName& featureName : featureNames)
|
||||
{
|
||||
FeatureValues& featureValues{ res[featureName] };
|
||||
|
||||
auto node{ root.get_child(featureName) };
|
||||
|
||||
bool hasChildren = false;
|
||||
for (const auto& child : node.get_child(""))
|
||||
{
|
||||
hasChildren = true;
|
||||
featureValues.push_back(child.second.get_value<double>());
|
||||
}
|
||||
|
||||
if (!hasChildren)
|
||||
featureValues.push_back(node.get_value<double>());
|
||||
}
|
||||
}
|
||||
catch (boost::property_tree::ptree_error& error)
|
||||
{
|
||||
LMS_LOG(DB, ERROR, "Track " << _track.id() << ": ptree exception: " << error.what());
|
||||
res.clear();
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace lms::db
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright (C) 2026 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/objects/TrackMusicNNEmbeddings.hpp"
|
||||
|
||||
#include <Wt/Dbo/Impl.h>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
#include "traits/IdTypeTraits.hpp"
|
||||
|
||||
DBO_INSTANTIATE_TEMPLATES(lms::db::TrackMusicNNEmbeddings)
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
TrackMusicNNEmbeddings::TrackMusicNNEmbeddings(ObjectPtr<Track> track)
|
||||
: _track{ getDboPtr(track) }
|
||||
{
|
||||
}
|
||||
|
||||
TrackMusicNNEmbeddings::pointer TrackMusicNNEmbeddings::create(Session& session, ObjectPtr<Track> track)
|
||||
{
|
||||
return session.getDboSession()->add(std::unique_ptr<TrackMusicNNEmbeddings>{ new TrackMusicNNEmbeddings{ track } });
|
||||
}
|
||||
|
||||
std::size_t TrackMusicNNEmbeddings::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM track_musicnn_embeddings"));
|
||||
}
|
||||
|
||||
TrackMusicNNEmbeddings::pointer TrackMusicNNEmbeddings::find(Session& session, TrackMusicNNEmbeddingsId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackMusicNNEmbeddings>().where("id = ?").bind(id));
|
||||
}
|
||||
|
||||
TrackMusicNNEmbeddings::pointer TrackMusicNNEmbeddings::find(Session& session, TrackId trackId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackMusicNNEmbeddings>().where("track_id = ?").bind(trackId));
|
||||
}
|
||||
|
||||
RangeResults<TrackMusicNNEmbeddingsId> TrackMusicNNEmbeddings::find(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession()->query<TrackMusicNNEmbeddingsId>("SELECT id from track_musicnn_embeddings") };
|
||||
|
||||
return utils::execRangeQuery<TrackMusicNNEmbeddingsId>(query, range);
|
||||
}
|
||||
|
||||
void TrackMusicNNEmbeddings::find(Session& session, std::function<void(const pointer&)> func)
|
||||
{
|
||||
auto query{ session.getDboSession()->find<TrackMusicNNEmbeddings>() };
|
||||
|
||||
utils::forEachQueryResult(query, [&](const TrackMusicNNEmbeddings::pointer& embeddings) {
|
||||
func(embeddings);
|
||||
});
|
||||
}
|
||||
|
||||
void TrackMusicNNEmbeddings::removeAll(Session& session)
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
utils::executeCommand(*session.getDboSession(), "DELETE FROM track_musicnn_embeddings");
|
||||
}
|
||||
|
||||
std::span<const std::byte> TrackMusicNNEmbeddings::getData() const
|
||||
{
|
||||
return std::span<const std::byte>{ reinterpret_cast<const std::byte*>(_data.data()), _data.size() };
|
||||
}
|
||||
|
||||
void TrackMusicNNEmbeddings::setData(std::span<const std::byte> data)
|
||||
{
|
||||
const auto* start{ reinterpret_cast<const unsigned char*>(data.data()) };
|
||||
_data.assign(start, start + data.size());
|
||||
}
|
||||
} // namespace lms::db
|
||||
+37
-18
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "QueryPlanRecorder.hpp"
|
||||
#include "profiling/QueryProfiler.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -29,35 +29,38 @@
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
std::unique_ptr<IQueryPlanRecorder> createQueryPlanRecorder()
|
||||
std::unique_ptr<IQueryProfiler> createQueryProfiler()
|
||||
{
|
||||
return std::make_unique<QueryPlanRecorder>();
|
||||
return std::make_unique<QueryProfiler>();
|
||||
}
|
||||
|
||||
QueryPlanRecorder::QueryPlanRecorder()
|
||||
QueryProfiler::QueryProfiler()
|
||||
{
|
||||
LMS_LOG(DB, INFO, "Recording database query plans");
|
||||
LMS_LOG(DB, INFO, "Recording database queries");
|
||||
}
|
||||
|
||||
QueryPlanRecorder::~QueryPlanRecorder() = default;
|
||||
QueryProfiler::~QueryProfiler() = default;
|
||||
|
||||
void QueryPlanRecorder::visitQueryPlans(const QueryPlanVisitor& visitor) const
|
||||
void QueryProfiler::visitQueries(const QueryVisitor& visitor) const
|
||||
{
|
||||
const std::shared_lock lock{ _mutex };
|
||||
|
||||
for (const auto& [query, plan] : _queryPlans)
|
||||
visitor(query, plan);
|
||||
for (const auto& [query, data] : _queries)
|
||||
{
|
||||
const QueryStats stats{
|
||||
.query = query,
|
||||
.plan = data.plan,
|
||||
.callCount = data.timeStats.getCount(),
|
||||
.totalTime = std::chrono::microseconds{ static_cast<long long>(data.timeStats.getMean() * static_cast<double>(data.timeStats.getCount())) },
|
||||
.meanTime = std::chrono::microseconds{ static_cast<long long>(data.timeStats.getMean()) },
|
||||
.stdDevTime = std::chrono::microseconds{ static_cast<long long>(data.timeStats.getSampleStdDev()) },
|
||||
};
|
||||
visitor(stats);
|
||||
}
|
||||
}
|
||||
|
||||
void QueryPlanRecorder::recordQueryPlanIfNeeded(Wt::Dbo::Session& session, const std::string& query)
|
||||
void QueryProfiler::recordQueryPlan(Wt::Dbo::Session& session, const std::string& query)
|
||||
{
|
||||
{
|
||||
const std::shared_lock lock{ _mutex };
|
||||
|
||||
if (_queryPlans.contains(query))
|
||||
return;
|
||||
}
|
||||
|
||||
Wt::Dbo::Transaction transaction{ session };
|
||||
|
||||
Wt::Dbo::SqlConnection* connection{ transaction.connection() };
|
||||
@@ -106,7 +109,23 @@ namespace lms::db
|
||||
|
||||
{
|
||||
const std::unique_lock lock{ _mutex };
|
||||
_queryPlans.try_emplace(query, std::move(result));
|
||||
_queries[query].plan = std::move(result);
|
||||
}
|
||||
}
|
||||
|
||||
void QueryProfiler::recordQueryExecution(Wt::Dbo::Session& session, const std::string& query, Clock::duration elapsed)
|
||||
{
|
||||
bool needQueryPlan{};
|
||||
const double elapsedUs{ std::chrono::duration_cast<std::chrono::duration<double, std::micro>>(elapsed).count() };
|
||||
{
|
||||
std::unique_lock lock{ _mutex };
|
||||
|
||||
auto& queryStats{ _queries[query] };
|
||||
queryStats.timeStats.add(elapsedUs);
|
||||
needQueryPlan = queryStats.plan.empty();
|
||||
}
|
||||
|
||||
if (needQueryPlan)
|
||||
recordQueryPlan(session, query);
|
||||
}
|
||||
} // namespace lms::db
|
||||
+18
-9
@@ -25,24 +25,33 @@
|
||||
|
||||
#include <Wt/Dbo/Session.h>
|
||||
|
||||
#include "database/IQueryPlanRecorder.hpp"
|
||||
#include "database/profiling/IQueryProfiler.hpp"
|
||||
#include "math/StatsAccumulator.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class QueryPlanRecorder : public IQueryPlanRecorder
|
||||
class QueryProfiler : public IQueryProfiler
|
||||
{
|
||||
public:
|
||||
QueryPlanRecorder();
|
||||
~QueryPlanRecorder() override;
|
||||
QueryPlanRecorder(const QueryPlanRecorder&) = delete;
|
||||
QueryPlanRecorder& operator=(const QueryPlanRecorder&) = delete;
|
||||
QueryProfiler();
|
||||
~QueryProfiler() override;
|
||||
QueryProfiler(const QueryProfiler&) = delete;
|
||||
QueryProfiler& operator=(const QueryProfiler&) = delete;
|
||||
|
||||
void visitQueryPlans(const QueryPlanVisitor& visitor) const override;
|
||||
void visitQueries(const QueryVisitor& visitor) const override;
|
||||
|
||||
void recordQueryPlanIfNeeded(Wt::Dbo::Session& session, const std::string& query);
|
||||
void recordQueryExecution(Wt::Dbo::Session& session, const std::string& query, Clock::duration elapsed);
|
||||
|
||||
private:
|
||||
void recordQueryPlan(Wt::Dbo::Session& session, const std::string& query);
|
||||
|
||||
struct QueryData
|
||||
{
|
||||
std::string plan;
|
||||
math::StatsAccumulator<double> timeStats; // in Us
|
||||
};
|
||||
|
||||
mutable std::shared_mutex _mutex;
|
||||
std::map<std::string, std::string> _queryPlans;
|
||||
std::map<std::string, QueryData> _queries;
|
||||
};
|
||||
} // namespace lms::db
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 <cassert>
|
||||
#include <string>
|
||||
|
||||
#include "core/Service.hpp"
|
||||
#include "database/profiling/IQueryProfiler.hpp"
|
||||
|
||||
#include "profiling/QueryProfiler.hpp"
|
||||
|
||||
namespace lms::db::utils
|
||||
{
|
||||
template<typename Query>
|
||||
class ScopedQueryProfiler
|
||||
{
|
||||
public:
|
||||
explicit ScopedQueryProfiler(const Query& query)
|
||||
: _recorder{ static_cast<QueryProfiler*>(core::Service<IQueryProfiler>::get()) }
|
||||
{
|
||||
if (_recorder)
|
||||
{
|
||||
_query = &query;
|
||||
_start = IQueryProfiler::Clock::now();
|
||||
}
|
||||
}
|
||||
|
||||
~ScopedQueryProfiler()
|
||||
{
|
||||
if (_recorder)
|
||||
{
|
||||
if (_active)
|
||||
_elapsed += IQueryProfiler::Clock::now() - _start;
|
||||
_recorder->recordQueryExecution(_query->session(), _query->asString(), _elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
ScopedQueryProfiler(const ScopedQueryProfiler&) = delete;
|
||||
ScopedQueryProfiler& operator=(const ScopedQueryProfiler&) = delete;
|
||||
|
||||
void suspend()
|
||||
{
|
||||
if (_recorder)
|
||||
{
|
||||
assert(_active);
|
||||
_elapsed += IQueryProfiler::Clock::now() - _start;
|
||||
_active = false;
|
||||
}
|
||||
}
|
||||
|
||||
void resume()
|
||||
{
|
||||
if (_recorder)
|
||||
{
|
||||
assert(!_active);
|
||||
_start = IQueryProfiler::Clock::now();
|
||||
_active = true;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
QueryProfiler* _recorder{};
|
||||
const Query* _query{};
|
||||
IQueryProfiler::Clock::time_point _start;
|
||||
IQueryProfiler::Clock::duration _elapsed{};
|
||||
bool _active{ true };
|
||||
};
|
||||
} // namespace lms::db::utils
|
||||
@@ -29,7 +29,6 @@
|
||||
#include <Wt/Dbo/collection.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "core/EnumSet.hpp"
|
||||
#include "core/UUID.hpp"
|
||||
|
||||
#include "database/IdRange.hpp"
|
||||
@@ -39,7 +38,6 @@
|
||||
#include "database/objects/ArtworkId.hpp"
|
||||
#include "database/objects/Filters.hpp"
|
||||
#include "database/objects/MediaLibraryId.hpp"
|
||||
#include "database/objects/ReleaseId.hpp"
|
||||
#include "database/objects/TrackId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
#include "database/objects/UserId.hpp"
|
||||
@@ -148,9 +146,6 @@ namespace lms::db
|
||||
ObjectPtr<Artwork> getPreferredArtwork() const;
|
||||
ArtworkId getPreferredArtworkId() const;
|
||||
|
||||
// No artistLinkTypes means get them all
|
||||
RangeResults<ArtistId> findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes = {}, std::optional<Range> range = std::nullopt) const;
|
||||
|
||||
// Get the cluster of the tracks made by this artist
|
||||
// Each clusters are grouped by cluster type, sorted by the number of occurence
|
||||
// size is the max number of cluster per cluster type
|
||||
|
||||
@@ -349,8 +349,6 @@ namespace lms::db
|
||||
void visitTrackArtists(TrackArtistLinkType type, std::function<void(const ObjectPtr<Artist>&)> visitor) const;
|
||||
std::vector<ArtistId> getTrackArtistIds(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
|
||||
bool hasVariousArtists() const;
|
||||
std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
|
||||
@@ -50,11 +50,11 @@ namespace lms::db
|
||||
};
|
||||
|
||||
// Do not modify values (just add)
|
||||
enum class SimilarityEngineType
|
||||
enum class RecommendationEngineType
|
||||
{
|
||||
Clusters = 0,
|
||||
Features,
|
||||
None,
|
||||
None = 2,
|
||||
AudioSimilarity = 3,
|
||||
};
|
||||
|
||||
ScanSettings() = default;
|
||||
@@ -65,10 +65,11 @@ namespace lms::db
|
||||
// Getters
|
||||
std::size_t getAudioScanVersion() const { return _audioScanVersion; }
|
||||
std::size_t getArtistInfoScanVersion() const { return _artistInfoScanVersion; }
|
||||
std::string_view getMusicNNModelIdentifier() const { return _musicnnModelIdentifier; }
|
||||
Wt::WTime getUpdateStartTime() const { return _startTime; }
|
||||
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
|
||||
std::vector<std::string_view> getExtraTagsToScan() const;
|
||||
SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
|
||||
RecommendationEngineType getRecommendationEngineType() const { return _recommendationEngineType; }
|
||||
std::vector<std::string> getArtistTagDelimiters() const;
|
||||
std::vector<std::string> getDefaultTagDelimiters() const;
|
||||
std::vector<std::string> getArtistsToNotSplit() const;
|
||||
@@ -80,14 +81,14 @@ namespace lms::db
|
||||
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
|
||||
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
|
||||
void setExtraTagsToScan(std::span<const std::string_view> extraTags);
|
||||
void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
|
||||
void setRecommendationEngineType(RecommendationEngineType type) { _recommendationEngineType = type; }
|
||||
void setArtistTagDelimiters(std::span<const std::string_view> delimiters);
|
||||
void setArtistsToNotSplit(std::span<const std::string_view> artists);
|
||||
void setDefaultTagDelimiters(std::span<const std::string_view> delimiters);
|
||||
void setSkipSingleReleasePlayLists(bool value);
|
||||
void setAllowMBIDArtistMerge(bool value);
|
||||
void setArtistImageFallbackToReleaseField(bool value);
|
||||
|
||||
void setMusicNNModelIdentifier(std::string_view identifier) { _musicnnModelIdentifier = identifier; }
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
@@ -96,7 +97,7 @@ namespace lms::db
|
||||
Wt::Dbo::field(a, _artistInfoScanVersion, "artist_info_scan_version");
|
||||
Wt::Dbo::field(a, _startTime, "start_time");
|
||||
Wt::Dbo::field(a, _updatePeriod, "update_period");
|
||||
Wt::Dbo::field(a, _similarityEngineType, "similarity_engine_type");
|
||||
Wt::Dbo::field(a, _recommendationEngineType, "recommendation_engine_type");
|
||||
Wt::Dbo::field(a, _extraTagsToScan, "extra_tags_to_scan");
|
||||
Wt::Dbo::field(a, _artistTagDelimiters, "artist_tag_delimiters");
|
||||
Wt::Dbo::field(a, _artistsToNotSplit, "artists_to_not_split");
|
||||
@@ -104,6 +105,7 @@ namespace lms::db
|
||||
Wt::Dbo::field(a, _skipSingleReleasePlayLists, "skip_single_release_playlists");
|
||||
Wt::Dbo::field(a, _allowMBIDArtistMerge, "allow_mbid_artist_merge");
|
||||
Wt::Dbo::field(a, _artistImageFallbackToReleaseField, "artist_image_fallback_to_release");
|
||||
Wt::Dbo::field(a, _musicnnModelIdentifier, "musicnn_model_identifier");
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -119,7 +121,7 @@ namespace lms::db
|
||||
int _artistInfoScanVersion{};
|
||||
Wt::WTime _startTime = Wt::WTime{ 0, 0, 0 };
|
||||
UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
|
||||
SimilarityEngineType _similarityEngineType{ SimilarityEngineType::Clusters };
|
||||
RecommendationEngineType _recommendationEngineType{ RecommendationEngineType::Clusters };
|
||||
std::string _extraTagsToScan;
|
||||
std::string _artistTagDelimiters;
|
||||
std::string _artistsToNotSplit;
|
||||
@@ -127,5 +129,6 @@ namespace lms::db
|
||||
bool _skipSingleReleasePlayLists{};
|
||||
bool _allowMBIDArtistMerge{};
|
||||
bool _artistImageFallbackToReleaseField{};
|
||||
std::string _musicnnModelIdentifier;
|
||||
};
|
||||
} // namespace lms::db
|
||||
|
||||
@@ -97,6 +97,8 @@ namespace lms::db
|
||||
DirectoryId directory; // if set, tracks in this directory
|
||||
std::optional<std::size_t> fileSize; // if set, tracks that match this file size
|
||||
TrackEmbeddedImageId embeddedImageId; // if set, tracks that have this embedded image
|
||||
std::optional<bool> hasMusicNNEmbeddings; // If set, tracks that have (or not) MusicNN embeddings
|
||||
TrackId lastTrackId; // If set, tracks that are after this one, must be used with sort by id
|
||||
|
||||
FindParameters& setFilters(const Filters& _filters)
|
||||
{
|
||||
@@ -191,6 +193,16 @@ namespace lms::db
|
||||
embeddedImageId = _embeddedImageId;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setHasMusicNNEmbeddings(std::optional<bool> _hasMusicNNEmbeddings)
|
||||
{
|
||||
hasMusicNNEmbeddings = _hasMusicNNEmbeddings;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setLastTrackId(TrackId _lastTrackId)
|
||||
{
|
||||
lastTrackId = _lastTrackId;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
Track() = default;
|
||||
@@ -203,19 +215,20 @@ namespace lms::db
|
||||
static void find(Session& session, TrackId& lastRetrievedId, std::size_t count, const std::function<void(const Track::pointer&)>& func, MediaLibraryId library = {});
|
||||
static void find(Session& session, const IdRange<TrackId>& idRange, const std::function<void(const Track::pointer&)>& func);
|
||||
static IdRange<TrackId> findNextIdRange(Session& session, TrackId lastRetrievedId, std::size_t count);
|
||||
static void findAbsoluteFilePath(Session& session, TrackId& lastRetrievedId, std::size_t count, const std::function<void(TrackId trackId, const std::filesystem::path& absoluteFilePath)>& func);
|
||||
|
||||
using TrackLocationVisitor = std::function<void(TrackId trackId, const std::filesystem::path& absoluteFilePath)>;
|
||||
static void findAbsoluteFilePath(Session& session, TrackId& lastRetrievedId, std::size_t count, const TrackLocationVisitor& func);
|
||||
static void findAbsoluteFilePath(Session& session, const FindParameters& params, const TrackLocationVisitor& func);
|
||||
|
||||
static bool exists(Session& session, TrackId id);
|
||||
static std::vector<pointer> findByRecordingMBID(Session& session, const core::UUID& MBID);
|
||||
static std::vector<pointer> findByMBID(Session& session, const core::UUID& MBID);
|
||||
static RangeResults<TrackId> findSimilarTrackIds(Session& session, const std::vector<TrackId>& trackIds, std::optional<Range> range = std::nullopt);
|
||||
|
||||
static RangeResults<TrackId> findIds(Session& session, const FindParameters& parameters);
|
||||
static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
|
||||
static void find(Session& session, const FindParameters& parameters, const std::function<void(const Track::pointer&)>& func);
|
||||
static void find(Session& session, const FindParameters& parameters, bool& moreResults, const std::function<void(const Track::pointer&)>& func);
|
||||
static RangeResults<TrackId> findIds(Session& session, const FindParameters& params);
|
||||
static RangeResults<pointer> find(Session& session, const FindParameters& params);
|
||||
static void find(Session& session, const FindParameters& params, const std::function<void(const Track::pointer&)>& func);
|
||||
static void find(Session& session, const FindParameters& params, bool& moreResults, const std::function<void(const Track::pointer&)>& func);
|
||||
static std::size_t getCount(Session& session, const FindParameters& params);
|
||||
static RangeResults<TrackId> findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static RangeResults<TrackId> findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional<Range> range = std::nullopt);
|
||||
|
||||
// Update utility functions
|
||||
static void updatePreferredArtwork(Session& session, TrackId trackId, ArtworkId artworkId);
|
||||
|
||||
+16
-21
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
* Copyright (C) 2026 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -20,10 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include <span>
|
||||
|
||||
#include <Wt/Dbo/Field.h>
|
||||
|
||||
@@ -32,34 +29,33 @@
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/TrackId.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(TrackFeaturesId)
|
||||
LMS_DECLARE_IDTYPE(TrackMusicNNEmbeddingsId)
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class Session;
|
||||
class Track;
|
||||
|
||||
using FeatureName = std::string;
|
||||
using FeatureValues = std::vector<double>;
|
||||
using FeatureValuesMap = std::unordered_map<FeatureName, FeatureValues>;
|
||||
|
||||
class TrackFeatures final : public Object<TrackFeatures, TrackFeaturesId>
|
||||
class TrackMusicNNEmbeddings final : public Object<TrackMusicNNEmbeddings, TrackMusicNNEmbeddingsId>
|
||||
{
|
||||
public:
|
||||
TrackFeatures() = default;
|
||||
TrackMusicNNEmbeddings() = default;
|
||||
|
||||
// Find utilities
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, TrackFeaturesId id);
|
||||
static pointer find(Session& session, TrackMusicNNEmbeddingsId id);
|
||||
static pointer find(Session& session, TrackId trackId);
|
||||
static RangeResults<TrackFeaturesId> find(Session& session, std::optional<Range> range = std::nullopt);
|
||||
|
||||
FeatureValues getFeatureValues(const FeatureName& feature) const;
|
||||
FeatureValuesMap getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const;
|
||||
static RangeResults<TrackMusicNNEmbeddingsId> find(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static void find(Session& session, std::function<void(const pointer&)> func);
|
||||
static void removeAll(Session& session);
|
||||
|
||||
// Accessors
|
||||
std::span<const std::byte> getData() const;
|
||||
TrackId getTrackId() const { return _track.id(); }
|
||||
Wt::Dbo::ptr<Track> getTrack() const { return _track; }
|
||||
|
||||
void setData(std::span<const std::byte> data);
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
@@ -69,11 +65,10 @@ namespace lms::db
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
TrackFeatures(ObjectPtr<Track> track, const std::string& jsonEncodedFeatures);
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures);
|
||||
TrackMusicNNEmbeddings(ObjectPtr<Track> track);
|
||||
static pointer create(Session& session, ObjectPtr<Track> track);
|
||||
|
||||
std::string _data;
|
||||
std::vector<unsigned char> _data;
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
};
|
||||
|
||||
} // namespace lms::db
|
||||
+19
-5
@@ -19,21 +19,35 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
// Due to technical limitations, query plans are recorded globally across all databases.
|
||||
// As a result, this class is implemented as a singleton rather than being owned per DB instance.
|
||||
class IQueryPlanRecorder
|
||||
class IQueryProfiler
|
||||
{
|
||||
public:
|
||||
virtual ~IQueryPlanRecorder() = default;
|
||||
virtual ~IQueryProfiler() = default;
|
||||
|
||||
using QueryPlanVisitor = std::function<void(std::string_view query, std::string_view plan)>;
|
||||
virtual void visitQueryPlans(const QueryPlanVisitor& visitor) const = 0;
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
struct QueryStats
|
||||
{
|
||||
std::string_view query;
|
||||
std::string_view plan;
|
||||
std::size_t callCount{};
|
||||
std::chrono::microseconds totalTime{};
|
||||
std::chrono::microseconds meanTime{};
|
||||
std::chrono::microseconds stdDevTime{};
|
||||
};
|
||||
|
||||
using QueryVisitor = std::function<void(const QueryStats&)>;
|
||||
virtual void visitQueries(const QueryVisitor& visitor) const = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IQueryPlanRecorder> createQueryPlanRecorder();
|
||||
std::unique_ptr<IQueryProfiler> createQueryProfiler();
|
||||
} // namespace lms::db
|
||||
@@ -27,7 +27,6 @@ add_executable(test-database
|
||||
TrackArtistLink.cpp
|
||||
TrackBookmark.cpp
|
||||
TrackEmbeddedImage.cpp
|
||||
TrackFeatures.cpp
|
||||
TrackList.cpp
|
||||
TrackLyrics.cpp
|
||||
User.cpp
|
||||
@@ -36,6 +35,7 @@ add_executable(test-database
|
||||
target_link_libraries(test-database PRIVATE
|
||||
lmsdatabase
|
||||
GTest::GTest
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
if (NOT CMAKE_CROSSCOMPILING)
|
||||
|
||||
@@ -568,81 +568,6 @@ namespace lms::db::tests
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, MultipleTracksSingleClusterSimilarity)
|
||||
{
|
||||
std::list<ScopedTrack> tracks;
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyClusterType" };
|
||||
|
||||
for (std::size_t i{}; i < 10; ++i)
|
||||
{
|
||||
tracks.emplace_back(session);
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
cluster.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto similarTracks{ Track::findSimilarTrackIds(session, { tracks.front().getId() }) };
|
||||
EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1);
|
||||
for (const TrackId similarTrackId : similarTracks.results)
|
||||
{
|
||||
EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, MultipleTracksMultipleClustersSimilarity)
|
||||
{
|
||||
std::list<ScopedTrack> tracks;
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" };
|
||||
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
|
||||
|
||||
for (std::size_t i{}; i < 5; ++i)
|
||||
{
|
||||
tracks.emplace_back(session);
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
cluster1.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t i{ 5 }; i < 10; ++i)
|
||||
{
|
||||
tracks.emplace_back(session);
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
cluster1.get().modify()->addTrack(tracks.back().get());
|
||||
cluster2.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
auto similarTracks{ Track::findSimilarTrackIds(session, { tracks.back().getId() }, Range{ 0, 4 }) };
|
||||
EXPECT_EQ(similarTracks.results.size(), 4);
|
||||
for (const TrackId similarTrackId : similarTracks.results)
|
||||
EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 5), std::next(std::cend(tracks), -1), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks));
|
||||
}
|
||||
|
||||
{
|
||||
auto similarTracks{ Track::findSimilarTrackIds(session, { tracks.front().getId() }) };
|
||||
EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1);
|
||||
for (const TrackId similarTrackId : similarTracks.results)
|
||||
EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster)
|
||||
{
|
||||
ScopedTrack track{ session };
|
||||
@@ -798,143 +723,4 @@ namespace lms::db::tests
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters)
|
||||
{
|
||||
ScopedArtist artist1{ session, "MyArtist1" };
|
||||
ScopedArtist artist2{ session, "MyArtist2" };
|
||||
ScopedArtist artist3{ session, "MyArtist3" };
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" };
|
||||
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(artist1->findSimilarArtistIds().results.size(), 0);
|
||||
EXPECT_EQ(artist2->findSimilarArtistIds().results.size(), 0);
|
||||
EXPECT_EQ(artist3->findSimilarArtistIds().results.size(), 0);
|
||||
}
|
||||
|
||||
std::list<ScopedTrack> tracks;
|
||||
for (std::size_t i{}; i < 10; ++i)
|
||||
{
|
||||
tracks.emplace_back(session);
|
||||
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
if (i < 5)
|
||||
session.create<TrackArtistLink>(tracks.back().get(), artist1.get(), TrackArtistLinkType::Artist);
|
||||
else
|
||||
{
|
||||
session.create<TrackArtistLink>(tracks.back().get(), artist2.get(), TrackArtistLinkType::Artist);
|
||||
cluster2.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
|
||||
cluster1.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
|
||||
tracks.emplace_back(session);
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
session.create<TrackArtistLink>(tracks.back().get(), artist3.get(), TrackArtistLinkType::Artist);
|
||||
cluster2.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
auto artists{ artist1->findSimilarArtistIds() };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist2.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto artists{ artist1->findSimilarArtistIds({ TrackArtistLinkType::Artist }) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist2.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto artists{ artist1->findSimilarArtistIds({ TrackArtistLinkType::Lyricist }) };
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto artists{ artist1->findSimilarArtistIds({ TrackArtistLinkType::Artist, TrackArtistLinkType::Lyricist }) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist2.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto artists{ artist1->findSimilarArtistIds({ TrackArtistLinkType::Composer }) };
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto artists{ artist2->findSimilarArtistIds() };
|
||||
ASSERT_EQ(artists.results.size(), 2);
|
||||
EXPECT_EQ(artists.results[0], artist1.getId());
|
||||
EXPECT_EQ(artists.results[1], artist3.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, MultipleTracksMultipleReleasesMultiClusters)
|
||||
{
|
||||
ScopedRelease release1{ session, "MyRelease1" };
|
||||
ScopedRelease release2{ session, "MyRelease2" };
|
||||
ScopedRelease release3{ session, "MyRelease3" };
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" };
|
||||
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release1->getSimilarReleases().size(), 0);
|
||||
EXPECT_EQ(release2->getSimilarReleases().size(), 0);
|
||||
EXPECT_EQ(release3->getSimilarReleases().size(), 0);
|
||||
}
|
||||
|
||||
std::list<ScopedTrack> tracks;
|
||||
for (std::size_t i{}; i < 10; ++i)
|
||||
{
|
||||
tracks.emplace_back(session);
|
||||
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
if (i < 5)
|
||||
tracks.back().get().modify()->setRelease(release1.get());
|
||||
else
|
||||
{
|
||||
tracks.back().get().modify()->setRelease(release2.get());
|
||||
cluster2.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
|
||||
cluster1.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
|
||||
tracks.emplace_back(session);
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
tracks.back().get().modify()->setRelease(release3.get());
|
||||
cluster2.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
auto releases{ release1->getSimilarReleases() };
|
||||
ASSERT_EQ(releases.size(), 1);
|
||||
EXPECT_EQ(releases.front()->getId(), release2.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto releases{ release2->getSimilarReleases() };
|
||||
ASSERT_EQ(releases.size(), 2);
|
||||
EXPECT_EQ(releases[0]->getId(), release1.getId());
|
||||
EXPECT_EQ(releases[1]->getId(), release3.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
@@ -36,7 +36,6 @@
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
#include "database/objects/TrackBookmark.hpp"
|
||||
#include "database/objects/TrackFeatures.hpp"
|
||||
#include "database/objects/TrackList.hpp"
|
||||
#include "database/objects/User.hpp"
|
||||
|
||||
@@ -142,9 +141,8 @@ namespace lms::db::tests
|
||||
class DatabaseFixture : public ::testing::Test
|
||||
{
|
||||
public:
|
||||
~DatabaseFixture();
|
||||
~DatabaseFixture() override;
|
||||
|
||||
public:
|
||||
static void SetUpTestCase();
|
||||
static void TearDownTestCase();
|
||||
|
||||
|
||||
@@ -102,9 +102,3 @@ namespace lms::db::tests
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 "Common.hpp"
|
||||
|
||||
#include "database/objects/TrackFeatures.hpp"
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
using ScopedTrackFeatures = ScopedEntity<db::TrackFeatures>;
|
||||
|
||||
TEST_F(DatabaseFixture, TrackFeatures)
|
||||
{
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(TrackFeatures::getCount(session), 0);
|
||||
}
|
||||
|
||||
ScopedTrackFeatures trackFeatures{ session, track.lockAndGet(), "" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
EXPECT_EQ(TrackFeatures::getCount(session), 1);
|
||||
|
||||
auto allTrackFeatures{ TrackFeatures::find(session) };
|
||||
ASSERT_EQ(allTrackFeatures.results.size(), 1);
|
||||
EXPECT_EQ(allTrackFeatures.results.front(), trackFeatures.getId());
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
Reference in New Issue
Block a user