Extracted genre, mood, language and grouping from generic clusters
This commit is contained in:
@@ -6,10 +6,14 @@ add_library(lmsdatabase STATIC
|
||||
impl/objects/AuthToken.cpp
|
||||
impl/objects/Cluster.cpp
|
||||
impl/objects/Directory.cpp
|
||||
impl/objects/Genre.cpp
|
||||
impl/objects/Grouping.cpp
|
||||
impl/objects/Image.cpp
|
||||
impl/objects/Language.cpp
|
||||
impl/objects/Listen.cpp
|
||||
impl/objects/MediaLibrary.cpp
|
||||
impl/objects/Medium.cpp
|
||||
impl/objects/Mood.cpp
|
||||
impl/objects/PlayListFile.cpp
|
||||
impl/objects/PlayQueue.cpp
|
||||
impl/objects/Podcast.cpp
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace lms::db
|
||||
{
|
||||
namespace
|
||||
{
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 107 };
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 108 };
|
||||
}
|
||||
|
||||
VersionInfo::VersionInfo()
|
||||
@@ -1774,6 +1774,127 @@ FROM track)");
|
||||
utils::executeCommand(*session.getDboSession(), R"(ALTER TABLE track RENAME COLUMN recording_mbid_new TO recording_mbid)");
|
||||
}
|
||||
|
||||
void migrateFromV107(Session& session)
|
||||
{
|
||||
// Extract Genre, Mood, Language and Grouping from Cluster (keep it for user tags)
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "genre" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"name" text not null,
|
||||
"track_count" integer not null default 0,
|
||||
"release_count" integer not null default 0
|
||||
))");
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_genre" (
|
||||
"track_id" bigint,
|
||||
"genre_id" bigint,
|
||||
primary key ("track_id", "genre_id"),
|
||||
constraint "fk_track_genre_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_track_genre_genre" foreign key ("genre_id") references "genre" ("id") on delete cascade deferrable initially deferred
|
||||
))");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO genre (version, name, track_count, release_count)
|
||||
SELECT 0, c.name, c.track_count, c.release_count FROM cluster c
|
||||
INNER JOIN cluster_type ct ON ct.id = c.cluster_type_id
|
||||
WHERE ct.name = 'GENRE')");
|
||||
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO track_genre (track_id, genre_id)
|
||||
SELECT tc.track_id, g.id FROM track_cluster tc
|
||||
INNER JOIN cluster c ON c.id = tc.cluster_id
|
||||
INNER JOIN cluster_type ct ON ct.id = c.cluster_type_id
|
||||
INNER JOIN genre g ON g.name = c.name
|
||||
WHERE ct.name = 'GENRE')");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "track_genre_genre" ON "track_genre" ("genre_id"))");
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "track_genre_track" ON "track_genre" ("track_id"))");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "mood" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"name" text not null
|
||||
))");
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_mood" (
|
||||
"track_id" bigint,
|
||||
"mood_id" bigint,
|
||||
primary key ("track_id", "mood_id"),
|
||||
constraint "fk_track_mood_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_track_mood_mood" foreign key ("mood_id") references "mood" ("id") on delete cascade deferrable initially deferred
|
||||
))");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO mood (version, name)
|
||||
SELECT 0, c.name FROM cluster c
|
||||
INNER JOIN cluster_type ct ON ct.id = c.cluster_type_id
|
||||
WHERE ct.name = 'MOOD')");
|
||||
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO track_mood (track_id, mood_id)
|
||||
SELECT tc.track_id, m.id FROM track_cluster tc
|
||||
INNER JOIN cluster c ON c.id = tc.cluster_id
|
||||
INNER JOIN cluster_type ct ON ct.id = c.cluster_type_id
|
||||
INNER JOIN mood m ON m.name = c.name
|
||||
WHERE ct.name = 'MOOD')");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "track_mood_mood" ON "track_mood" ("mood_id"))");
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "track_mood_track" ON "track_mood" ("track_id"))");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "language" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"name" text not null
|
||||
))");
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_language" (
|
||||
"track_id" bigint,
|
||||
"language_id" bigint,
|
||||
primary key ("track_id", "language_id"),
|
||||
constraint "fk_track_language_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_track_language_language" foreign key ("language_id") references "language" ("id") on delete cascade deferrable initially deferred
|
||||
))");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO language (version, name)
|
||||
SELECT 0, c.name FROM cluster c
|
||||
INNER JOIN cluster_type ct ON ct.id = c.cluster_type_id
|
||||
WHERE ct.name = 'LANGUAGE')");
|
||||
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO track_language (track_id, language_id)
|
||||
SELECT tc.track_id, l.id FROM track_cluster tc
|
||||
INNER JOIN cluster c ON c.id = tc.cluster_id
|
||||
INNER JOIN cluster_type ct ON ct.id = c.cluster_type_id
|
||||
INNER JOIN language l ON l.name = c.name
|
||||
WHERE ct.name = 'LANGUAGE')");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "track_language_language" ON "track_language" ("language_id"))");
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "track_language_track" ON "track_language" ("track_id"))");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "grouping" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"name" text not null
|
||||
))");
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_grouping" (
|
||||
"track_id" bigint,
|
||||
"grouping_id" bigint,
|
||||
primary key ("track_id", "grouping_id"),
|
||||
constraint "fk_track_grouping_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_track_grouping_grouping" foreign key ("grouping_id") references "grouping" ("id") on delete cascade deferrable initially deferred
|
||||
))");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO grouping (version, name)
|
||||
SELECT 0, c.name FROM cluster c
|
||||
INNER JOIN cluster_type ct ON ct.id = c.cluster_type_id
|
||||
WHERE ct.name = 'GROUPING')");
|
||||
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO track_grouping (track_id, grouping_id)
|
||||
SELECT tc.track_id, g.id FROM track_cluster tc
|
||||
INNER JOIN cluster c ON c.id = tc.cluster_id
|
||||
INNER JOIN cluster_type ct ON ct.id = c.cluster_type_id
|
||||
INNER JOIN grouping g ON g.name = c.name
|
||||
WHERE ct.name = 'GROUPING')");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "track_grouping_grouping" ON "track_grouping" ("grouping_id"))");
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "track_grouping_track" ON "track_grouping" ("track_id"))");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), R"(DELETE FROM track_cluster WHERE cluster_id IN (SELECT c.id FROM cluster c INNER JOIN cluster_type ct ON ct.id = c.cluster_type_id WHERE ct.name IN ('GENRE', 'MOOD', 'LANGUAGE', 'GROUPING')))");
|
||||
utils::executeCommand(*session.getDboSession(), R"(ALTER TABLE cluster DROP COLUMN track_count)");
|
||||
utils::executeCommand(*session.getDboSession(), R"(ALTER TABLE cluster DROP COLUMN release_count)");
|
||||
utils::executeCommand(*session.getDboSession(), R"(DELETE FROM cluster WHERE cluster_type_id IN (SELECT id FROM cluster_type WHERE name IN ('GENRE', 'MOOD', 'LANGUAGE', 'GROUPING')))");
|
||||
utils::executeCommand(*session.getDboSession(), R"(DELETE FROM cluster_type WHERE name IN ('GENRE', 'MOOD', 'LANGUAGE', 'GROUPING'))");
|
||||
}
|
||||
|
||||
bool doDbMigration(Session& session)
|
||||
{
|
||||
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||
@@ -1857,6 +1978,7 @@ FROM track)");
|
||||
{ 104, migrateFromV104 },
|
||||
{ 105, migrateFromV105 },
|
||||
{ 106, migrateFromV106 },
|
||||
{ 107, migrateFromV107 },
|
||||
};
|
||||
|
||||
bool migrationPerformed{};
|
||||
|
||||
@@ -30,10 +30,14 @@
|
||||
#include "database/objects/AuthToken.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Image.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/Listen.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/PlayListFile.hpp"
|
||||
#include "database/objects/PlayQueue.hpp"
|
||||
#include "database/objects/Podcast.hpp"
|
||||
@@ -81,6 +85,10 @@ namespace lms::db
|
||||
_session.mapClass<AuthToken>("auth_token");
|
||||
_session.mapClass<Cluster>("cluster");
|
||||
_session.mapClass<ClusterType>("cluster_type");
|
||||
_session.mapClass<Genre>("genre");
|
||||
_session.mapClass<Grouping>("grouping");
|
||||
_session.mapClass<Language>("language");
|
||||
_session.mapClass<Mood>("mood");
|
||||
_session.mapClass<Country>("country");
|
||||
_session.mapClass<Directory>("directory");
|
||||
_session.mapClass<Image>("image");
|
||||
@@ -210,6 +218,11 @@ namespace lms::db
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
|
||||
|
||||
utils::executeCommand(_session, "CREATE UNIQUE INDEX IF NOT EXISTS genre_name_idx ON genre(name)");
|
||||
utils::executeCommand(_session, "CREATE UNIQUE INDEX IF NOT EXISTS grouping_name_idx ON grouping(name)");
|
||||
utils::executeCommand(_session, "CREATE UNIQUE INDEX IF NOT EXISTS language_name_idx ON language(name)");
|
||||
utils::executeCommand(_session, "CREATE UNIQUE INDEX IF NOT EXISTS mood_name_idx ON mood(name)");
|
||||
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS country_id_idx ON country(id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS country_name_idx ON country(name COLLATE NOCASE)");
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/User.hpp"
|
||||
@@ -55,6 +59,10 @@ namespace lms::db
|
||||
|| params.trackArtistLinkType.has_value()
|
||||
|| params.track.isValid()
|
||||
|| params.filters.clusters.size() == 1
|
||||
|| params.filters.genre.isValid()
|
||||
|| params.filters.grouping.isValid()
|
||||
|| params.filters.language.isValid()
|
||||
|| params.filters.mood.isValid()
|
||||
|| params.filters.codec.has_value()
|
||||
|| params.filters.label.isValid()
|
||||
|| params.filters.releaseType.isValid())
|
||||
@@ -162,6 +170,34 @@ namespace lms::db
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
if (params.filters.genre.isValid())
|
||||
{
|
||||
query.join("track_genre t_g ON t_g.track_id = t_a_l.track_id")
|
||||
.where("t_g.genre_id = ?")
|
||||
.bind(params.filters.genre);
|
||||
}
|
||||
|
||||
if (params.filters.grouping.isValid())
|
||||
{
|
||||
query.join("track_grouping t_gr ON t_gr.track_id = t_a_l.track_id")
|
||||
.where("t_gr.grouping_id = ?")
|
||||
.bind(params.filters.grouping);
|
||||
}
|
||||
|
||||
if (params.filters.language.isValid())
|
||||
{
|
||||
query.join("track_language t_l ON t_l.track_id = t_a_l.track_id")
|
||||
.where("t_l.language_id = ?")
|
||||
.bind(params.filters.language);
|
||||
}
|
||||
|
||||
if (params.filters.mood.isValid())
|
||||
{
|
||||
query.join("track_mood t_m ON t_m.track_id = t_a_l.track_id")
|
||||
.where("t_m.mood_id = ?")
|
||||
.bind(params.filters.mood);
|
||||
}
|
||||
|
||||
if (params.track.isValid())
|
||||
query.where("t_a_l.track_id = ?").bind(params.track);
|
||||
|
||||
|
||||
@@ -25,8 +25,12 @@
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* 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/objects/Genre.hpp"
|
||||
|
||||
#include <Wt/Dbo/Impl.h>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||
#include "database/objects/TrackLyrics.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
#include "traits/IdTypeTraits.hpp"
|
||||
#include "traits/StringViewTraits.hpp"
|
||||
|
||||
DBO_INSTANTIATE_TEMPLATES(lms::db::Genre)
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
namespace
|
||||
{
|
||||
template<typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Genre::FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession()->query<ResultType>("SELECT " + std::string{ itemToSelect } + " FROM genre g") };
|
||||
|
||||
if (params.artist.isValid() || params.track.isValid() || params.release.isValid()
|
||||
|| params.sortMethod == GenreSortMethod::TrackCountDesc)
|
||||
query.join("track_genre t_g ON t_g.genre_id = g.id");
|
||||
|
||||
if (params.track.isValid())
|
||||
query.where("t_g.track_id = ?").bind(params.track);
|
||||
|
||||
if (params.release.isValid() || params.artist.isValid())
|
||||
query.join("track t ON t.id = t_g.track_id");
|
||||
|
||||
if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
|
||||
if (params.artist.isValid())
|
||||
{
|
||||
query.join("track_artist_link t_a_l ON t_a_l.track_id = t.id");
|
||||
query.where("t_a_l.artist_id = ?").bind(params.artist);
|
||||
}
|
||||
|
||||
switch (params.sortMethod)
|
||||
{
|
||||
case GenreSortMethod::None:
|
||||
break;
|
||||
case GenreSortMethod::Name:
|
||||
query.orderBy("g.name COLLATE NOCASE");
|
||||
break;
|
||||
case GenreSortMethod::TrackCountDesc:
|
||||
query.orderBy("COUNT(t_g.track_id) DESC");
|
||||
break;
|
||||
}
|
||||
|
||||
// track_genre has a UNIQUE constraint on (track_id, genre_id), so no duplicates when filtering by track
|
||||
if (!params.track.isValid())
|
||||
query.groupBy("g.id");
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
template<typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Genre::FindParameters& params)
|
||||
{
|
||||
std::string_view itemToSelect;
|
||||
|
||||
if constexpr (std::is_same_v<ResultType, GenreId>)
|
||||
itemToSelect = "g.id";
|
||||
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Genre>>)
|
||||
itemToSelect = "g";
|
||||
else
|
||||
static_assert("Unhandled type");
|
||||
|
||||
return createQuery<ResultType>(session, itemToSelect, params);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Genre::Genre(std::string_view name)
|
||||
: _name{ name.substr(0, maxNameLength) }
|
||||
{
|
||||
LMS_LOG_IF(DB, WARNING, name.size() > maxNameLength, "Genre name too long, truncated to '" << _name << "'");
|
||||
}
|
||||
|
||||
Genre::pointer Genre::create(Session& session, std::string_view name)
|
||||
{
|
||||
return session.getDboSession()->add(std::unique_ptr<Genre>{ new Genre{ name } });
|
||||
}
|
||||
|
||||
std::size_t Genre::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM genre"));
|
||||
}
|
||||
|
||||
RangeResults<GenreId> Genre::findIds(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createQuery<GenreId>(session, params) };
|
||||
return utils::execRangeQuery<GenreId>(query, params.range);
|
||||
}
|
||||
|
||||
RangeResults<Genre::pointer> Genre::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Genre>>(session, params) };
|
||||
return utils::execRangeQuery<Genre::pointer>(query, params.range);
|
||||
}
|
||||
|
||||
void Genre::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Genre>>(session, params) };
|
||||
utils::forEachQueryRangeResult(query, params.range, func);
|
||||
}
|
||||
|
||||
Genre::pointer Genre::find(Session& session, GenreId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<Genre>().where("id = ?").bind(id));
|
||||
}
|
||||
|
||||
Genre::pointer Genre::find(Session& session, std::string_view name)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
if (name.size() > maxNameLength)
|
||||
name = name.substr(0, maxNameLength);
|
||||
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<Genre>().where("name = ?").bind(name));
|
||||
}
|
||||
|
||||
RangeResults<GenreId> Genre::findOrphanIds(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ session.getDboSession()->query<GenreId>("SELECT g.id FROM genre g WHERE NOT EXISTS (SELECT 1 FROM track_genre t_g WHERE t_g.genre_id = g.id)") };
|
||||
return utils::execRangeQuery<GenreId>(query, range);
|
||||
}
|
||||
|
||||
std::size_t Genre::computeTrackCount(Session& session, GenreId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(t.id) FROM track t INNER JOIN track_genre t_g ON t_g.track_id = t.id").where("t_g.genre_id = ?").bind(id));
|
||||
}
|
||||
|
||||
std::size_t Genre::computeReleaseCount(Session& session, GenreId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(DISTINCT t.release_id) FROM track t INNER JOIN track_genre t_g ON t_g.track_id = t.id").where("t_g.genre_id = ?").bind(id));
|
||||
}
|
||||
|
||||
} // namespace lms::db
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* 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/objects/Grouping.hpp"
|
||||
|
||||
#include <Wt/Dbo/Impl.h>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||
#include "database/objects/TrackLyrics.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
#include "traits/IdTypeTraits.hpp"
|
||||
#include "traits/StringViewTraits.hpp"
|
||||
|
||||
DBO_INSTANTIATE_TEMPLATES(lms::db::Grouping)
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
namespace
|
||||
{
|
||||
template<typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Grouping::FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession()->query<ResultType>("SELECT " + std::string{ itemToSelect } + " FROM grouping g") };
|
||||
|
||||
if (params.artist.isValid() || params.track.isValid() || params.release.isValid()
|
||||
|| params.sortMethod == GroupingSortMethod::TrackCountDesc)
|
||||
query.join("track_grouping t_gr ON t_gr.grouping_id = g.id");
|
||||
|
||||
if (params.track.isValid())
|
||||
query.where("t_gr.track_id = ?").bind(params.track);
|
||||
|
||||
if (params.release.isValid() || params.artist.isValid())
|
||||
query.join("track t ON t.id = t_gr.track_id");
|
||||
|
||||
if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
|
||||
if (params.artist.isValid())
|
||||
{
|
||||
query.join("track_artist_link t_a_l ON t_a_l.track_id = t.id");
|
||||
query.where("t_a_l.artist_id = ?").bind(params.artist);
|
||||
}
|
||||
|
||||
switch (params.sortMethod)
|
||||
{
|
||||
case GroupingSortMethod::None:
|
||||
break;
|
||||
case GroupingSortMethod::Name:
|
||||
query.orderBy("g.name COLLATE NOCASE");
|
||||
break;
|
||||
case GroupingSortMethod::TrackCountDesc:
|
||||
query.orderBy("COUNT(t_gr.track_id) DESC");
|
||||
break;
|
||||
}
|
||||
|
||||
// track_grouping has a UNIQUE constraint on (track_id, grouping_id), so no duplicates when filtering by track
|
||||
if (!params.track.isValid())
|
||||
query.groupBy("g.id");
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
template<typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Grouping::FindParameters& params)
|
||||
{
|
||||
std::string_view itemToSelect;
|
||||
|
||||
if constexpr (std::is_same_v<ResultType, GroupingId>)
|
||||
itemToSelect = "g.id";
|
||||
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Grouping>>)
|
||||
itemToSelect = "g";
|
||||
else
|
||||
static_assert("Unhandled type");
|
||||
|
||||
return createQuery<ResultType>(session, itemToSelect, params);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Grouping::Grouping(std::string_view name)
|
||||
: _name{ name.substr(0, maxNameLength) }
|
||||
{
|
||||
LMS_LOG_IF(DB, WARNING, name.size() > maxNameLength, "Grouping name too long, truncated to '" << _name << "'");
|
||||
}
|
||||
|
||||
Grouping::pointer Grouping::create(Session& session, std::string_view name)
|
||||
{
|
||||
return session.getDboSession()->add(std::unique_ptr<Grouping>{ new Grouping{ name } });
|
||||
}
|
||||
|
||||
std::size_t Grouping::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM grouping"));
|
||||
}
|
||||
|
||||
RangeResults<GroupingId> Grouping::findIds(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createQuery<GroupingId>(session, params) };
|
||||
return utils::execRangeQuery<GroupingId>(query, params.range);
|
||||
}
|
||||
|
||||
RangeResults<Grouping::pointer> Grouping::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Grouping>>(session, params) };
|
||||
return utils::execRangeQuery<Grouping::pointer>(query, params.range);
|
||||
}
|
||||
|
||||
void Grouping::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Grouping>>(session, params) };
|
||||
utils::forEachQueryRangeResult(query, params.range, func);
|
||||
}
|
||||
|
||||
Grouping::pointer Grouping::find(Session& session, GroupingId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<Grouping>().where("id = ?").bind(id));
|
||||
}
|
||||
|
||||
Grouping::pointer Grouping::find(Session& session, std::string_view name)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
if (name.size() > maxNameLength)
|
||||
name = name.substr(0, maxNameLength);
|
||||
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<Grouping>().where("name = ?").bind(name));
|
||||
}
|
||||
|
||||
RangeResults<GroupingId> Grouping::findOrphanIds(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ session.getDboSession()->query<GroupingId>("SELECT g.id FROM grouping g WHERE NOT EXISTS (SELECT 1 FROM track_grouping t_gr WHERE t_gr.grouping_id = g.id)") };
|
||||
return utils::execRangeQuery<GroupingId>(query, range);
|
||||
}
|
||||
|
||||
} // namespace lms::db
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* 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/objects/Language.hpp"
|
||||
|
||||
#include <Wt/Dbo/Impl.h>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||
#include "database/objects/TrackLyrics.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
#include "traits/IdTypeTraits.hpp"
|
||||
#include "traits/StringViewTraits.hpp"
|
||||
|
||||
DBO_INSTANTIATE_TEMPLATES(lms::db::Language)
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
namespace
|
||||
{
|
||||
template<typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Language::FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession()->query<ResultType>("SELECT " + std::string{ itemToSelect } + " FROM language l") };
|
||||
|
||||
if (params.artist.isValid() || params.track.isValid() || params.release.isValid()
|
||||
|| params.sortMethod == LanguageSortMethod::TrackCountDesc)
|
||||
query.join("track_language t_l ON t_l.language_id = l.id");
|
||||
|
||||
if (params.track.isValid())
|
||||
query.where("t_l.track_id = ?").bind(params.track);
|
||||
|
||||
if (params.release.isValid() || params.artist.isValid())
|
||||
query.join("track t ON t.id = t_l.track_id");
|
||||
|
||||
if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
|
||||
if (params.artist.isValid())
|
||||
{
|
||||
query.join("track_artist_link t_a_l ON t_a_l.track_id = t.id");
|
||||
query.where("t_a_l.artist_id = ?").bind(params.artist);
|
||||
}
|
||||
|
||||
switch (params.sortMethod)
|
||||
{
|
||||
case LanguageSortMethod::None:
|
||||
break;
|
||||
case LanguageSortMethod::Name:
|
||||
query.orderBy("l.name COLLATE NOCASE");
|
||||
break;
|
||||
case LanguageSortMethod::TrackCountDesc:
|
||||
query.orderBy("COUNT(t_l.track_id) DESC");
|
||||
break;
|
||||
}
|
||||
|
||||
// track_language has a UNIQUE constraint on (track_id, language_id), so no duplicates when filtering by track
|
||||
if (!params.track.isValid())
|
||||
query.groupBy("l.id");
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
template<typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Language::FindParameters& params)
|
||||
{
|
||||
std::string_view itemToSelect;
|
||||
|
||||
if constexpr (std::is_same_v<ResultType, LanguageId>)
|
||||
itemToSelect = "l.id";
|
||||
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Language>>)
|
||||
itemToSelect = "l";
|
||||
else
|
||||
static_assert("Unhandled type");
|
||||
|
||||
return createQuery<ResultType>(session, itemToSelect, params);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Language::Language(std::string_view name)
|
||||
: _name{ name.substr(0, maxNameLength) }
|
||||
{
|
||||
LMS_LOG_IF(DB, WARNING, name.size() > maxNameLength, "Language name too long, truncated to '" << _name << "'");
|
||||
}
|
||||
|
||||
Language::pointer Language::create(Session& session, std::string_view name)
|
||||
{
|
||||
return session.getDboSession()->add(std::unique_ptr<Language>{ new Language{ name } });
|
||||
}
|
||||
|
||||
std::size_t Language::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM language"));
|
||||
}
|
||||
|
||||
RangeResults<LanguageId> Language::findIds(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createQuery<LanguageId>(session, params) };
|
||||
return utils::execRangeQuery<LanguageId>(query, params.range);
|
||||
}
|
||||
|
||||
RangeResults<Language::pointer> Language::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Language>>(session, params) };
|
||||
return utils::execRangeQuery<Language::pointer>(query, params.range);
|
||||
}
|
||||
|
||||
void Language::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Language>>(session, params) };
|
||||
utils::forEachQueryRangeResult(query, params.range, func);
|
||||
}
|
||||
|
||||
Language::pointer Language::find(Session& session, LanguageId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<Language>().where("id = ?").bind(id));
|
||||
}
|
||||
|
||||
Language::pointer Language::find(Session& session, std::string_view name)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
if (name.size() > maxNameLength)
|
||||
name = name.substr(0, maxNameLength);
|
||||
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<Language>().where("name = ?").bind(name));
|
||||
}
|
||||
|
||||
RangeResults<LanguageId> Language::findOrphanIds(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ session.getDboSession()->query<LanguageId>("SELECT l.id FROM language l WHERE NOT EXISTS (SELECT 1 FROM track_language t_l WHERE t_l.language_id = l.id)") };
|
||||
return utils::execRangeQuery<LanguageId>(query, range);
|
||||
}
|
||||
|
||||
} // namespace lms::db
|
||||
@@ -23,6 +23,10 @@
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/User.hpp"
|
||||
|
||||
@@ -53,7 +57,11 @@ namespace lms::db
|
||||
|| params.filters.codec.has_value()
|
||||
|| params.filters.label.isValid()
|
||||
|| params.filters.releaseType.isValid()
|
||||
|| params.trackArtistLinkType.has_value())
|
||||
|| params.trackArtistLinkType.has_value()
|
||||
|| params.filters.genre.isValid()
|
||||
|| params.filters.grouping.isValid()
|
||||
|| params.filters.language.isValid()
|
||||
|| params.filters.mood.isValid())
|
||||
{
|
||||
query.join("track t ON t.id = t_a_l.track_id");
|
||||
|
||||
@@ -74,6 +82,30 @@ namespace lms::db
|
||||
query.join("release_release_type r_r_t ON r_r_t.release_id = t.release_id");
|
||||
query.where("r_r_t.release_type_id = ?").bind(params.filters.releaseType);
|
||||
}
|
||||
|
||||
if (params.filters.genre.isValid())
|
||||
{
|
||||
query.join("track_genre t_g ON t_g.track_id = t.id");
|
||||
query.where("t_g.genre_id = ?").bind(params.filters.genre);
|
||||
}
|
||||
|
||||
if (params.filters.grouping.isValid())
|
||||
{
|
||||
query.join("track_grouping t_gr ON t_gr.track_id = t.id");
|
||||
query.where("t_gr.grouping_id = ?").bind(params.filters.grouping);
|
||||
}
|
||||
|
||||
if (params.filters.language.isValid())
|
||||
{
|
||||
query.join("track_language t_l ON t_l.track_id = t.id");
|
||||
query.where("t_l.language_id = ?").bind(params.filters.language);
|
||||
}
|
||||
|
||||
if (params.filters.mood.isValid())
|
||||
{
|
||||
query.join("track_mood t_m ON t_m.track_id = t.id");
|
||||
query.where("t_m.mood_id = ?").bind(params.filters.mood);
|
||||
}
|
||||
}
|
||||
|
||||
if (params.releaseArtistsOnly)
|
||||
@@ -156,6 +188,30 @@ namespace lms::db
|
||||
query.where("r_r_t.release_type_id = ?").bind(params.filters.releaseType);
|
||||
}
|
||||
|
||||
if (params.filters.genre.isValid())
|
||||
{
|
||||
query.join("track_genre t_g ON t_g.track_id = t.id");
|
||||
query.where("t_g.genre_id = ?").bind(params.filters.genre);
|
||||
}
|
||||
|
||||
if (params.filters.grouping.isValid())
|
||||
{
|
||||
query.join("track_grouping t_gr ON t_gr.track_id = t.id");
|
||||
query.where("t_gr.grouping_id = ?").bind(params.filters.grouping);
|
||||
}
|
||||
|
||||
if (params.filters.language.isValid())
|
||||
{
|
||||
query.join("track_language t_l ON t_l.track_id = t.id");
|
||||
query.where("t_l.language_id = ?").bind(params.filters.language);
|
||||
}
|
||||
|
||||
if (params.filters.mood.isValid())
|
||||
{
|
||||
query.join("track_mood t_m ON t_m.track_id = t.id");
|
||||
query.where("t_m.mood_id = ?").bind(params.filters.mood);
|
||||
}
|
||||
|
||||
if (!params.filters.clusters.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
@@ -215,6 +271,30 @@ namespace lms::db
|
||||
query.where("r_r_t.release_type_id = ?").bind(params.filters.releaseType);
|
||||
}
|
||||
|
||||
if (params.filters.genre.isValid())
|
||||
{
|
||||
query.join("track_genre t_g ON t_g.track_id = t.id");
|
||||
query.where("t_g.genre_id = ?").bind(params.filters.genre);
|
||||
}
|
||||
|
||||
if (params.filters.grouping.isValid())
|
||||
{
|
||||
query.join("track_grouping t_gr ON t_gr.track_id = t.id");
|
||||
query.where("t_gr.grouping_id = ?").bind(params.filters.grouping);
|
||||
}
|
||||
|
||||
if (params.filters.language.isValid())
|
||||
{
|
||||
query.join("track_language t_l ON t_l.track_id = t.id");
|
||||
query.where("t_l.language_id = ?").bind(params.filters.language);
|
||||
}
|
||||
|
||||
if (params.filters.mood.isValid())
|
||||
{
|
||||
query.join("track_mood t_m ON t_m.track_id = t.id");
|
||||
query.where("t_m.mood_id = ?").bind(params.filters.mood);
|
||||
}
|
||||
|
||||
if (!params.filters.clusters.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
#include <Wt/Dbo/Impl.h>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
@@ -26,7 +26,11 @@
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* 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/objects/Mood.hpp"
|
||||
|
||||
#include <Wt/Dbo/Impl.h>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||
#include "database/objects/TrackLyrics.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
#include "traits/IdTypeTraits.hpp"
|
||||
#include "traits/StringViewTraits.hpp"
|
||||
|
||||
DBO_INSTANTIATE_TEMPLATES(lms::db::Mood)
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
namespace
|
||||
{
|
||||
template<typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Mood::FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession()->query<ResultType>("SELECT " + std::string{ itemToSelect } + " FROM mood m") };
|
||||
|
||||
if (params.artist.isValid() || params.track.isValid() || params.release.isValid()
|
||||
|| params.sortMethod == MoodSortMethod::TrackCountDesc)
|
||||
query.join("track_mood t_m ON t_m.mood_id = m.id");
|
||||
|
||||
if (params.track.isValid())
|
||||
query.where("t_m.track_id = ?").bind(params.track);
|
||||
|
||||
if (params.release.isValid() || params.artist.isValid())
|
||||
query.join("track t ON t.id = t_m.track_id");
|
||||
|
||||
if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
|
||||
if (params.artist.isValid())
|
||||
{
|
||||
query.join("track_artist_link t_a_l ON t_a_l.track_id = t.id");
|
||||
query.where("t_a_l.artist_id = ?").bind(params.artist);
|
||||
}
|
||||
|
||||
switch (params.sortMethod)
|
||||
{
|
||||
case MoodSortMethod::None:
|
||||
break;
|
||||
case MoodSortMethod::Name:
|
||||
query.orderBy("m.name COLLATE NOCASE");
|
||||
break;
|
||||
case MoodSortMethod::TrackCountDesc:
|
||||
query.orderBy("COUNT(t_m.track_id) DESC");
|
||||
break;
|
||||
}
|
||||
|
||||
// track_mood has a UNIQUE constraint on (track_id, mood_id), so no duplicates when filtering by track
|
||||
if (!params.track.isValid())
|
||||
query.groupBy("m.id");
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
template<typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Mood::FindParameters& params)
|
||||
{
|
||||
std::string_view itemToSelect;
|
||||
|
||||
if constexpr (std::is_same_v<ResultType, MoodId>)
|
||||
itemToSelect = "m.id";
|
||||
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Mood>>)
|
||||
itemToSelect = "m";
|
||||
else
|
||||
static_assert("Unhandled type");
|
||||
|
||||
return createQuery<ResultType>(session, itemToSelect, params);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Mood::Mood(std::string_view name)
|
||||
: _name{ name.substr(0, maxNameLength) }
|
||||
{
|
||||
LMS_LOG_IF(DB, WARNING, name.size() > maxNameLength, "Mood name too long, truncated to '" << _name << "'");
|
||||
}
|
||||
|
||||
Mood::pointer Mood::create(Session& session, std::string_view name)
|
||||
{
|
||||
return session.getDboSession()->add(std::unique_ptr<Mood>{ new Mood{ name } });
|
||||
}
|
||||
|
||||
std::size_t Mood::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM mood"));
|
||||
}
|
||||
|
||||
RangeResults<MoodId> Mood::findIds(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createQuery<MoodId>(session, params) };
|
||||
return utils::execRangeQuery<MoodId>(query, params.range);
|
||||
}
|
||||
|
||||
RangeResults<Mood::pointer> Mood::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Mood>>(session, params) };
|
||||
return utils::execRangeQuery<Mood::pointer>(query, params.range);
|
||||
}
|
||||
|
||||
void Mood::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Mood>>(session, params) };
|
||||
utils::forEachQueryRangeResult(query, params.range, func);
|
||||
}
|
||||
|
||||
Mood::pointer Mood::find(Session& session, MoodId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<Mood>().where("id = ?").bind(id));
|
||||
}
|
||||
|
||||
Mood::pointer Mood::find(Session& session, std::string_view name)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
if (name.size() > maxNameLength)
|
||||
name = name.substr(0, maxNameLength);
|
||||
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<Mood>().where("name = ?").bind(name));
|
||||
}
|
||||
|
||||
RangeResults<MoodId> Mood::findOrphanIds(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ session.getDboSession()->query<MoodId>("SELECT m.id FROM mood m WHERE NOT EXISTS (SELECT 1 FROM track_mood t_m WHERE t_m.mood_id = m.id)") };
|
||||
return utils::execRangeQuery<MoodId>(query, range);
|
||||
}
|
||||
|
||||
} // namespace lms::db
|
||||
@@ -26,7 +26,11 @@
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackList.hpp"
|
||||
#include "database/objects/User.hpp"
|
||||
|
||||
@@ -27,8 +27,12 @@
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/User.hpp"
|
||||
|
||||
|
||||
@@ -29,8 +29,12 @@
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/ReleaseArtistLink.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
@@ -77,7 +81,11 @@ namespace lms::db
|
||||
|| params.originalDateRange
|
||||
|| params.trackArtist.isValid()
|
||||
|| params.filters.clusters.size() == 1
|
||||
|| params.filters.genre.isValid()
|
||||
|| params.filters.grouping.isValid()
|
||||
|| params.filters.language.isValid()
|
||||
|| params.filters.mediaLibrary.isValid()
|
||||
|| params.filters.mood.isValid()
|
||||
|| params.filters.codec.has_value()
|
||||
|| params.directory.isValid()
|
||||
|| params.parentDirectory.isValid())
|
||||
@@ -215,6 +223,34 @@ namespace lms::db
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
if (params.filters.genre.isValid())
|
||||
{
|
||||
query.join("track_genre t_g ON t_g.track_id = t.id")
|
||||
.where("t_g.genre_id = ?")
|
||||
.bind(params.filters.genre);
|
||||
}
|
||||
|
||||
if (params.filters.grouping.isValid())
|
||||
{
|
||||
query.join("track_grouping t_gr ON t_gr.track_id = t.id")
|
||||
.where("t_gr.grouping_id = ?")
|
||||
.bind(params.filters.grouping);
|
||||
}
|
||||
|
||||
if (params.filters.language.isValid())
|
||||
{
|
||||
query.join("track_language t_l ON t_l.track_id = t.id")
|
||||
.where("t_l.language_id = ?")
|
||||
.bind(params.filters.language);
|
||||
}
|
||||
|
||||
if (params.filters.mood.isValid())
|
||||
{
|
||||
query.join("track_mood t_m ON t_m.track_id = t.id")
|
||||
.where("t_m.mood_id = ?")
|
||||
.bind(params.filters.mood);
|
||||
}
|
||||
|
||||
if (params.filters.codec.has_value())
|
||||
query.where("t.codec = ?").bind(detail::getDbCodec(params.filters.codec.value()));
|
||||
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
#include "core/ILogger.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/User.hpp"
|
||||
|
||||
|
||||
@@ -30,8 +30,12 @@
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||
@@ -109,6 +113,34 @@ namespace lms::db
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
if (params.filters.genre.isValid())
|
||||
{
|
||||
query.join("track_genre t_g ON t_g.track_id = t.id")
|
||||
.where("t_g.genre_id = ?")
|
||||
.bind(params.filters.genre);
|
||||
}
|
||||
|
||||
if (params.filters.grouping.isValid())
|
||||
{
|
||||
query.join("track_grouping t_gr ON t_gr.track_id = t.id")
|
||||
.where("t_gr.grouping_id = ?")
|
||||
.bind(params.filters.grouping);
|
||||
}
|
||||
|
||||
if (params.filters.language.isValid())
|
||||
{
|
||||
query.join("track_language t_l ON t_l.track_id = t.id")
|
||||
.where("t_l.language_id = ?")
|
||||
.bind(params.filters.language);
|
||||
}
|
||||
|
||||
if (params.filters.mood.isValid())
|
||||
{
|
||||
query.join("track_mood t_m ON t_m.track_id = t.id")
|
||||
.where("t_m.mood_id = ?")
|
||||
.bind(params.filters.mood);
|
||||
}
|
||||
|
||||
if (params.artist.isValid() || !params.artistName.empty())
|
||||
{
|
||||
query.join("artist a ON a.id = t_a_l.artist_id")
|
||||
@@ -446,6 +478,62 @@ namespace lms::db
|
||||
return utils::fetchQueryResults(query);
|
||||
}
|
||||
|
||||
std::vector<Genre::pointer> Track::getGenres() const
|
||||
{
|
||||
return utils::fetchQueryResults<Genre::pointer>(_genres.find());
|
||||
}
|
||||
|
||||
std::vector<GenreId> Track::getGenreIds() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
const auto query{ session()->query<GenreId>("SELECT t_g.genre_id FROM track_genre t_g").where("t_g.track_id = ?").bind(getId()) };
|
||||
|
||||
return utils::fetchQueryResults(query);
|
||||
}
|
||||
|
||||
std::vector<Grouping::pointer> Track::getGroupings() const
|
||||
{
|
||||
return utils::fetchQueryResults<Grouping::pointer>(_groupings.find());
|
||||
}
|
||||
|
||||
std::vector<GroupingId> Track::getGroupingIds() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
const auto query{ session()->query<GroupingId>("SELECT t_gr.grouping_id FROM track_grouping t_gr").where("t_gr.track_id = ?").bind(getId()) };
|
||||
|
||||
return utils::fetchQueryResults(query);
|
||||
}
|
||||
|
||||
std::vector<Language::pointer> Track::getLanguages() const
|
||||
{
|
||||
return utils::fetchQueryResults<Language::pointer>(_languages.find());
|
||||
}
|
||||
|
||||
std::vector<LanguageId> Track::getLanguageIds() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
const auto query{ session()->query<LanguageId>("SELECT t_l.language_id FROM track_language t_l").where("t_l.track_id = ?").bind(getId()) };
|
||||
|
||||
return utils::fetchQueryResults(query);
|
||||
}
|
||||
|
||||
std::vector<Mood::pointer> Track::getMoods() const
|
||||
{
|
||||
return utils::fetchQueryResults<Mood::pointer>(_moods.find());
|
||||
}
|
||||
|
||||
std::vector<MoodId> Track::getMoodIds() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
const auto query{ session()->query<MoodId>("SELECT t_m.mood_id FROM track_mood t_m").where("t_m.track_id = ?").bind(getId()) };
|
||||
|
||||
return utils::fetchQueryResults(query);
|
||||
}
|
||||
|
||||
ObjectPtr<MediaLibrary> Track::getMediaLibrary() const
|
||||
{
|
||||
return _mediaLibrary;
|
||||
@@ -566,6 +654,34 @@ namespace lms::db
|
||||
_clusters.insert(getDboPtr(cluster));
|
||||
}
|
||||
|
||||
void Track::setGenres(std::span<const ObjectPtr<Genre>> genres)
|
||||
{
|
||||
_genres.clear();
|
||||
for (const ObjectPtr<Genre>& genre : genres)
|
||||
_genres.insert(getDboPtr(genre));
|
||||
}
|
||||
|
||||
void Track::setGroupings(std::span<const ObjectPtr<Grouping>> groupings)
|
||||
{
|
||||
_groupings.clear();
|
||||
for (const ObjectPtr<Grouping>& grouping : groupings)
|
||||
_groupings.insert(getDboPtr(grouping));
|
||||
}
|
||||
|
||||
void Track::setLanguages(std::span<const ObjectPtr<Language>> languages)
|
||||
{
|
||||
_languages.clear();
|
||||
for (const ObjectPtr<Language>& language : languages)
|
||||
_languages.insert(getDboPtr(language));
|
||||
}
|
||||
|
||||
void Track::setMoods(std::span<const ObjectPtr<Mood>> moods)
|
||||
{
|
||||
_moods.clear();
|
||||
for (const ObjectPtr<Mood>& mood : moods)
|
||||
_moods.insert(getDboPtr(mood));
|
||||
}
|
||||
|
||||
void Track::clearLyrics()
|
||||
{
|
||||
_trackLyrics.clear();
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
#include "core/ILogger.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
#include <Wt/Dbo/Impl.h>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/User.hpp"
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
|
||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/PlayListFile.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/User.hpp"
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
#include <Wt/Dbo/Impl.h>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/AuthToken.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/UIState.hpp"
|
||||
|
||||
@@ -104,21 +104,13 @@ namespace lms::db
|
||||
// Accessors
|
||||
std::string_view getName() const { return _name; }
|
||||
ObjectPtr<ClusterType> getType() const { return _clusterType; }
|
||||
std::size_t getTrackCount() const { return _trackCount; }
|
||||
RangeResults<TrackId> getTracks(std::optional<Range> range = std::nullopt) const;
|
||||
std::size_t getReleasesCount() const { return _releaseCount; };
|
||||
|
||||
void setReleaseCount(std::size_t releaseCount) { _releaseCount = releaseCount; }
|
||||
void setTrackCount(std::size_t trackCount) { _trackCount = trackCount; }
|
||||
void addTrack(ObjectPtr<Track> track);
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
// cached field since queries are too long
|
||||
Wt::Dbo::field(a, _trackCount, "track_count");
|
||||
Wt::Dbo::field(a, _releaseCount, "release_count");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _clusterType, "cluster_type", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
|
||||
@@ -130,8 +122,6 @@ namespace lms::db
|
||||
static pointer create(Session& session, ObjectPtr<ClusterType> type, std::string_view name);
|
||||
|
||||
std::string _name;
|
||||
int _trackCount{};
|
||||
int _releaseCount{};
|
||||
|
||||
Wt::Dbo::ptr<ClusterType> _clusterType;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks;
|
||||
|
||||
@@ -26,8 +26,12 @@
|
||||
#include "core/media/Codec.hpp"
|
||||
|
||||
#include "database/objects/ClusterId.hpp"
|
||||
#include "database/objects/GenreId.hpp"
|
||||
#include "database/objects/GroupingId.hpp"
|
||||
#include "database/objects/LabelId.hpp"
|
||||
#include "database/objects/LanguageId.hpp"
|
||||
#include "database/objects/MediaLibraryId.hpp"
|
||||
#include "database/objects/MoodId.hpp"
|
||||
#include "database/objects/ReleaseTypeId.hpp"
|
||||
|
||||
namespace lms::db
|
||||
@@ -36,7 +40,11 @@ namespace lms::db
|
||||
{
|
||||
MediaLibraryId mediaLibrary; // tracks that belongs to this library
|
||||
std::vector<ClusterId> clusters; // tracks that belong to *all* these clusters
|
||||
GenreId genre; // tracks that belong to this genre
|
||||
GroupingId grouping; // tracks that belong to this grouping
|
||||
LabelId label; // tracks which release has this label
|
||||
LanguageId language; // tracks that belong to this language
|
||||
MoodId mood; // tracks that belong to this mood
|
||||
ReleaseTypeId releaseType; // tracks which release has this type
|
||||
std::optional<core::media::Codec> codec; // tracks that match this codec
|
||||
|
||||
@@ -45,6 +53,26 @@ namespace lms::db
|
||||
clusters.assign(std::cbegin(_clusters), std::cend(_clusters));
|
||||
return *this;
|
||||
}
|
||||
Filters& setGenre(GenreId _genre)
|
||||
{
|
||||
genre = _genre;
|
||||
return *this;
|
||||
}
|
||||
Filters& setGrouping(GroupingId _grouping)
|
||||
{
|
||||
grouping = _grouping;
|
||||
return *this;
|
||||
}
|
||||
Filters& setLanguage(LanguageId _language)
|
||||
{
|
||||
language = _language;
|
||||
return *this;
|
||||
}
|
||||
Filters& setMood(MoodId _mood)
|
||||
{
|
||||
mood = _mood;
|
||||
return *this;
|
||||
}
|
||||
Filters& setMediaLibrary(MediaLibraryId _mediaLibrary)
|
||||
{
|
||||
mediaLibrary = _mediaLibrary;
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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 <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <Wt/Dbo/Field.h>
|
||||
#include <Wt/Dbo/collection.h>
|
||||
|
||||
#include "database/Object.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/ArtistId.hpp"
|
||||
#include "database/objects/GenreId.hpp"
|
||||
#include "database/objects/ReleaseId.hpp"
|
||||
#include "database/objects/TrackId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class Session;
|
||||
class Track;
|
||||
|
||||
class Genre final : public Object<Genre, GenreId>
|
||||
{
|
||||
public:
|
||||
static constexpr std::size_t maxNameLength{ 512 };
|
||||
|
||||
struct FindParameters
|
||||
{
|
||||
std::optional<Range> range;
|
||||
GenreSortMethod sortMethod{ GenreSortMethod::None };
|
||||
ArtistId artist; // if set, genres of tracks by this artist
|
||||
TrackId track; // if set, genres of this track
|
||||
ReleaseId release; // if set, genres involved in this release
|
||||
|
||||
FindParameters& setRange(std::optional<Range> _range)
|
||||
{
|
||||
range = _range;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setSortMethod(GenreSortMethod _method)
|
||||
{
|
||||
sortMethod = _method;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setTrack(TrackId _track)
|
||||
{
|
||||
track = _track;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setArtist(ArtistId _artist)
|
||||
{
|
||||
artist = _artist;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setRelease(ReleaseId _release)
|
||||
{
|
||||
release = _release;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
Genre() = default;
|
||||
|
||||
static std::size_t getCount(Session& session);
|
||||
static RangeResults<GenreId> findIds(Session& session, const FindParameters& params);
|
||||
static RangeResults<pointer> find(Session& session, const FindParameters& params);
|
||||
static void find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func);
|
||||
static pointer find(Session& session, GenreId id);
|
||||
static pointer find(Session& session, std::string_view name);
|
||||
static RangeResults<GenreId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||
|
||||
static std::size_t computeTrackCount(Session& session, GenreId id);
|
||||
static std::size_t computeReleaseCount(Session& session, GenreId id);
|
||||
|
||||
std::string_view getName() const { return _name; }
|
||||
std::size_t getTrackCount() const { return _trackCount; }
|
||||
std::size_t getReleaseCount() const { return _releaseCount; }
|
||||
|
||||
void setTrackCount(std::size_t count) { _trackCount = count; }
|
||||
void setReleaseCount(std::size_t count) { _releaseCount = count; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _trackCount, "track_count");
|
||||
Wt::Dbo::field(a, _releaseCount, "release_count");
|
||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_genre", "", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
Genre(std::string_view name);
|
||||
static pointer create(Session& session, std::string_view name);
|
||||
|
||||
std::string _name;
|
||||
int _trackCount{};
|
||||
int _releaseCount{};
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks;
|
||||
};
|
||||
|
||||
} // namespace lms::db
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 "database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(GenreId)
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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 <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <Wt/Dbo/Field.h>
|
||||
#include <Wt/Dbo/collection.h>
|
||||
|
||||
#include "database/Object.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/ArtistId.hpp"
|
||||
#include "database/objects/GroupingId.hpp"
|
||||
#include "database/objects/ReleaseId.hpp"
|
||||
#include "database/objects/TrackId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class Session;
|
||||
class Track;
|
||||
|
||||
class Grouping final : public Object<Grouping, GroupingId>
|
||||
{
|
||||
public:
|
||||
static constexpr std::size_t maxNameLength{ 512 };
|
||||
|
||||
struct FindParameters
|
||||
{
|
||||
std::optional<Range> range;
|
||||
GroupingSortMethod sortMethod{ GroupingSortMethod::None };
|
||||
ArtistId artist; // if set, groupings of tracks by this artist
|
||||
TrackId track; // if set, groupings of this track
|
||||
ReleaseId release; // if set, groupings involved in this release
|
||||
|
||||
FindParameters& setRange(std::optional<Range> _range)
|
||||
{
|
||||
range = _range;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setSortMethod(GroupingSortMethod _method)
|
||||
{
|
||||
sortMethod = _method;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setTrack(TrackId _track)
|
||||
{
|
||||
track = _track;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setArtist(ArtistId _artist)
|
||||
{
|
||||
artist = _artist;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setRelease(ReleaseId _release)
|
||||
{
|
||||
release = _release;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
Grouping() = default;
|
||||
|
||||
static std::size_t getCount(Session& session);
|
||||
static RangeResults<GroupingId> findIds(Session& session, const FindParameters& params);
|
||||
static RangeResults<pointer> find(Session& session, const FindParameters& params);
|
||||
static void find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func);
|
||||
static pointer find(Session& session, GroupingId id);
|
||||
static pointer find(Session& session, std::string_view name);
|
||||
static RangeResults<GroupingId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||
|
||||
std::string_view getName() const { return _name; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_grouping", "", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
Grouping(std::string_view name);
|
||||
static pointer create(Session& session, std::string_view name);
|
||||
|
||||
std::string _name;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks;
|
||||
};
|
||||
|
||||
} // namespace lms::db
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 "database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(GroupingId)
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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 <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <Wt/Dbo/Field.h>
|
||||
#include <Wt/Dbo/collection.h>
|
||||
|
||||
#include "database/Object.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/ArtistId.hpp"
|
||||
#include "database/objects/LanguageId.hpp"
|
||||
#include "database/objects/ReleaseId.hpp"
|
||||
#include "database/objects/TrackId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class Session;
|
||||
class Track;
|
||||
|
||||
class Language final : public Object<Language, LanguageId>
|
||||
{
|
||||
public:
|
||||
static constexpr std::size_t maxNameLength{ 512 };
|
||||
|
||||
struct FindParameters
|
||||
{
|
||||
std::optional<Range> range;
|
||||
LanguageSortMethod sortMethod{ LanguageSortMethod::None };
|
||||
ArtistId artist; // if set, languages of tracks by this artist
|
||||
TrackId track; // if set, languages of this track
|
||||
ReleaseId release; // if set, languages involved in this release
|
||||
|
||||
FindParameters& setRange(std::optional<Range> _range)
|
||||
{
|
||||
range = _range;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setSortMethod(LanguageSortMethod _method)
|
||||
{
|
||||
sortMethod = _method;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setTrack(TrackId _track)
|
||||
{
|
||||
track = _track;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setArtist(ArtistId _artist)
|
||||
{
|
||||
artist = _artist;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setRelease(ReleaseId _release)
|
||||
{
|
||||
release = _release;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
Language() = default;
|
||||
|
||||
static std::size_t getCount(Session& session);
|
||||
static RangeResults<LanguageId> findIds(Session& session, const FindParameters& params);
|
||||
static RangeResults<pointer> find(Session& session, const FindParameters& params);
|
||||
static void find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func);
|
||||
static pointer find(Session& session, LanguageId id);
|
||||
static pointer find(Session& session, std::string_view name);
|
||||
static RangeResults<LanguageId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||
|
||||
std::string_view getName() const { return _name; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_language", "", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
Language(std::string_view name);
|
||||
static pointer create(Session& session, std::string_view name);
|
||||
|
||||
std::string _name;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks;
|
||||
};
|
||||
|
||||
} // namespace lms::db
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 "database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(LanguageId)
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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 <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <Wt/Dbo/Field.h>
|
||||
#include <Wt/Dbo/collection.h>
|
||||
|
||||
#include "database/Object.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/ArtistId.hpp"
|
||||
#include "database/objects/MoodId.hpp"
|
||||
#include "database/objects/ReleaseId.hpp"
|
||||
#include "database/objects/TrackId.hpp"
|
||||
#include "database/objects/Types.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class Session;
|
||||
class Track;
|
||||
|
||||
class Mood final : public Object<Mood, MoodId>
|
||||
{
|
||||
public:
|
||||
static constexpr std::size_t maxNameLength{ 512 };
|
||||
|
||||
struct FindParameters
|
||||
{
|
||||
std::optional<Range> range;
|
||||
MoodSortMethod sortMethod{ MoodSortMethod::None };
|
||||
ArtistId artist; // if set, moods of tracks by this artist
|
||||
TrackId track; // if set, moods of this track
|
||||
ReleaseId release; // if set, moods involved in this release
|
||||
|
||||
FindParameters& setRange(std::optional<Range> _range)
|
||||
{
|
||||
range = _range;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setSortMethod(MoodSortMethod _method)
|
||||
{
|
||||
sortMethod = _method;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setTrack(TrackId _track)
|
||||
{
|
||||
track = _track;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setArtist(ArtistId _artist)
|
||||
{
|
||||
artist = _artist;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setRelease(ReleaseId _release)
|
||||
{
|
||||
release = _release;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
Mood() = default;
|
||||
|
||||
static std::size_t getCount(Session& session);
|
||||
static RangeResults<MoodId> findIds(Session& session, const FindParameters& params);
|
||||
static RangeResults<pointer> find(Session& session, const FindParameters& params);
|
||||
static void find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func);
|
||||
static pointer find(Session& session, MoodId id);
|
||||
static pointer find(Session& session, std::string_view name);
|
||||
static RangeResults<MoodId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||
|
||||
std::string_view getName() const { return _name; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_mood", "", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
Mood(std::string_view name);
|
||||
static pointer create(Session& session, std::string_view name);
|
||||
|
||||
std::string _name;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks;
|
||||
};
|
||||
|
||||
} // namespace lms::db
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 "database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(MoodId)
|
||||
@@ -45,8 +45,12 @@
|
||||
#include "database/objects/ClusterId.hpp"
|
||||
#include "database/objects/DirectoryId.hpp"
|
||||
#include "database/objects/Filters.hpp"
|
||||
#include "database/objects/GenreId.hpp"
|
||||
#include "database/objects/GroupingId.hpp"
|
||||
#include "database/objects/LanguageId.hpp"
|
||||
#include "database/objects/MediaLibraryId.hpp"
|
||||
#include "database/objects/MediumId.hpp"
|
||||
#include "database/objects/MoodId.hpp"
|
||||
#include "database/objects/ReleaseId.hpp"
|
||||
#include "database/objects/TrackEmbeddedImageId.hpp"
|
||||
#include "database/objects/TrackId.hpp"
|
||||
@@ -62,6 +66,10 @@ namespace lms::db
|
||||
class Cluster;
|
||||
class ClusterType;
|
||||
class Directory;
|
||||
class Genre;
|
||||
class Grouping;
|
||||
class Language;
|
||||
class Mood;
|
||||
class TrackEmbeddedImageLink;
|
||||
class MediaLibrary;
|
||||
class Medium;
|
||||
@@ -270,6 +278,10 @@ namespace lms::db
|
||||
void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); }
|
||||
void setMedium(ObjectPtr<Medium> medium) { _medium = getDboPtr(medium); }
|
||||
void setClusters(const std::vector<ObjectPtr<Cluster>>& clusters);
|
||||
void setGenres(std::span<const ObjectPtr<Genre>> genres);
|
||||
void setGroupings(std::span<const ObjectPtr<Grouping>> groupings);
|
||||
void setLanguages(std::span<const ObjectPtr<Language>> languages);
|
||||
void setMoods(std::span<const ObjectPtr<Mood>> moods);
|
||||
void clearLyrics();
|
||||
void clearEmbeddedLyrics();
|
||||
void addLyrics(const ObjectPtr<TrackLyrics>& lyrics);
|
||||
@@ -330,6 +342,14 @@ namespace lms::db
|
||||
ObjectPtr<Medium> getMedium() const { return _medium; }
|
||||
std::vector<ObjectPtr<Cluster>> getClusters() const;
|
||||
std::vector<ClusterId> getClusterIds() const;
|
||||
std::vector<ObjectPtr<Genre>> getGenres() const;
|
||||
std::vector<GenreId> getGenreIds() const;
|
||||
std::vector<ObjectPtr<Grouping>> getGroupings() const;
|
||||
std::vector<GroupingId> getGroupingIds() const;
|
||||
std::vector<ObjectPtr<Language>> getLanguages() const;
|
||||
std::vector<LanguageId> getLanguageIds() const;
|
||||
std::vector<ObjectPtr<Mood>> getMoods() const;
|
||||
std::vector<MoodId> getMoodIds() const;
|
||||
ObjectPtr<MediaLibrary> getMediaLibrary() const;
|
||||
ObjectPtr<Directory> getDirectory() const;
|
||||
ObjectPtr<Artwork> getPreferredArtwork() const;
|
||||
@@ -378,6 +398,10 @@ namespace lms::db
|
||||
Wt::Dbo::belongsTo(a, _preferredMediaArtwork, "preferred_media_artwork", Wt::Dbo::OnDeleteSetNull);
|
||||
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, _genres, Wt::Dbo::ManyToMany, "track_genre", "", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _groupings, Wt::Dbo::ManyToMany, "track_grouping", "", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _languages, Wt::Dbo::ManyToMany, "track_language", "", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _moods, Wt::Dbo::ManyToMany, "track_mood", "", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _trackLyrics, Wt::Dbo::ManyToOne, "track");
|
||||
Wt::Dbo::hasMany(a, _embeddedImageLinks, Wt::Dbo::ManyToOne, "track");
|
||||
}
|
||||
@@ -429,6 +453,10 @@ namespace lms::db
|
||||
Wt::Dbo::ptr<Artwork> _preferredMediaArtwork;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Genre>> _genres;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Grouping>> _groupings;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Language>> _languages;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Mood>> _moods;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackLyrics>> _trackLyrics;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackEmbeddedImageLink>> _embeddedImageLinks;
|
||||
};
|
||||
|
||||
@@ -65,6 +65,20 @@ namespace lms::db
|
||||
Name,
|
||||
};
|
||||
|
||||
enum class GenreSortMethod
|
||||
{
|
||||
None,
|
||||
Name,
|
||||
TrackCountDesc,
|
||||
};
|
||||
|
||||
enum class GroupingSortMethod
|
||||
{
|
||||
None,
|
||||
Name,
|
||||
TrackCountDesc,
|
||||
};
|
||||
|
||||
using ImageHashType = core::TaggedType<class ImageHash, std::uint64_t>;
|
||||
|
||||
enum class LabelSortMethod
|
||||
@@ -73,6 +87,20 @@ namespace lms::db
|
||||
Name,
|
||||
};
|
||||
|
||||
enum class LanguageSortMethod
|
||||
{
|
||||
None,
|
||||
Name,
|
||||
TrackCountDesc,
|
||||
};
|
||||
|
||||
enum class MoodSortMethod
|
||||
{
|
||||
None,
|
||||
Name,
|
||||
TrackCountDesc,
|
||||
};
|
||||
|
||||
enum class MediumSortMethod
|
||||
{
|
||||
None,
|
||||
|
||||
@@ -8,10 +8,14 @@ add_executable(test-database
|
||||
Common.cpp
|
||||
DatabaseTest.cpp
|
||||
Directory.cpp
|
||||
Genre.cpp
|
||||
Grouping.cpp
|
||||
Image.cpp
|
||||
Language.cpp
|
||||
Listen.cpp
|
||||
Medium.cpp
|
||||
Migration.cpp
|
||||
Mood.cpp
|
||||
PlayListFile.cpp
|
||||
Podcast.cpp
|
||||
RatedArtist.cpp
|
||||
|
||||
@@ -59,8 +59,6 @@ namespace lms::db::tests
|
||||
|
||||
void DatabaseFixture::testDatabaseEmpty()
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_TRUE(session.areAllTablesEmpty());
|
||||
|
||||
@@ -29,8 +29,12 @@
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/Listen.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/ScanSettings.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* 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 "Common.hpp"
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
using ScopedGenre = ScopedEntity<db::Genre>;
|
||||
|
||||
TEST_F(DatabaseFixture, Genre_create)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Genre::getCount(session), 0);
|
||||
}
|
||||
|
||||
ScopedGenre genre{ session, "Rock" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Genre::getCount(session), 1);
|
||||
|
||||
const Genre::pointer found{ Genre::find(session, genre.getId()) };
|
||||
ASSERT_TRUE(found);
|
||||
EXPECT_EQ(found->getName(), "Rock");
|
||||
EXPECT_EQ(found->getTrackCount(), 0);
|
||||
EXPECT_EQ(found->getReleaseCount(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Genre_findByName)
|
||||
{
|
||||
ScopedGenre genre{ session, "Jazz" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_TRUE(Genre::find(session, "Jazz"));
|
||||
EXPECT_FALSE(Genre::find(session, "jazz"));
|
||||
EXPECT_FALSE(Genre::find(session, ""));
|
||||
EXPECT_FALSE(Genre::find(session, "Jaz"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Genre_orphan)
|
||||
{
|
||||
ScopedGenre genre{ session, "Blues" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto orphans{ Genre::findOrphanIds(session) };
|
||||
ASSERT_EQ(orphans.results.size(), 1);
|
||||
EXPECT_EQ(orphans.results.front(), genre.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Genre_singleTrack)
|
||||
{
|
||||
ScopedTrack track{ session };
|
||||
ScopedGenre genre1{ session, "Metal" };
|
||||
ScopedGenre genre2{ session, "Punk" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Genre::findOrphanIds(session).results.size(), 2);
|
||||
EXPECT_EQ(track->getGenres().size(), 0);
|
||||
EXPECT_EQ(track->getGenreIds().size(), 0);
|
||||
EXPECT_EQ(Genre::computeTrackCount(session, genre1.getId()), 0);
|
||||
EXPECT_EQ(Genre::computeTrackCount(session, genre2.getId()), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setGenres(std::array{ genre1.get() });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto genres{ Genre::findIds(session, Genre::FindParameters{}.setTrack(track.getId())) };
|
||||
ASSERT_EQ(genres.results.size(), 1);
|
||||
EXPECT_EQ(genres.results.front(), genre1.getId());
|
||||
|
||||
EXPECT_EQ(Genre::computeTrackCount(session, genre1.getId()), 1);
|
||||
EXPECT_EQ(Genre::computeTrackCount(session, genre2.getId()), 0);
|
||||
|
||||
const auto orphans{ Genre::findOrphanIds(session) };
|
||||
ASSERT_EQ(orphans.results.size(), 1);
|
||||
EXPECT_EQ(orphans.results.front(), genre2.getId());
|
||||
|
||||
const auto trackGenres{ track->getGenres() };
|
||||
ASSERT_EQ(trackGenres.size(), 1);
|
||||
EXPECT_EQ(trackGenres.front()->getId(), genre1.getId());
|
||||
|
||||
const auto trackGenreIds{ track->getGenreIds() };
|
||||
ASSERT_EQ(trackGenreIds.size(), 1);
|
||||
EXPECT_EQ(trackGenreIds.front(), genre1.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre1.getId()))) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
|
||||
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre2.getId()))) };
|
||||
EXPECT_EQ(tracks2.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Genre_multipleGenresOnTrack)
|
||||
{
|
||||
ScopedTrack track{ session };
|
||||
ScopedGenre genre1{ session, "Electronic" };
|
||||
ScopedGenre genre2{ session, "Ambient" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setGenres(std::array{ genre1.get(), genre2.get() });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Genre::computeTrackCount(session, genre1.getId()), 1);
|
||||
EXPECT_EQ(Genre::computeTrackCount(session, genre2.getId()), 1);
|
||||
EXPECT_EQ(Genre::findOrphanIds(session).results.size(), 0);
|
||||
|
||||
const auto trackGenres{ track->getGenres() };
|
||||
EXPECT_EQ(trackGenres.size(), 2);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre1.getId()))) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
|
||||
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre2.getId()))) };
|
||||
ASSERT_EQ(tracks2.results.size(), 1);
|
||||
EXPECT_EQ(tracks2.results.front(), track.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Genre_computeReleaseCount)
|
||||
{
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedGenre genre{ session, "Classical" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
track.get().modify()->setGenres(std::array{ genre.get() });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Genre::computeTrackCount(session, genre.getId()), 1);
|
||||
EXPECT_EQ(Genre::computeReleaseCount(session, genre.getId()), 1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Genre_nameTruncation)
|
||||
{
|
||||
const std::string longName(Genre::maxNameLength + 100, 'x');
|
||||
const std::string expectedName(Genre::maxNameLength, 'x');
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
Genre::pointer genre{ session.create<Genre>(longName) };
|
||||
ASSERT_TRUE(genre);
|
||||
EXPECT_EQ(genre->getName().size(), Genre::maxNameLength);
|
||||
EXPECT_EQ(genre->getName(), expectedName);
|
||||
genre.remove();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Genre_sortByName)
|
||||
{
|
||||
ScopedGenre g1{ session, "Zzz" };
|
||||
ScopedGenre g2{ session, "Aaa" };
|
||||
ScopedGenre g3{ session, "Mmm" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto genres{ Genre::findIds(session, Genre::FindParameters{}.setSortMethod(GenreSortMethod::Name)) };
|
||||
ASSERT_EQ(genres.results.size(), 3);
|
||||
EXPECT_EQ(genres.results[0], g2.getId());
|
||||
EXPECT_EQ(genres.results[1], g3.getId());
|
||||
EXPECT_EQ(genres.results[2], g1.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Genre_sortByTrackCount)
|
||||
{
|
||||
ScopedGenre g1{ session, "Rock" };
|
||||
ScopedGenre g2{ session, "Jazz" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedTrack track3{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track1.get().modify()->setGenres(std::array{ g1.get() });
|
||||
track2.get().modify()->setGenres(std::array{ g1.get() });
|
||||
track3.get().modify()->setGenres(std::array{ g2.get() });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto genres{ Genre::findIds(session, Genre::FindParameters{}.setSortMethod(GenreSortMethod::TrackCountDesc)) };
|
||||
ASSERT_EQ(genres.results.size(), 2);
|
||||
EXPECT_EQ(genres.results[0], g1.getId());
|
||||
EXPECT_EQ(genres.results[1], g2.getId());
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* 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 "Common.hpp"
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
using ScopedGrouping = ScopedEntity<db::Grouping>;
|
||||
|
||||
TEST_F(DatabaseFixture, Grouping_create)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Grouping::getCount(session), 0);
|
||||
}
|
||||
|
||||
ScopedGrouping grouping{ session, "Soundtrack" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Grouping::getCount(session), 1);
|
||||
|
||||
const Grouping::pointer found{ Grouping::find(session, grouping.getId()) };
|
||||
ASSERT_TRUE(found);
|
||||
EXPECT_EQ(found->getName(), "Soundtrack");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Grouping_findByName)
|
||||
{
|
||||
ScopedGrouping grouping{ session, "Live" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_TRUE(Grouping::find(session, "Live"));
|
||||
EXPECT_FALSE(Grouping::find(session, "live"));
|
||||
EXPECT_FALSE(Grouping::find(session, ""));
|
||||
EXPECT_FALSE(Grouping::find(session, "Liv"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Grouping_orphan)
|
||||
{
|
||||
ScopedGrouping grouping{ session, "Podcast" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto orphans{ Grouping::findOrphanIds(session) };
|
||||
ASSERT_EQ(orphans.results.size(), 1);
|
||||
EXPECT_EQ(orphans.results.front(), grouping.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Grouping_singleTrack)
|
||||
{
|
||||
ScopedTrack track{ session };
|
||||
ScopedGrouping grouping1{ session, "Audiobook" };
|
||||
ScopedGrouping grouping2{ session, "Remix" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Grouping::findOrphanIds(session).results.size(), 2);
|
||||
EXPECT_EQ(track->getGroupings().size(), 0);
|
||||
EXPECT_EQ(track->getGroupingIds().size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setGroupings(std::array{ grouping1.get() });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto groupings{ Grouping::findIds(session, Grouping::FindParameters{}.setTrack(track.getId())) };
|
||||
ASSERT_EQ(groupings.results.size(), 1);
|
||||
EXPECT_EQ(groupings.results.front(), grouping1.getId());
|
||||
|
||||
const auto orphans{ Grouping::findOrphanIds(session) };
|
||||
ASSERT_EQ(orphans.results.size(), 1);
|
||||
EXPECT_EQ(orphans.results.front(), grouping2.getId());
|
||||
|
||||
const auto trackGroupings{ track->getGroupings() };
|
||||
ASSERT_EQ(trackGroupings.size(), 1);
|
||||
EXPECT_EQ(trackGroupings.front()->getId(), grouping1.getId());
|
||||
|
||||
const auto trackGroupingIds{ track->getGroupingIds() };
|
||||
ASSERT_EQ(trackGroupingIds.size(), 1);
|
||||
EXPECT_EQ(trackGroupingIds.front(), grouping1.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGrouping(grouping1.getId()))) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
|
||||
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGrouping(grouping2.getId()))) };
|
||||
EXPECT_EQ(tracks2.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Grouping_multipleGroupingsOnTrack)
|
||||
{
|
||||
ScopedTrack track{ session };
|
||||
ScopedGrouping grouping1{ session, "Soundtrack" };
|
||||
ScopedGrouping grouping2{ session, "Live" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setGroupings(std::array{ grouping1.get(), grouping2.get() });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Grouping::findOrphanIds(session).results.size(), 0);
|
||||
|
||||
const auto trackGroupings{ track->getGroupings() };
|
||||
EXPECT_EQ(trackGroupings.size(), 2);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGrouping(grouping1.getId()))) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
|
||||
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGrouping(grouping2.getId()))) };
|
||||
ASSERT_EQ(tracks2.results.size(), 1);
|
||||
EXPECT_EQ(tracks2.results.front(), track.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Grouping_sortByName)
|
||||
{
|
||||
ScopedGrouping g1{ session, "Zzz" };
|
||||
ScopedGrouping g2{ session, "Aaa" };
|
||||
ScopedGrouping g3{ session, "Mmm" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto groupings{ Grouping::findIds(session, Grouping::FindParameters{}.setSortMethod(GroupingSortMethod::Name)) };
|
||||
ASSERT_EQ(groupings.results.size(), 3);
|
||||
EXPECT_EQ(groupings.results[0], g2.getId());
|
||||
EXPECT_EQ(groupings.results[1], g3.getId());
|
||||
EXPECT_EQ(groupings.results[2], g1.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Grouping_sortByTrackCount)
|
||||
{
|
||||
ScopedGrouping g1{ session, "Compilation" };
|
||||
ScopedGrouping g2{ session, "Live" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedTrack track3{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track1.get().modify()->setGroupings(std::array{ g1.get() });
|
||||
track2.get().modify()->setGroupings(std::array{ g1.get() });
|
||||
track3.get().modify()->setGroupings(std::array{ g2.get() });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto groupings{ Grouping::findIds(session, Grouping::FindParameters{}.setSortMethod(GroupingSortMethod::TrackCountDesc)) };
|
||||
ASSERT_EQ(groupings.results.size(), 2);
|
||||
EXPECT_EQ(groupings.results[0], g1.getId());
|
||||
EXPECT_EQ(groupings.results[1], g2.getId());
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* 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 "Common.hpp"
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
using ScopedLanguage = ScopedEntity<db::Language>;
|
||||
|
||||
TEST_F(DatabaseFixture, Language_create)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Language::getCount(session), 0);
|
||||
}
|
||||
|
||||
ScopedLanguage language{ session, "eng" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Language::getCount(session), 1);
|
||||
|
||||
const Language::pointer found{ Language::find(session, language.getId()) };
|
||||
ASSERT_TRUE(found);
|
||||
EXPECT_EQ(found->getName(), "eng");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Language_findByName)
|
||||
{
|
||||
ScopedLanguage language{ session, "fra" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_TRUE(Language::find(session, "fra"));
|
||||
EXPECT_FALSE(Language::find(session, "FRA"));
|
||||
EXPECT_FALSE(Language::find(session, ""));
|
||||
EXPECT_FALSE(Language::find(session, "fr"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Language_orphan)
|
||||
{
|
||||
ScopedLanguage language{ session, "jpn" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto orphans{ Language::findOrphanIds(session) };
|
||||
ASSERT_EQ(orphans.results.size(), 1);
|
||||
EXPECT_EQ(orphans.results.front(), language.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Language_singleTrack)
|
||||
{
|
||||
ScopedTrack track{ session };
|
||||
ScopedLanguage language1{ session, "eng" };
|
||||
ScopedLanguage language2{ session, "spa" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Language::findOrphanIds(session).results.size(), 2);
|
||||
EXPECT_EQ(track->getLanguages().size(), 0);
|
||||
EXPECT_EQ(track->getLanguageIds().size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setLanguages(std::array{ language1.get() });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto languages{ Language::findIds(session, Language::FindParameters{}.setTrack(track.getId())) };
|
||||
ASSERT_EQ(languages.results.size(), 1);
|
||||
EXPECT_EQ(languages.results.front(), language1.getId());
|
||||
|
||||
const auto orphans{ Language::findOrphanIds(session) };
|
||||
ASSERT_EQ(orphans.results.size(), 1);
|
||||
EXPECT_EQ(orphans.results.front(), language2.getId());
|
||||
|
||||
const auto trackLanguages{ track->getLanguages() };
|
||||
ASSERT_EQ(trackLanguages.size(), 1);
|
||||
EXPECT_EQ(trackLanguages.front()->getId(), language1.getId());
|
||||
|
||||
const auto trackLanguageIds{ track->getLanguageIds() };
|
||||
ASSERT_EQ(trackLanguageIds.size(), 1);
|
||||
EXPECT_EQ(trackLanguageIds.front(), language1.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setLanguage(language1.getId()))) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
|
||||
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setLanguage(language2.getId()))) };
|
||||
EXPECT_EQ(tracks2.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Language_multipleLanguagesOnTrack)
|
||||
{
|
||||
ScopedTrack track{ session };
|
||||
ScopedLanguage language1{ session, "eng" };
|
||||
ScopedLanguage language2{ session, "fra" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setLanguages(std::array{ language1.get(), language2.get() });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Language::findOrphanIds(session).results.size(), 0);
|
||||
|
||||
const auto trackLanguages{ track->getLanguages() };
|
||||
EXPECT_EQ(trackLanguages.size(), 2);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setLanguage(language1.getId()))) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
|
||||
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setLanguage(language2.getId()))) };
|
||||
ASSERT_EQ(tracks2.results.size(), 1);
|
||||
EXPECT_EQ(tracks2.results.front(), track.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Language_sortByName)
|
||||
{
|
||||
ScopedLanguage l1{ session, "zho" };
|
||||
ScopedLanguage l2{ session, "ara" };
|
||||
ScopedLanguage l3{ session, "por" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto languages{ Language::findIds(session, Language::FindParameters{}.setSortMethod(LanguageSortMethod::Name)) };
|
||||
ASSERT_EQ(languages.results.size(), 3);
|
||||
EXPECT_EQ(languages.results[0], l2.getId());
|
||||
EXPECT_EQ(languages.results[1], l3.getId());
|
||||
EXPECT_EQ(languages.results[2], l1.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Language_sortByTrackCount)
|
||||
{
|
||||
ScopedLanguage l1{ session, "eng" };
|
||||
ScopedLanguage l2{ session, "fra" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedTrack track3{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track1.get().modify()->setLanguages(std::array{ l1.get() });
|
||||
track2.get().modify()->setLanguages(std::array{ l1.get() });
|
||||
track3.get().modify()->setLanguages(std::array{ l2.get() });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto languages{ Language::findIds(session, Language::FindParameters{}.setSortMethod(LanguageSortMethod::TrackCountDesc)) };
|
||||
ASSERT_EQ(languages.results.size(), 2);
|
||||
EXPECT_EQ(languages.results[0], l1.getId());
|
||||
EXPECT_EQ(languages.results[1], l2.getId());
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* 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 "Common.hpp"
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
using ScopedMood = ScopedEntity<db::Mood>;
|
||||
|
||||
TEST_F(DatabaseFixture, Mood_create)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Mood::getCount(session), 0);
|
||||
}
|
||||
|
||||
ScopedMood mood{ session, "Happy" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Mood::getCount(session), 1);
|
||||
|
||||
const Mood::pointer found{ Mood::find(session, mood.getId()) };
|
||||
ASSERT_TRUE(found);
|
||||
EXPECT_EQ(found->getName(), "Happy");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Mood_findByName)
|
||||
{
|
||||
ScopedMood mood{ session, "Chill" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_TRUE(Mood::find(session, "Chill"));
|
||||
EXPECT_FALSE(Mood::find(session, "chill"));
|
||||
EXPECT_FALSE(Mood::find(session, ""));
|
||||
EXPECT_FALSE(Mood::find(session, "Chil"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Mood_orphan)
|
||||
{
|
||||
ScopedMood mood{ session, "Sad" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto orphans{ Mood::findOrphanIds(session) };
|
||||
ASSERT_EQ(orphans.results.size(), 1);
|
||||
EXPECT_EQ(orphans.results.front(), mood.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Mood_singleTrack)
|
||||
{
|
||||
ScopedTrack track{ session };
|
||||
ScopedMood mood1{ session, "Dark" };
|
||||
ScopedMood mood2{ session, "Upbeat" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Mood::findOrphanIds(session).results.size(), 2);
|
||||
EXPECT_EQ(track->getMoods().size(), 0);
|
||||
EXPECT_EQ(track->getMoodIds().size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setMoods(std::array{ mood1.get() });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto moods{ Mood::findIds(session, Mood::FindParameters{}.setTrack(track.getId())) };
|
||||
ASSERT_EQ(moods.results.size(), 1);
|
||||
EXPECT_EQ(moods.results.front(), mood1.getId());
|
||||
|
||||
const auto orphans{ Mood::findOrphanIds(session) };
|
||||
ASSERT_EQ(orphans.results.size(), 1);
|
||||
EXPECT_EQ(orphans.results.front(), mood2.getId());
|
||||
|
||||
const auto trackMoods{ track->getMoods() };
|
||||
ASSERT_EQ(trackMoods.size(), 1);
|
||||
EXPECT_EQ(trackMoods.front()->getId(), mood1.getId());
|
||||
|
||||
const auto trackMoodIds{ track->getMoodIds() };
|
||||
ASSERT_EQ(trackMoodIds.size(), 1);
|
||||
EXPECT_EQ(trackMoodIds.front(), mood1.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setMood(mood1.getId()))) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
|
||||
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setMood(mood2.getId()))) };
|
||||
EXPECT_EQ(tracks2.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Mood_multipleMoodsOnTrack)
|
||||
{
|
||||
ScopedTrack track{ session };
|
||||
ScopedMood mood1{ session, "Melancholic" };
|
||||
ScopedMood mood2{ session, "Romantic" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setMoods(std::array{ mood1.get(), mood2.get() });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Mood::findOrphanIds(session).results.size(), 0);
|
||||
|
||||
const auto trackMoods{ track->getMoods() };
|
||||
EXPECT_EQ(trackMoods.size(), 2);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setMood(mood1.getId()))) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
|
||||
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setMood(mood2.getId()))) };
|
||||
ASSERT_EQ(tracks2.results.size(), 1);
|
||||
EXPECT_EQ(tracks2.results.front(), track.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Mood_sortByName)
|
||||
{
|
||||
ScopedMood m1{ session, "Zzz" };
|
||||
ScopedMood m2{ session, "Aaa" };
|
||||
ScopedMood m3{ session, "Mmm" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto moods{ Mood::findIds(session, Mood::FindParameters{}.setSortMethod(MoodSortMethod::Name)) };
|
||||
ASSERT_EQ(moods.results.size(), 3);
|
||||
EXPECT_EQ(moods.results[0], m2.getId());
|
||||
EXPECT_EQ(moods.results[1], m3.getId());
|
||||
EXPECT_EQ(moods.results[2], m1.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Mood_sortByTrackCount)
|
||||
{
|
||||
ScopedMood m1{ session, "Energetic" };
|
||||
ScopedMood m2{ session, "Chill" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedTrack track3{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track1.get().modify()->setMoods(std::array{ m1.get() });
|
||||
track2.get().modify()->setMoods(std::array{ m1.get() });
|
||||
track3.get().modify()->setMoods(std::array{ m2.get() });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto moods{ Mood::findIds(session, Mood::FindParameters{}.setSortMethod(MoodSortMethod::TrackCountDesc)) };
|
||||
ASSERT_EQ(moods.results.size(), 2);
|
||||
EXPECT_EQ(moods.results[0], m1.getId());
|
||||
EXPECT_EQ(moods.results[1], m2.getId());
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
@@ -27,7 +27,11 @@
|
||||
namespace lms::db::tests
|
||||
{
|
||||
using ScopedArtwork = ScopedEntity<db::Artwork>;
|
||||
using ScopedGenre = ScopedEntity<db::Genre>;
|
||||
using ScopedGrouping = ScopedEntity<db::Grouping>;
|
||||
using ScopedImage = ScopedEntity<db::Image>;
|
||||
using ScopedLanguage = ScopedEntity<db::Language>;
|
||||
using ScopedMood = ScopedEntity<db::Mood>;
|
||||
|
||||
TEST_F(DatabaseFixture, Track)
|
||||
{
|
||||
@@ -590,4 +594,33 @@ namespace lms::db::tests
|
||||
EXPECT_EQ(track->getPreferredMediaArtwork(), Artwork::pointer{});
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Track_combinedTagFilters)
|
||||
{
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedGenre genre{ session, "Rock" };
|
||||
ScopedMood mood{ session, "Energetic" };
|
||||
ScopedGrouping grouping{ session, "Compilation" };
|
||||
ScopedLanguage language{ session, "eng" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track1.get().modify()->setGenres(std::array{ genre.get() });
|
||||
track1.get().modify()->setMoods(std::array{ mood.get() });
|
||||
track1.get().modify()->setGroupings(std::array{ grouping.get() });
|
||||
track1.get().modify()->setLanguages(std::array{ language.get() });
|
||||
track2.get().modify()->setGenres(std::array{ genre.get() });
|
||||
track2.get().modify()->setMoods(std::array{ mood.get() });
|
||||
track2.get().modify()->setGroupings(std::array{ grouping.get() });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto results{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre.getId()).setMood(mood.getId()).setGrouping(grouping.getId()).setLanguage(language.getId()))) };
|
||||
ASSERT_EQ(results.results.size(), 1);
|
||||
EXPECT_EQ(results.results.front(), track1.getId());
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
add_library(lmsrecommendation STATIC
|
||||
impl/clusters/ClustersEngine.cpp
|
||||
impl/tags/TagsEngine.cpp
|
||||
impl/audio-similarity/musicnn/MusicNNEmbeddingEngine.cpp
|
||||
impl/audio-similarity/musicnn/MusicNNEmbeddingProvider.cpp
|
||||
impl/track-selection-constraints/SameArtistConstraint.cpp
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
#include "database/Session.hpp"
|
||||
|
||||
#include "audio-similarity/musicnn/MusicNNEmbeddingEngine.hpp"
|
||||
#include "clusters/ClustersEngine.hpp"
|
||||
#include "tags/TagsEngine.hpp"
|
||||
|
||||
namespace lms::recommendation
|
||||
{
|
||||
@@ -57,7 +57,7 @@ namespace lms::recommendation
|
||||
switch (type)
|
||||
{
|
||||
case db::ScanSettings::RecommendationEngineType::Clusters:
|
||||
return std::make_unique<ClusterEngine>(db);
|
||||
return std::make_unique<TagsEngine>(db);
|
||||
case db::ScanSettings::RecommendationEngineType::AudioSimilarity:
|
||||
return std::make_unique<MusicNNEmbeddingEngine>(db);
|
||||
case db::ScanSettings::RecommendationEngineType::None:
|
||||
|
||||
+130
-76
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ClustersEngine.hpp"
|
||||
#include "TagsEngine.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
@@ -28,7 +28,10 @@
|
||||
#include "database/IDb.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackList.hpp"
|
||||
@@ -42,42 +45,48 @@
|
||||
#include "track-selection-constraints/SameReleaseConstraint.hpp"
|
||||
#include "track-selection-constraints/TrackCandidateContext.hpp"
|
||||
|
||||
#define LOG(sev, message) LMS_LOG(RECOMMENDATION, sev, "[clusters] " << message)
|
||||
#define LOG(sev, message) LMS_LOG(RECOMMENDATION, sev, "[tags] " << message)
|
||||
|
||||
namespace lms::recommendation
|
||||
{
|
||||
namespace
|
||||
{
|
||||
template<typename IdType>
|
||||
std::vector<std::pair<IdType, std::size_t>> computeClusterOverlap(
|
||||
const std::unordered_map<IdType, std::vector<db::ClusterId>>& profileMap,
|
||||
std::vector<std::pair<IdType, std::size_t>> computeTagOverlap(
|
||||
const std::unordered_map<IdType, std::vector<TagId>>& profileMap,
|
||||
const std::unordered_set<IdType>& excludeIds,
|
||||
const std::unordered_set<db::ClusterId>& queryClusters)
|
||||
const std::unordered_set<TagId>& queryTags)
|
||||
{
|
||||
std::vector<std::pair<IdType, std::size_t>> results;
|
||||
for (const auto& [candidateId, candidateClusters] : profileMap)
|
||||
|
||||
for (const auto& [candidateId, candidateTags] : profileMap)
|
||||
{
|
||||
if (excludeIds.contains(candidateId))
|
||||
continue;
|
||||
|
||||
std::size_t count{};
|
||||
for (const db::ClusterId clusterId : candidateClusters)
|
||||
if (queryClusters.contains(clusterId))
|
||||
for (const TagId& tagId : candidateTags)
|
||||
{
|
||||
if (queryTags.contains(tagId))
|
||||
++count;
|
||||
}
|
||||
|
||||
if (count > 0)
|
||||
results.emplace_back(candidateId, count);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
template<typename IdType>
|
||||
ResultContainer<IdType> findSimilarByClusterOverlap(
|
||||
const std::unordered_map<IdType, std::vector<db::ClusterId>>& profileMap,
|
||||
ResultContainer<IdType> findSimilarByTagOverlap(
|
||||
const std::unordered_map<IdType, std::vector<TagId>>& profileMap,
|
||||
IdType queryId,
|
||||
const std::vector<db::ClusterId>& queryClusters,
|
||||
const std::vector<TagId>& queryTags,
|
||||
std::size_t maxCount)
|
||||
{
|
||||
const std::unordered_set<db::ClusterId> querySet{ queryClusters.cbegin(), queryClusters.cend() };
|
||||
auto overlapCounts{ computeClusterOverlap(profileMap, { queryId }, querySet) };
|
||||
const std::unordered_set<TagId> querySet{ queryTags.cbegin(), queryTags.cend() };
|
||||
auto overlapCounts{ computeTagOverlap(profileMap, { queryId }, querySet) };
|
||||
|
||||
const std::size_t resultCount{ std::min(maxCount, overlapCounts.size()) };
|
||||
std::partial_sort(overlapCounts.begin(), std::next(overlapCounts.begin(), resultCount), overlapCounts.end(),
|
||||
@@ -92,51 +101,51 @@ namespace lms::recommendation
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<IEngine> createClustersEngine(db::IDb& db)
|
||||
std::unique_ptr<IEngine> createTagsEngine(db::IDb& db)
|
||||
{
|
||||
return std::make_unique<ClusterEngine>(db);
|
||||
return std::make_unique<TagsEngine>(db);
|
||||
}
|
||||
|
||||
ClusterEngine::ClusterEngine(db::IDb& db)
|
||||
TagsEngine::TagsEngine(db::IDb& db)
|
||||
: _db{ db }
|
||||
{
|
||||
constexpr float sameReleaseWeight{ 0.5F };
|
||||
constexpr float sameArtistWeight{ 0.5F };
|
||||
|
||||
_trackEvaluator.addHardConstraint(std::make_unique<DuplicateTrackConstraint>());
|
||||
_trackEvaluator.addHardConstraint(std::make_unique<SameRecordingMBIDConstraint>(_trackMetadata));
|
||||
_trackEvaluator.addSoftConstraint(std::make_unique<SameReleaseConstraint>(_trackMetadata), sameReleaseWeight);
|
||||
_trackEvaluator.addSoftConstraint(std::make_unique<SameArtistConstraint>(_trackMetadata), sameArtistWeight);
|
||||
}
|
||||
|
||||
ClusterEngine::~ClusterEngine() = default;
|
||||
TagsEngine::~TagsEngine() = default;
|
||||
|
||||
void ClusterEngine::load()
|
||||
void TagsEngine::load()
|
||||
{
|
||||
LMS_SCOPED_TRACE_OVERVIEW("ClustersEngine", "Loading");
|
||||
LMS_SCOPED_TRACE_OVERVIEW("TagsEngine", "Loading");
|
||||
LOG(INFO, "loading...");
|
||||
|
||||
_trackMetadata.clear();
|
||||
_trackClusters.clear();
|
||||
_releaseClusters.clear();
|
||||
_artistClusters.clear();
|
||||
_trackTags.clear();
|
||||
_releaseTags.clear();
|
||||
_artistTags.clear();
|
||||
|
||||
db::Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
buildTrackClusters(session);
|
||||
buildTrackTags(session);
|
||||
buildTrackMetadata(session);
|
||||
buildReleaseClusters();
|
||||
buildArtistClusters();
|
||||
buildReleaseTags();
|
||||
buildArtistTags();
|
||||
|
||||
LOG(INFO, "loaded " << _trackClusters.size() << " tracks, " << _releaseClusters.size() << " releases, " << _artistClusters.size() << " artists");
|
||||
LOG(INFO, "loaded " << _trackTags.size() << " tracks, " << _releaseTags.size() << " releases, " << _artistTags.size() << " artists");
|
||||
}
|
||||
|
||||
void ClusterEngine::buildTrackMetadata(db::Session& session)
|
||||
void TagsEngine::buildTrackMetadata(db::Session& session)
|
||||
{
|
||||
LOG(DEBUG, "building track metadata...");
|
||||
|
||||
// Ensure cluster tracks with no release/artist have an entry
|
||||
for (const auto& [trackId, _] : _trackClusters)
|
||||
for (const auto& [trackId, tags] : _trackTags)
|
||||
_trackMetadata.try_emplace(trackId);
|
||||
|
||||
db::Track::find(session, db::Track::FindParameters{}, [&](const db::Track::pointer& track) {
|
||||
@@ -159,6 +168,7 @@ namespace lms::recommendation
|
||||
{
|
||||
db::Release::FindParameters params;
|
||||
params.setArtist(artist->getId());
|
||||
|
||||
for (const db::ReleaseId releaseId : db::Release::findIds(session, params).results)
|
||||
{
|
||||
db::Track::FindParameters trackParams;
|
||||
@@ -176,82 +186,126 @@ namespace lms::recommendation
|
||||
std::sort(metadata.artistIds.begin(), metadata.artistIds.end());
|
||||
}
|
||||
|
||||
void ClusterEngine::buildTrackClusters(db::Session& session)
|
||||
void TagsEngine::buildTrackTags(db::Session& session)
|
||||
{
|
||||
LOG(DEBUG, "building track clusters...");
|
||||
LOG(DEBUG, "building track tags...");
|
||||
|
||||
db::Cluster::find(session, db::Cluster::FindParameters{}, [&](const db::Cluster::pointer& cluster) {
|
||||
const db::ClusterId clusterId{ cluster->getId() };
|
||||
for (const db::TrackId trackId : cluster->getTracks().results)
|
||||
_trackClusters[trackId].push_back(clusterId);
|
||||
db::Genre::find(session, db::Genre::FindParameters{}, [&](const db::Genre::pointer& genre) {
|
||||
const TagId tagId{ .type = TagId::Type::Genre, .id = genre->getId() };
|
||||
|
||||
db::Track::FindParameters params;
|
||||
params.filters.setGenre(genre->getId());
|
||||
|
||||
db::Track::find(session, params, [&](const db::Track::pointer& track) {
|
||||
_trackTags[track->getId()].push_back(tagId);
|
||||
});
|
||||
});
|
||||
|
||||
db::Mood::find(session, db::Mood::FindParameters{}, [&](const db::Mood::pointer& mood) {
|
||||
const TagId tagId{ .type = TagId::Type::Mood, .id = mood->getId() };
|
||||
|
||||
db::Track::FindParameters params;
|
||||
params.filters.setMood(mood->getId());
|
||||
|
||||
db::Track::find(session, params, [&](const db::Track::pointer& track) {
|
||||
_trackTags[track->getId()].push_back(tagId);
|
||||
});
|
||||
});
|
||||
|
||||
db::Grouping::find(session, db::Grouping::FindParameters{}, [&](const db::Grouping::pointer& grouping) {
|
||||
const TagId tagId{ .type = TagId::Type::Grouping, .id = grouping->getId() };
|
||||
|
||||
db::Track::FindParameters params;
|
||||
params.filters.setGrouping(grouping->getId());
|
||||
|
||||
db::Track::find(session, params, [&](const db::Track::pointer& track) {
|
||||
_trackTags[track->getId()].push_back(tagId);
|
||||
});
|
||||
});
|
||||
|
||||
db::Language::find(session, db::Language::FindParameters{}, [&](const db::Language::pointer& language) {
|
||||
const TagId tagId{ .type = TagId::Type::Language, .id = language->getId() };
|
||||
|
||||
db::Track::FindParameters params;
|
||||
params.filters.setLanguage(language->getId());
|
||||
|
||||
db::Track::find(session, params, [&](const db::Track::pointer& track) {
|
||||
_trackTags[track->getId()].push_back(tagId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void ClusterEngine::buildReleaseClusters()
|
||||
void TagsEngine::buildReleaseTags()
|
||||
{
|
||||
LOG(DEBUG, "building release clusters...");
|
||||
LOG(DEBUG, "building release tags...");
|
||||
|
||||
for (const auto& [trackId, clusters] : _trackClusters)
|
||||
for (const auto& [trackId, tags] : _trackTags)
|
||||
{
|
||||
const auto metaIt{ _trackMetadata.find(trackId) };
|
||||
if (metaIt == _trackMetadata.cend())
|
||||
continue;
|
||||
|
||||
if (const db::ReleaseId releaseId{ metaIt->second.releaseId }; releaseId.isValid())
|
||||
for (const db::ClusterId clusterId : clusters)
|
||||
_releaseClusters[releaseId].push_back(clusterId);
|
||||
{
|
||||
for (const TagId& tagId : tags)
|
||||
_releaseTags[releaseId].push_back(tagId);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& [_, clusters] : _releaseClusters)
|
||||
for (auto& [releaseId, tags] : _releaseTags)
|
||||
{
|
||||
std::sort(clusters.begin(), clusters.end());
|
||||
clusters.erase(std::unique(clusters.begin(), clusters.end()), clusters.end());
|
||||
std::sort(tags.begin(), tags.end());
|
||||
tags.erase(std::unique(tags.begin(), tags.end()), tags.end());
|
||||
}
|
||||
}
|
||||
|
||||
void ClusterEngine::buildArtistClusters()
|
||||
void TagsEngine::buildArtistTags()
|
||||
{
|
||||
LOG(DEBUG, "building artist clusters...");
|
||||
LOG(DEBUG, "building artist tags...");
|
||||
|
||||
for (const auto& [trackId, clusters] : _trackClusters)
|
||||
for (const auto& [trackId, tags] : _trackTags)
|
||||
{
|
||||
const auto metaIt{ _trackMetadata.find(trackId) };
|
||||
if (metaIt == _trackMetadata.cend())
|
||||
continue;
|
||||
|
||||
for (const db::ArtistId artistId : metaIt->second.artistIds)
|
||||
for (const db::ClusterId clusterId : clusters)
|
||||
_artistClusters[artistId].push_back(clusterId);
|
||||
{
|
||||
for (const TagId& tagId : tags)
|
||||
_artistTags[artistId].push_back(tagId);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& [_, clusters] : _artistClusters)
|
||||
for (auto& [artistId, tags] : _artistTags)
|
||||
{
|
||||
std::sort(clusters.begin(), clusters.end());
|
||||
clusters.erase(std::unique(clusters.begin(), clusters.end()), clusters.end());
|
||||
std::sort(tags.begin(), tags.end());
|
||||
tags.erase(std::unique(tags.begin(), tags.end()), tags.end());
|
||||
}
|
||||
}
|
||||
|
||||
TrackResults ClusterEngine::findSimilarTracks(std::span<const db::TrackId> trackIds, std::size_t maxCount) const
|
||||
TrackResults TagsEngine::findSimilarTracks(std::span<const db::TrackId> trackIds, std::size_t maxCount) const
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("ClustersEngine", "Find similar tracks");
|
||||
LMS_SCOPED_TRACE_DETAILED("TagsEngine", "Find similar tracks");
|
||||
|
||||
if (maxCount == 0 || trackIds.empty())
|
||||
return {};
|
||||
|
||||
std::unordered_set<db::ClusterId> queryClusters;
|
||||
std::unordered_set<TagId> queryTags;
|
||||
for (const db::TrackId trackId : trackIds)
|
||||
{
|
||||
const auto it{ _trackClusters.find(trackId) };
|
||||
if (it != _trackClusters.cend())
|
||||
for (const db::ClusterId clusterId : it->second)
|
||||
queryClusters.insert(clusterId);
|
||||
const auto it{ _trackTags.find(trackId) };
|
||||
if (it != _trackTags.cend())
|
||||
{
|
||||
for (const TagId& tagId : it->second)
|
||||
queryTags.insert(tagId);
|
||||
}
|
||||
}
|
||||
|
||||
if (queryClusters.empty())
|
||||
if (queryTags.empty())
|
||||
return {};
|
||||
|
||||
const std::unordered_set<db::TrackId> excludeSet{ std::cbegin(trackIds), std::cend(trackIds) };
|
||||
auto overlapCounts{ computeClusterOverlap(_trackClusters, excludeSet, queryClusters) };
|
||||
auto overlapCounts{ computeTagOverlap(_trackTags, excludeSet, queryTags) };
|
||||
|
||||
static constexpr std::size_t oversamplingFactor{ 5 };
|
||||
const std::size_t candidateCount{ std::min(maxCount * oversamplingFactor, overlapCounts.size()) };
|
||||
@@ -268,7 +322,7 @@ namespace lms::recommendation
|
||||
return greedySelect(std::move(candidates), std::move(seeds), maxCount);
|
||||
}
|
||||
|
||||
TrackResults ClusterEngine::greedySelect(std::vector<db::TrackId> candidates, std::vector<db::TrackId> selectedTracks, std::size_t maxCount) const
|
||||
TrackResults TagsEngine::greedySelect(std::vector<db::TrackId> candidates, std::vector<db::TrackId> selectedTracks, std::size_t maxCount) const
|
||||
{
|
||||
selectedTracks.reserve(selectedTracks.size() + maxCount);
|
||||
|
||||
@@ -310,9 +364,9 @@ namespace lms::recommendation
|
||||
return res;
|
||||
}
|
||||
|
||||
TrackResults ClusterEngine::findSimilarTracksFromTrackList(db::TrackListId tracklistId, std::size_t maxCount) const
|
||||
TrackResults TagsEngine::findSimilarTracksFromTrackList(db::TrackListId tracklistId, std::size_t maxCount) const
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("ClustersEngine", "Find similar tracks from tracklist");
|
||||
LMS_SCOPED_TRACE_DETAILED("TagsEngine", "Find similar tracks from tracklist");
|
||||
|
||||
if (maxCount == 0)
|
||||
return {};
|
||||
@@ -335,37 +389,37 @@ namespace lms::recommendation
|
||||
return findSimilarTracks(trackIds, maxCount);
|
||||
}
|
||||
|
||||
ReleaseResults ClusterEngine::findSimilarReleases(db::ReleaseId releaseId, std::size_t maxCount) const
|
||||
ReleaseResults TagsEngine::findSimilarReleases(db::ReleaseId releaseId, std::size_t maxCount) const
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("ClustersEngine", "Find similar releases");
|
||||
LMS_SCOPED_TRACE_DETAILED("TagsEngine", "Find similar releases");
|
||||
|
||||
if (maxCount == 0)
|
||||
return {};
|
||||
|
||||
const auto queryIt{ _releaseClusters.find(releaseId) };
|
||||
if (queryIt == _releaseClusters.cend() || queryIt->second.empty())
|
||||
const auto queryIt{ _releaseTags.find(releaseId) };
|
||||
if (queryIt == _releaseTags.cend() || queryIt->second.empty())
|
||||
return {};
|
||||
|
||||
return findSimilarByClusterOverlap<db::ReleaseId>(_releaseClusters, releaseId, queryIt->second, maxCount);
|
||||
return findSimilarByTagOverlap<db::ReleaseId>(_releaseTags, releaseId, queryIt->second, maxCount);
|
||||
}
|
||||
|
||||
ArtistResults ClusterEngine::findSimilarArtists(db::ArtistId artistId, core::EnumSet<db::TrackArtistLinkType> linkTypes, std::size_t maxCount) const
|
||||
ArtistResults TagsEngine::findSimilarArtists(db::ArtistId artistId, core::EnumSet<db::TrackArtistLinkType> linkTypes, std::size_t maxCount) const
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("ClustersEngine", "Find similar artists");
|
||||
LMS_SCOPED_TRACE_DETAILED("TagsEngine", "Find similar artists");
|
||||
|
||||
if (maxCount == 0 || !linkTypes.contains(db::TrackArtistLinkType::Artist))
|
||||
return {};
|
||||
|
||||
const auto queryIt{ _artistClusters.find(artistId) };
|
||||
if (queryIt == _artistClusters.cend() || queryIt->second.empty())
|
||||
const auto queryIt{ _artistTags.find(artistId) };
|
||||
if (queryIt == _artistTags.cend() || queryIt->second.empty())
|
||||
return {};
|
||||
|
||||
return findSimilarByClusterOverlap<db::ArtistId>(_artistClusters, artistId, queryIt->second, maxCount);
|
||||
return findSimilarByTagOverlap<db::ArtistId>(_artistTags, artistId, queryIt->second, maxCount);
|
||||
}
|
||||
|
||||
TrackResults ClusterEngine::findTrackSimilarityPath(db::TrackId startTrackId, db::TrackId endTrackId, std::size_t maxCount) const
|
||||
TrackResults TagsEngine::findTrackSimilarityPath(db::TrackId startTrackId, db::TrackId endTrackId, std::size_t maxCount) const
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("ClustersEngine", "Find track similarity path");
|
||||
LMS_SCOPED_TRACE_DETAILED("TagsEngine", "Find track similarity path");
|
||||
|
||||
if (maxCount == 0)
|
||||
return {};
|
||||
+50
-12
@@ -19,11 +19,14 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <span>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "database/objects/ClusterId.hpp"
|
||||
#include "database/IdType.hpp"
|
||||
|
||||
#include "track-selection-constraints/TrackCandidateEvaluator.hpp"
|
||||
#include "track-selection-constraints/TrackMetadata.hpp"
|
||||
|
||||
@@ -36,13 +39,48 @@ namespace lms::db
|
||||
|
||||
namespace lms::recommendation
|
||||
{
|
||||
class ClusterEngine : public IEngine
|
||||
struct TagId
|
||||
{
|
||||
enum class Type : std::uint8_t
|
||||
{
|
||||
Genre,
|
||||
Mood,
|
||||
Grouping,
|
||||
Language
|
||||
};
|
||||
Type type;
|
||||
db::IdType id;
|
||||
|
||||
auto operator<=>(const TagId&) const = default;
|
||||
};
|
||||
} // namespace lms::recommendation
|
||||
|
||||
namespace std
|
||||
{
|
||||
template<>
|
||||
struct hash<lms::recommendation::TagId>
|
||||
{
|
||||
std::size_t operator()(const lms::recommendation::TagId& tagId) const noexcept
|
||||
{
|
||||
using UnderlyingType1 = std::underlying_type<lms::recommendation::TagId::Type>::type;
|
||||
using UnderlyingType2 = lms::db::IdType::ValueType;
|
||||
|
||||
const std::size_t h1{ std::hash<UnderlyingType1>{}(static_cast<UnderlyingType1>(tagId.type)) };
|
||||
const std::size_t h2{ std::hash<UnderlyingType2>{}(tagId.id.getValue()) };
|
||||
return h1 ^ (h2 << 4);
|
||||
}
|
||||
};
|
||||
} // namespace std
|
||||
|
||||
namespace lms::recommendation
|
||||
{
|
||||
class TagsEngine : public IEngine
|
||||
{
|
||||
public:
|
||||
ClusterEngine(db::IDb& db);
|
||||
~ClusterEngine() override;
|
||||
ClusterEngine(const ClusterEngine&) = delete;
|
||||
ClusterEngine& operator=(const ClusterEngine&) = delete;
|
||||
TagsEngine(db::IDb& db);
|
||||
~TagsEngine() override;
|
||||
TagsEngine(const TagsEngine&) = delete;
|
||||
TagsEngine& operator=(const TagsEngine&) = delete;
|
||||
|
||||
private:
|
||||
void load() override;
|
||||
@@ -54,17 +92,17 @@ namespace lms::recommendation
|
||||
ArtistResults findSimilarArtists(db::ArtistId artistId, core::EnumSet<db::TrackArtistLinkType> linkTypes, std::size_t maxCount) const override;
|
||||
|
||||
TrackResults greedySelect(std::vector<db::TrackId> candidates, std::vector<db::TrackId> selectedTracks, std::size_t maxCount) const;
|
||||
void buildTrackClusters(db::Session& session);
|
||||
void buildTrackTags(db::Session& session);
|
||||
void buildTrackMetadata(db::Session& session);
|
||||
void buildReleaseClusters();
|
||||
void buildArtistClusters();
|
||||
void buildReleaseTags();
|
||||
void buildArtistTags();
|
||||
|
||||
db::IDb& _db;
|
||||
|
||||
TrackMetadataMap _trackMetadata;
|
||||
std::unordered_map<db::TrackId, std::vector<db::ClusterId>> _trackClusters;
|
||||
std::unordered_map<db::ReleaseId, std::vector<db::ClusterId>> _releaseClusters;
|
||||
std::unordered_map<db::ArtistId, std::vector<db::ClusterId>> _artistClusters;
|
||||
std::unordered_map<db::TrackId, std::vector<TagId>> _trackTags;
|
||||
std::unordered_map<db::ReleaseId, std::vector<TagId>> _releaseTags;
|
||||
std::unordered_map<db::ArtistId, std::vector<TagId>> _artistTags;
|
||||
TrackCandidateEvaluator _trackEvaluator;
|
||||
};
|
||||
} // namespace lms::recommendation
|
||||
@@ -28,7 +28,7 @@ add_library(lmsscanner STATIC
|
||||
impl/steps/ScanStepCheckForDuplicatedFiles.cpp
|
||||
impl/steps/ScanStepCheckForRemovedFiles.cpp
|
||||
impl/steps/ScanStepCompact.cpp
|
||||
impl/steps/ScanStepComputeClusterStats.cpp
|
||||
impl/steps/ScanStepComputeGenreStats.cpp
|
||||
impl/steps/ScanStepExtractMusicNNEmbeddings.cpp
|
||||
impl/steps/ScanStepOptimize.cpp
|
||||
impl/steps/ScanStepRemoveOrphanedDbEntries.cpp
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
#include "steps/ScanStepCheckForDuplicatedFiles.hpp"
|
||||
#include "steps/ScanStepCheckForRemovedFiles.hpp"
|
||||
#include "steps/ScanStepCompact.hpp"
|
||||
#include "steps/ScanStepComputeClusterStats.hpp"
|
||||
#include "steps/ScanStepComputeGenreStats.hpp"
|
||||
#include "steps/ScanStepExtractMusicNNEmbeddings.hpp"
|
||||
#include "steps/ScanStepOptimize.hpp"
|
||||
#include "steps/ScanStepRemoveOrphanedDbEntries.hpp"
|
||||
@@ -528,7 +528,7 @@ namespace lms::scanner
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepRemoveOrphanedDbEntries>(params));
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepCompact>(params));
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepOptimize>(params));
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepComputeClusterStats>(params));
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepComputeGenreStats>(params));
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepCheckForDuplicatedFiles>(params));
|
||||
|
||||
// Audio extraction scan step must be last as it is the most long running
|
||||
|
||||
@@ -35,8 +35,12 @@
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/ReleaseArtistLink.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
@@ -308,6 +312,62 @@ namespace lms::scanner
|
||||
return dbMedium;
|
||||
}
|
||||
|
||||
std::vector<db::Genre::pointer> getOrCreateGenres(db::Session& session, std::span<const std::string> names)
|
||||
{
|
||||
std::vector<db::Genre::pointer> genres;
|
||||
genres.reserve(names.size());
|
||||
for (const std::string& name : names)
|
||||
{
|
||||
db::Genre::pointer genre{ db::Genre::find(session, name) };
|
||||
if (!genre)
|
||||
genre = session.create<db::Genre>(name);
|
||||
genres.push_back(genre);
|
||||
}
|
||||
return genres;
|
||||
}
|
||||
|
||||
std::vector<db::Grouping::pointer> getOrCreateGroupings(db::Session& session, std::span<const std::string> names)
|
||||
{
|
||||
std::vector<db::Grouping::pointer> groupings;
|
||||
groupings.reserve(names.size());
|
||||
for (const std::string& name : names)
|
||||
{
|
||||
db::Grouping::pointer grouping{ db::Grouping::find(session, name) };
|
||||
if (!grouping)
|
||||
grouping = session.create<db::Grouping>(name);
|
||||
groupings.push_back(grouping);
|
||||
}
|
||||
return groupings;
|
||||
}
|
||||
|
||||
std::vector<db::Language::pointer> getOrCreateLanguages(db::Session& session, std::span<const std::string> names)
|
||||
{
|
||||
std::vector<db::Language::pointer> languages;
|
||||
languages.reserve(names.size());
|
||||
for (const std::string& name : names)
|
||||
{
|
||||
db::Language::pointer language{ db::Language::find(session, name) };
|
||||
if (!language)
|
||||
language = session.create<db::Language>(name);
|
||||
languages.push_back(language);
|
||||
}
|
||||
return languages;
|
||||
}
|
||||
|
||||
std::vector<db::Mood::pointer> getOrCreateMoods(db::Session& session, std::span<const std::string> names)
|
||||
{
|
||||
std::vector<db::Mood::pointer> moods;
|
||||
moods.reserve(names.size());
|
||||
for (const std::string& name : names)
|
||||
{
|
||||
db::Mood::pointer mood{ db::Mood::find(session, name) };
|
||||
if (!mood)
|
||||
mood = session.create<db::Mood>(name);
|
||||
moods.push_back(mood);
|
||||
}
|
||||
return moods;
|
||||
}
|
||||
|
||||
std::vector<db::Cluster::pointer> getOrCreateClusters(db::Session& session, const Track& track)
|
||||
{
|
||||
std::vector<db::Cluster::pointer> clusters;
|
||||
@@ -327,12 +387,6 @@ namespace lms::scanner
|
||||
}
|
||||
} };
|
||||
|
||||
// TODO: migrate these fields in dedicated tables in DB
|
||||
getOrCreateClusters("GENRE", track.genres);
|
||||
getOrCreateClusters("MOOD", track.moods);
|
||||
getOrCreateClusters("LANGUAGE", track.languages);
|
||||
getOrCreateClusters("GROUPING", track.groupings);
|
||||
|
||||
for (const auto& [tag, values] : track.userExtraTags)
|
||||
getOrCreateClusters(tag, values);
|
||||
|
||||
@@ -778,6 +832,10 @@ namespace lms::scanner
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Performer, role, performers, allowFallback);
|
||||
|
||||
track.modify()->setClusters(getOrCreateClusters(dbSession, _file->track));
|
||||
track.modify()->setGenres(getOrCreateGenres(dbSession, _file->track.genres));
|
||||
track.modify()->setGroupings(getOrCreateGroupings(dbSession, _file->track.groupings));
|
||||
track.modify()->setLanguages(getOrCreateLanguages(dbSession, _file->track.languages));
|
||||
track.modify()->setMoods(getOrCreateMoods(dbSession, _file->track.moods));
|
||||
track.modify()->setName(title);
|
||||
track.modify()->setTrackNumber(_file->track.position);
|
||||
track.modify()->setDate(_file->track.date);
|
||||
|
||||
+18
-29
@@ -17,68 +17,59 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ScanStepComputeClusterStats.hpp"
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "ScanStepComputeGenreStats.hpp"
|
||||
|
||||
#include "database/IDb.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
|
||||
#include "ScanContext.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
bool ScanStepComputeClusterStats::needProcess(const ScanContext& context) const
|
||||
bool ScanStepComputeGenreStats::needProcess(const ScanContext& context) const
|
||||
{
|
||||
return context.stats.getChangesCount() > 0;
|
||||
}
|
||||
|
||||
void ScanStepComputeClusterStats::process(ScanContext& context)
|
||||
void ScanStepComputeGenreStats::process(ScanContext& context)
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
Session& dbSession{ _db.getTLSSession() };
|
||||
|
||||
const std::size_t clusterCount{ [&] {
|
||||
const std::size_t genreCount{ [&] {
|
||||
auto transaction{ dbSession.createReadTransaction() };
|
||||
return Cluster::getCount(dbSession);
|
||||
return Genre::getCount(dbSession);
|
||||
}() };
|
||||
|
||||
context.currentStepStats.totalElems = clusterCount;
|
||||
context.currentStepStats.totalElems = genreCount;
|
||||
|
||||
foreachSubRange(Range{ 0, clusterCount }, 100, [&](Range range) {
|
||||
const std::vector<ClusterId> clusterIds{ [&] {
|
||||
Cluster::FindParameters params;
|
||||
foreachSubRange(Range{ 0, genreCount }, 100, [&](Range range) {
|
||||
const std::vector<GenreId> genreIds{ [&] {
|
||||
Genre::FindParameters params;
|
||||
params.setRange(range);
|
||||
|
||||
{
|
||||
auto transaction{ dbSession.createReadTransaction() };
|
||||
return std::move(Cluster::findIds(dbSession, params).results);
|
||||
}
|
||||
auto transaction{ dbSession.createReadTransaction() };
|
||||
return std::move(Genre::findIds(dbSession, params).results);
|
||||
}() };
|
||||
|
||||
for (const ClusterId clusterId : clusterIds)
|
||||
for (const GenreId genreId : genreIds)
|
||||
{
|
||||
if (_abortScan)
|
||||
break;
|
||||
|
||||
std::size_t trackCount;
|
||||
std::size_t releaseCount;
|
||||
|
||||
{
|
||||
auto transaction{ dbSession.createReadTransaction() };
|
||||
|
||||
trackCount = Cluster::computeTrackCount(dbSession, clusterId);
|
||||
releaseCount = Cluster::computeReleaseCount(dbSession, clusterId);
|
||||
trackCount = Genre::computeTrackCount(dbSession, genreId);
|
||||
releaseCount = Genre::computeReleaseCount(dbSession, genreId);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ dbSession.createWriteTransaction() };
|
||||
|
||||
auto cluster{ Cluster::find(dbSession, clusterId) };
|
||||
cluster.modify()->setTrackCount(trackCount);
|
||||
cluster.modify()->setReleaseCount(releaseCount);
|
||||
auto genre{ Genre::find(dbSession, genreId) };
|
||||
genre.modify()->setTrackCount(trackCount);
|
||||
genre.modify()->setReleaseCount(releaseCount);
|
||||
}
|
||||
|
||||
context.currentStepStats.processedElems++;
|
||||
@@ -87,7 +78,5 @@ namespace lms::scanner
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Recomputed stats for " << context.currentStepStats.processedElems << " clusters!");
|
||||
}
|
||||
} // namespace lms::scanner
|
||||
+3
-3
@@ -23,14 +23,14 @@
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
class ScanStepComputeClusterStats : public ScanStepBase
|
||||
class ScanStepComputeGenreStats : public ScanStepBase
|
||||
{
|
||||
public:
|
||||
using ScanStepBase::ScanStepBase;
|
||||
|
||||
private:
|
||||
ScanStep getStep() const override { return ScanStep::ComputeClusterStats; }
|
||||
core::LiteralString getStepName() const override { return "Compute cluster stats"; }
|
||||
ScanStep getStep() const override { return ScanStep::ComputeGenreStats; }
|
||||
core::LiteralString getStepName() const override { return "Compute genre stats"; }
|
||||
bool needProcess(const ScanContext& context) const override;
|
||||
void process(ScanContext& context) override;
|
||||
};
|
||||
@@ -25,7 +25,11 @@
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Language.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||
@@ -44,6 +48,10 @@ namespace lms::scanner
|
||||
{
|
||||
removeOrphanedClusters(context);
|
||||
removeOrphanedClusterTypes(context);
|
||||
removeOrphanedGenres(context);
|
||||
removeOrphanedGroupings(context);
|
||||
removeOrphanedLanguages(context);
|
||||
removeOrphanedMoods(context);
|
||||
removeOrphanedArtists(context);
|
||||
removeOrphanedReleases(context);
|
||||
removeOrphanedMediums(context); // after release so that most entries are removed using the medium foreign key
|
||||
@@ -66,6 +74,30 @@ namespace lms::scanner
|
||||
removeOrphanedEntries<db::ClusterType>(context);
|
||||
}
|
||||
|
||||
void ScanStepRemoveOrphanedDbEntries::removeOrphanedGenres(ScanContext& context)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned genres...");
|
||||
removeOrphanedEntries<db::Genre>(context);
|
||||
}
|
||||
|
||||
void ScanStepRemoveOrphanedDbEntries::removeOrphanedGroupings(ScanContext& context)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned groupings...");
|
||||
removeOrphanedEntries<db::Grouping>(context);
|
||||
}
|
||||
|
||||
void ScanStepRemoveOrphanedDbEntries::removeOrphanedLanguages(ScanContext& context)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned languages...");
|
||||
removeOrphanedEntries<db::Language>(context);
|
||||
}
|
||||
|
||||
void ScanStepRemoveOrphanedDbEntries::removeOrphanedMoods(ScanContext& context)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned moods...");
|
||||
removeOrphanedEntries<db::Mood>(context);
|
||||
}
|
||||
|
||||
void ScanStepRemoveOrphanedDbEntries::removeOrphanedArtists(ScanContext& context)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned artists...");
|
||||
|
||||
@@ -36,6 +36,10 @@ namespace lms::scanner
|
||||
|
||||
void removeOrphanedClusters(ScanContext& context);
|
||||
void removeOrphanedClusterTypes(ScanContext& context);
|
||||
void removeOrphanedGenres(ScanContext& context);
|
||||
void removeOrphanedGroupings(ScanContext& context);
|
||||
void removeOrphanedLanguages(ScanContext& context);
|
||||
void removeOrphanedMoods(ScanContext& context);
|
||||
void removeOrphanedArtists(ScanContext& context);
|
||||
void removeOrphanedMediums(ScanContext& context);
|
||||
void removeOrphanedReleases(ScanContext& context);
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace lms::scanner
|
||||
AssociateTrackImages,
|
||||
CheckForDuplicatedFiles,
|
||||
CheckForRemovedFiles,
|
||||
ComputeClusterStats,
|
||||
ComputeGenreStats,
|
||||
Compact,
|
||||
ExtractMusicNNEmbeddings,
|
||||
Optimize,
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/User.hpp"
|
||||
@@ -87,18 +87,15 @@ namespace lms::api::subsonic
|
||||
// Mandatory param
|
||||
const std::string genre{ getMandatoryParameterAs<std::string>(context.getParameters(), "genre") };
|
||||
|
||||
if (const ClusterType::pointer clusterType{ ClusterType::find(context.getDbSession(), "GENRE") })
|
||||
if (const Genre::pointer genreObj{ Genre::find(context.getDbSession(), genre) })
|
||||
{
|
||||
if (const Cluster::pointer cluster{ clusterType->getCluster(genre) })
|
||||
{
|
||||
Release::FindParameters params;
|
||||
params.filters.setMediaLibrary(mediaLibraryId);
|
||||
params.filters.setClusters(std::initializer_list<ClusterId>{ cluster->getId() });
|
||||
params.setSortMethod(ReleaseSortMethod::Name);
|
||||
params.setRange(range);
|
||||
Release::FindParameters params;
|
||||
params.filters.setMediaLibrary(mediaLibraryId);
|
||||
params.filters.setGenre(genreObj->getId());
|
||||
params.setSortMethod(ReleaseSortMethod::Name);
|
||||
params.setRange(range);
|
||||
|
||||
releases = Release::findIds(context.getDbSession(), params);
|
||||
}
|
||||
releases = Release::findIds(context.getDbSession(), params);
|
||||
}
|
||||
}
|
||||
else if (type == "byYear")
|
||||
@@ -273,19 +270,15 @@ namespace lms::api::subsonic
|
||||
|
||||
auto transaction{ context.getDbSession().createReadTransaction() };
|
||||
|
||||
auto clusterType{ ClusterType::find(context.getDbSession(), "GENRE") };
|
||||
if (!clusterType)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
auto cluster{ clusterType->getCluster(genre) };
|
||||
if (!cluster)
|
||||
const Genre::pointer genreObj{ Genre::find(context.getDbSession(), genre) };
|
||||
if (!genreObj)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
Response response{ Response::createOkResponse(context.getServerProtocolVersion()) };
|
||||
Response::Node& songsByGenreNode{ response.createNode("songsByGenre") };
|
||||
|
||||
Track::FindParameters params;
|
||||
params.filters.setClusters(std::initializer_list<ClusterId>{ cluster->getId() });
|
||||
params.filters.setGenre(genreObj->getId());
|
||||
params.filters.setMediaLibrary(mediaLibrary);
|
||||
params.setRange(Range{ offset, count });
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "database/objects/ArtistInfo.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
@@ -379,14 +380,9 @@ namespace lms::api::subsonic
|
||||
|
||||
auto transaction{ context.getDbSession().createReadTransaction() };
|
||||
|
||||
const ClusterType::pointer clusterType{ ClusterType::find(context.getDbSession(), "GENRE") };
|
||||
if (clusterType)
|
||||
{
|
||||
const auto clusters{ clusterType->getClusters() };
|
||||
|
||||
for (const Cluster::pointer& cluster : clusters)
|
||||
genresNode.addArrayChild("genre", createGenreNode(context, cluster));
|
||||
}
|
||||
db::Genre::find(context.getDbSession(), db::Genre::FindParameters{}.setSortMethod(db::GenreSortMethod::Name), [&](const db::Genre::pointer& genre) {
|
||||
genresNode.addArrayChild("genre", createGenreNode(context, genre));
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -25,9 +25,11 @@
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/ReleaseArtistLink.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
@@ -134,13 +136,14 @@ namespace lms::api::subsonic
|
||||
|
||||
albumNode.setAttribute("playCount", core::Service<scrobbling::IScrobblingService>::get()->getCount(context.getUser()->getId(), release->getId()));
|
||||
|
||||
// Report the first GENRE for this track
|
||||
const ClusterType::pointer genreClusterType{ ClusterType::find(context.getDbSession(), "GENRE") };
|
||||
if (genreClusterType)
|
||||
// Report the first genre for this release
|
||||
{
|
||||
const auto clusters{ release->getClusters(genreClusterType->getId(), 1) };
|
||||
if (!clusters.empty())
|
||||
albumNode.setAttribute("genre", clusters.front()->getName());
|
||||
Genre::FindParameters p;
|
||||
p.setRelease(release->getId());
|
||||
p.setRange(Range{ 0, 1 });
|
||||
const auto genres{ Genre::find(context.getDbSession(), p).results };
|
||||
if (!genres.empty())
|
||||
albumNode.setAttribute("genre", genres.front()->getName());
|
||||
}
|
||||
|
||||
if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(context.getUser()->getId(), release->getId()) }; dateTime.isValid())
|
||||
@@ -168,31 +171,31 @@ namespace lms::api::subsonic
|
||||
albumNode.setAttribute("musicBrainzId", mbid ? mbid->toString() : "");
|
||||
}
|
||||
|
||||
auto addClusters{ [&](Response::Node::Key field, std::string_view clusterTypeName) {
|
||||
albumNode.createEmptyArrayValue(field);
|
||||
|
||||
Cluster::FindParameters params;
|
||||
albumNode.createEmptyArrayValue("moods");
|
||||
{
|
||||
Mood::FindParameters params;
|
||||
params.setRelease(release->getId());
|
||||
params.setClusterTypeName(clusterTypeName);
|
||||
|
||||
Cluster::find(context.getDbSession(), params, [&](const Cluster::pointer& cluster) {
|
||||
albumNode.addArrayValue(field, cluster->getName());
|
||||
Mood::find(context.getDbSession(), params, [&](const Mood::pointer& mood) {
|
||||
albumNode.addArrayValue("moods", mood->getName());
|
||||
});
|
||||
} };
|
||||
}
|
||||
|
||||
addClusters("moods", "MOOD");
|
||||
addClusters("groupings", "GROUPING");
|
||||
albumNode.createEmptyArrayValue("groupings");
|
||||
{
|
||||
Grouping::FindParameters params;
|
||||
params.setRelease(release->getId());
|
||||
Grouping::find(context.getDbSession(), params, [&](const Grouping::pointer& grouping) {
|
||||
albumNode.addArrayValue("groupings", grouping->getName());
|
||||
});
|
||||
}
|
||||
|
||||
// Genres
|
||||
albumNode.createEmptyArrayChild("genres");
|
||||
if (genreClusterType)
|
||||
{
|
||||
Cluster::FindParameters params;
|
||||
Genre::FindParameters params;
|
||||
params.setRelease(release->getId());
|
||||
params.setClusterType(genreClusterType->getId());
|
||||
|
||||
Cluster::find(context.getDbSession(), params, [&](const Cluster::pointer& cluster) {
|
||||
albumNode.addArrayChild("genres", createItemGenreNode(cluster->getName()));
|
||||
Genre::find(context.getDbSession(), params, [&](const Genre::pointer& genre) {
|
||||
albumNode.addArrayChild("genres", createItemGenreNode(genre->getName()));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -19,28 +19,28 @@
|
||||
|
||||
#include "responses/Genre.hpp"
|
||||
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
|
||||
#include "RequestContext.hpp"
|
||||
|
||||
namespace lms::api::subsonic
|
||||
{
|
||||
Response::Node createGenreNode(RequestContext& context, const db::Cluster::pointer& cluster)
|
||||
Response::Node createGenreNode(RequestContext& context, const db::Genre::pointer& genre)
|
||||
{
|
||||
Response::Node clusterNode;
|
||||
Response::Node genreNode;
|
||||
|
||||
switch (context.getResponseFormat())
|
||||
{
|
||||
case ResponseFormat::json:
|
||||
clusterNode.setAttribute("value", cluster->getName());
|
||||
genreNode.setAttribute("value", genre->getName());
|
||||
break;
|
||||
case ResponseFormat::xml:
|
||||
clusterNode.setValue(cluster->getName());
|
||||
genreNode.setValue(genre->getName());
|
||||
break;
|
||||
}
|
||||
clusterNode.setAttribute("songCount", cluster->getTrackCount());
|
||||
clusterNode.setAttribute("albumCount", cluster->getReleasesCount());
|
||||
genreNode.setAttribute("songCount", genre->getTrackCount());
|
||||
genreNode.setAttribute("albumCount", genre->getReleaseCount());
|
||||
|
||||
return clusterNode;
|
||||
return genreNode;
|
||||
}
|
||||
} // namespace lms::api::subsonic
|
||||
} // namespace lms::api::subsonic
|
||||
|
||||
@@ -25,12 +25,12 @@
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class Cluster;
|
||||
class Genre;
|
||||
}
|
||||
|
||||
namespace lms::api::subsonic
|
||||
{
|
||||
class RequestContext;
|
||||
|
||||
Response::Node createGenreNode(RequestContext& context, const db::ObjectPtr<db::Cluster>& cluster);
|
||||
Response::Node createGenreNode(RequestContext& context, const db::ObjectPtr<db::Genre>& genre);
|
||||
} // namespace lms::api::subsonic
|
||||
|
||||
@@ -31,10 +31,12 @@
|
||||
#include "database/Types.hpp"
|
||||
#include "database/objects/Artist.hpp"
|
||||
#include "database/objects/Artwork.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
#include "database/objects/Directory.hpp"
|
||||
#include "database/objects/Genre.hpp"
|
||||
#include "database/objects/Grouping.hpp"
|
||||
#include "database/objects/MediaLibrary.hpp"
|
||||
#include "database/objects/Medium.hpp"
|
||||
#include "database/objects/Mood.hpp"
|
||||
#include "database/objects/Release.hpp"
|
||||
#include "database/objects/ReleaseArtistLink.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
@@ -162,17 +164,10 @@ namespace lms::api::subsonic
|
||||
if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(context.getUser()->getId(), track->getId()) }; dateTime.isValid())
|
||||
trackResponse.setAttribute("starred", core::stringUtils::toISO8601String(dateTime));
|
||||
|
||||
// Report the first GENRE for this track
|
||||
std::vector<db::Cluster::pointer> genres;
|
||||
{
|
||||
db::Cluster::FindParameters params;
|
||||
params.setTrack(track->getId());
|
||||
params.setClusterTypeName("GENRE");
|
||||
|
||||
genres = db::Cluster::find(context.getDbSession(), params).results;
|
||||
if (!genres.empty())
|
||||
trackResponse.setAttribute("genre", genres.front()->getName());
|
||||
}
|
||||
// Report the first genre for this track
|
||||
const auto genres{ track->getGenres() };
|
||||
if (!genres.empty())
|
||||
trackResponse.setAttribute("genre", genres.front()->getName());
|
||||
|
||||
// OpenSubsonic specific fields (must always be set)
|
||||
if (!context.isOpenSubsonicEnabled())
|
||||
@@ -223,19 +218,13 @@ namespace lms::api::subsonic
|
||||
if (release)
|
||||
trackResponse.setAttribute("displayAlbumArtist", release->getArtistDisplayName());
|
||||
|
||||
auto addClusters{ [&](Response::Node::Key field, std::string_view clusterTypeName) {
|
||||
trackResponse.createEmptyArrayValue(field);
|
||||
trackResponse.createEmptyArrayValue("moods");
|
||||
for (const auto& mood : track->getMoods())
|
||||
trackResponse.addArrayValue("moods", mood->getName());
|
||||
|
||||
db::Cluster::FindParameters params;
|
||||
params.setTrack(track->getId());
|
||||
params.setClusterTypeName(clusterTypeName);
|
||||
|
||||
for (const auto& cluster : db::Cluster::find(context.getDbSession(), params).results)
|
||||
trackResponse.addArrayValue(field, cluster->getName());
|
||||
} };
|
||||
|
||||
addClusters("moods", "MOOD");
|
||||
addClusters("groupings", "GROUPING");
|
||||
trackResponse.createEmptyArrayValue("groupings");
|
||||
for (const auto& grouping : track->getGroupings())
|
||||
trackResponse.addArrayValue("groupings", grouping->getName());
|
||||
|
||||
// Genres
|
||||
trackResponse.createEmptyArrayChild("genres");
|
||||
|
||||
Reference in New Issue
Block a user