diff --git a/src/libs/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt
index b59f17d1..bc164a03 100644
--- a/src/libs/database/CMakeLists.txt
+++ b/src/libs/database/CMakeLists.txt
@@ -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
diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp
index 93e4f5f8..ba9d3a67 100644
--- a/src/libs/database/impl/Migration.cpp
+++ b/src/libs/database/impl/Migration.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{};
diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp
index df51253e..875a15ac 100644
--- a/src/libs/database/impl/Session.cpp
+++ b/src/libs/database/impl/Session.cpp
@@ -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
("auth_token");
_session.mapClass("cluster");
_session.mapClass("cluster_type");
+ _session.mapClass("genre");
+ _session.mapClass("grouping");
+ _session.mapClass("language");
+ _session.mapClass("mood");
_session.mapClass("country");
_session.mapClass("directory");
_session.mapClass("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)");
diff --git a/src/libs/database/impl/objects/Artist.cpp b/src/libs/database/impl/objects/Artist.cpp
index d064e7fc..ea9e6b48 100644
--- a/src/libs/database/impl/objects/Artist.cpp
+++ b/src/libs/database/impl/objects/Artist.cpp
@@ -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);
diff --git a/src/libs/database/impl/objects/Cluster.cpp b/src/libs/database/impl/objects/Cluster.cpp
index 44e63e45..b574e798 100644
--- a/src/libs/database/impl/objects/Cluster.cpp
+++ b/src/libs/database/impl/objects/Cluster.cpp
@@ -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"
diff --git a/src/libs/database/impl/objects/Genre.cpp b/src/libs/database/impl/objects/Genre.cpp
new file mode 100644
index 00000000..d2149ad3
--- /dev/null
+++ b/src/libs/database/impl/objects/Genre.cpp
@@ -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 .
+ */
+
+#include "database/objects/Genre.hpp"
+
+#include
+
+#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
+ Wt::Dbo::Query createQuery(Session& session, std::string_view itemToSelect, const Genre::FindParameters& params)
+ {
+ session.checkReadTransaction();
+
+ auto query{ session.getDboSession()->query("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
+ Wt::Dbo::Query createQuery(Session& session, const Genre::FindParameters& params)
+ {
+ std::string_view itemToSelect;
+
+ if constexpr (std::is_same_v)
+ itemToSelect = "g.id";
+ else if constexpr (std::is_same_v>)
+ itemToSelect = "g";
+ else
+ static_assert("Unhandled type");
+
+ return createQuery(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{ new Genre{ name } });
+ }
+
+ std::size_t Genre::getCount(Session& session)
+ {
+ session.checkReadTransaction();
+ return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM genre"));
+ }
+
+ RangeResults Genre::findIds(Session& session, const FindParameters& params)
+ {
+ session.checkReadTransaction();
+ auto query{ createQuery(session, params) };
+ return utils::execRangeQuery(query, params.range);
+ }
+
+ RangeResults Genre::find(Session& session, const FindParameters& params)
+ {
+ session.checkReadTransaction();
+ auto query{ createQuery>(session, params) };
+ return utils::execRangeQuery(query, params.range);
+ }
+
+ void Genre::find(Session& session, const FindParameters& params, std::function func)
+ {
+ session.checkReadTransaction();
+ auto query{ createQuery>(session, params) };
+ utils::forEachQueryRangeResult(query, params.range, func);
+ }
+
+ Genre::pointer Genre::find(Session& session, GenreId id)
+ {
+ session.checkReadTransaction();
+ return utils::fetchQuerySingleResult(session.getDboSession()->find().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().where("name = ?").bind(name));
+ }
+
+ RangeResults Genre::findOrphanIds(Session& session, std::optional range)
+ {
+ session.checkReadTransaction();
+ auto query{ session.getDboSession()->query("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(query, range);
+ }
+
+ std::size_t Genre::computeTrackCount(Session& session, GenreId id)
+ {
+ session.checkReadTransaction();
+ return utils::fetchQuerySingleResult(session.getDboSession()->query("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("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
diff --git a/src/libs/database/impl/objects/Grouping.cpp b/src/libs/database/impl/objects/Grouping.cpp
new file mode 100644
index 00000000..96f6683a
--- /dev/null
+++ b/src/libs/database/impl/objects/Grouping.cpp
@@ -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 .
+ */
+
+#include "database/objects/Grouping.hpp"
+
+#include
+
+#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
+ Wt::Dbo::Query createQuery(Session& session, std::string_view itemToSelect, const Grouping::FindParameters& params)
+ {
+ session.checkReadTransaction();
+
+ auto query{ session.getDboSession()->query("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
+ Wt::Dbo::Query createQuery(Session& session, const Grouping::FindParameters& params)
+ {
+ std::string_view itemToSelect;
+
+ if constexpr (std::is_same_v)
+ itemToSelect = "g.id";
+ else if constexpr (std::is_same_v>)
+ itemToSelect = "g";
+ else
+ static_assert("Unhandled type");
+
+ return createQuery(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{ new Grouping{ name } });
+ }
+
+ std::size_t Grouping::getCount(Session& session)
+ {
+ session.checkReadTransaction();
+ return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM grouping"));
+ }
+
+ RangeResults Grouping::findIds(Session& session, const FindParameters& params)
+ {
+ session.checkReadTransaction();
+ auto query{ createQuery(session, params) };
+ return utils::execRangeQuery(query, params.range);
+ }
+
+ RangeResults Grouping::find(Session& session, const FindParameters& params)
+ {
+ session.checkReadTransaction();
+ auto query{ createQuery>(session, params) };
+ return utils::execRangeQuery(query, params.range);
+ }
+
+ void Grouping::find(Session& session, const FindParameters& params, std::function func)
+ {
+ session.checkReadTransaction();
+ auto query{ createQuery>(session, params) };
+ utils::forEachQueryRangeResult(query, params.range, func);
+ }
+
+ Grouping::pointer Grouping::find(Session& session, GroupingId id)
+ {
+ session.checkReadTransaction();
+ return utils::fetchQuerySingleResult(session.getDboSession()->find().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().where("name = ?").bind(name));
+ }
+
+ RangeResults Grouping::findOrphanIds(Session& session, std::optional range)
+ {
+ session.checkReadTransaction();
+ auto query{ session.getDboSession()->query("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(query, range);
+ }
+
+} // namespace lms::db
diff --git a/src/libs/database/impl/objects/Language.cpp b/src/libs/database/impl/objects/Language.cpp
new file mode 100644
index 00000000..9a3698c2
--- /dev/null
+++ b/src/libs/database/impl/objects/Language.cpp
@@ -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 .
+ */
+
+#include "database/objects/Language.hpp"
+
+#include
+
+#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
+ Wt::Dbo::Query createQuery(Session& session, std::string_view itemToSelect, const Language::FindParameters& params)
+ {
+ session.checkReadTransaction();
+
+ auto query{ session.getDboSession()->query("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
+ Wt::Dbo::Query createQuery(Session& session, const Language::FindParameters& params)
+ {
+ std::string_view itemToSelect;
+
+ if constexpr (std::is_same_v)
+ itemToSelect = "l.id";
+ else if constexpr (std::is_same_v>)
+ itemToSelect = "l";
+ else
+ static_assert("Unhandled type");
+
+ return createQuery(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{ new Language{ name } });
+ }
+
+ std::size_t Language::getCount(Session& session)
+ {
+ session.checkReadTransaction();
+ return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM language"));
+ }
+
+ RangeResults Language::findIds(Session& session, const FindParameters& params)
+ {
+ session.checkReadTransaction();
+ auto query{ createQuery(session, params) };
+ return utils::execRangeQuery(query, params.range);
+ }
+
+ RangeResults Language::find(Session& session, const FindParameters& params)
+ {
+ session.checkReadTransaction();
+ auto query{ createQuery>(session, params) };
+ return utils::execRangeQuery(query, params.range);
+ }
+
+ void Language::find(Session& session, const FindParameters& params, std::function func)
+ {
+ session.checkReadTransaction();
+ auto query{ createQuery>(session, params) };
+ utils::forEachQueryRangeResult(query, params.range, func);
+ }
+
+ Language::pointer Language::find(Session& session, LanguageId id)
+ {
+ session.checkReadTransaction();
+ return utils::fetchQuerySingleResult(session.getDboSession()->find().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().where("name = ?").bind(name));
+ }
+
+ RangeResults Language::findOrphanIds(Session& session, std::optional range)
+ {
+ session.checkReadTransaction();
+ auto query{ session.getDboSession()->query("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(query, range);
+ }
+
+} // namespace lms::db
diff --git a/src/libs/database/impl/objects/Listen.cpp b/src/libs/database/impl/objects/Listen.cpp
index 82b472dc..9b13c0c6 100644
--- a/src/libs/database/impl/objects/Listen.cpp
+++ b/src/libs/database/impl/objects/Listen.cpp
@@ -23,6 +23,10 @@
#include
#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;
diff --git a/src/libs/database/impl/objects/MediaLibrary.cpp b/src/libs/database/impl/objects/MediaLibrary.cpp
index 73c0a60d..853658ca 100644
--- a/src/libs/database/impl/objects/MediaLibrary.cpp
+++ b/src/libs/database/impl/objects/MediaLibrary.cpp
@@ -22,6 +22,10 @@
#include
#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"
diff --git a/src/libs/database/impl/objects/Medium.cpp b/src/libs/database/impl/objects/Medium.cpp
index 80ba9714..b98fbb72 100644
--- a/src/libs/database/impl/objects/Medium.cpp
+++ b/src/libs/database/impl/objects/Medium.cpp
@@ -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"
diff --git a/src/libs/database/impl/objects/Mood.cpp b/src/libs/database/impl/objects/Mood.cpp
new file mode 100644
index 00000000..18430ba4
--- /dev/null
+++ b/src/libs/database/impl/objects/Mood.cpp
@@ -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 .
+ */
+
+#include "database/objects/Mood.hpp"
+
+#include
+
+#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
+ Wt::Dbo::Query createQuery(Session& session, std::string_view itemToSelect, const Mood::FindParameters& params)
+ {
+ session.checkReadTransaction();
+
+ auto query{ session.getDboSession()->query("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
+ Wt::Dbo::Query createQuery(Session& session, const Mood::FindParameters& params)
+ {
+ std::string_view itemToSelect;
+
+ if constexpr (std::is_same_v)
+ itemToSelect = "m.id";
+ else if constexpr (std::is_same_v>)
+ itemToSelect = "m";
+ else
+ static_assert("Unhandled type");
+
+ return createQuery(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{ new Mood{ name } });
+ }
+
+ std::size_t Mood::getCount(Session& session)
+ {
+ session.checkReadTransaction();
+ return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM mood"));
+ }
+
+ RangeResults Mood::findIds(Session& session, const FindParameters& params)
+ {
+ session.checkReadTransaction();
+ auto query{ createQuery(session, params) };
+ return utils::execRangeQuery(query, params.range);
+ }
+
+ RangeResults Mood::find(Session& session, const FindParameters& params)
+ {
+ session.checkReadTransaction();
+ auto query{ createQuery>(session, params) };
+ return utils::execRangeQuery(query, params.range);
+ }
+
+ void Mood::find(Session& session, const FindParameters& params, std::function func)
+ {
+ session.checkReadTransaction();
+ auto query{ createQuery>(session, params) };
+ utils::forEachQueryRangeResult(query, params.range, func);
+ }
+
+ Mood::pointer Mood::find(Session& session, MoodId id)
+ {
+ session.checkReadTransaction();
+ return utils::fetchQuerySingleResult(session.getDboSession()->find().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().where("name = ?").bind(name));
+ }
+
+ RangeResults Mood::findOrphanIds(Session& session, std::optional range)
+ {
+ session.checkReadTransaction();
+ auto query{ session.getDboSession()->query("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(query, range);
+ }
+
+} // namespace lms::db
diff --git a/src/libs/database/impl/objects/PlayListFile.cpp b/src/libs/database/impl/objects/PlayListFile.cpp
index 83db070e..702e6918 100644
--- a/src/libs/database/impl/objects/PlayListFile.cpp
+++ b/src/libs/database/impl/objects/PlayListFile.cpp
@@ -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"
diff --git a/src/libs/database/impl/objects/PlayQueue.cpp b/src/libs/database/impl/objects/PlayQueue.cpp
index 13364d9c..ffa824ab 100644
--- a/src/libs/database/impl/objects/PlayQueue.cpp
+++ b/src/libs/database/impl/objects/PlayQueue.cpp
@@ -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"
diff --git a/src/libs/database/impl/objects/RatedTrack.cpp b/src/libs/database/impl/objects/RatedTrack.cpp
index 8a8a1d7d..e3cc5e0f 100644
--- a/src/libs/database/impl/objects/RatedTrack.cpp
+++ b/src/libs/database/impl/objects/RatedTrack.cpp
@@ -23,6 +23,10 @@
#include
#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"
diff --git a/src/libs/database/impl/objects/Release.cpp b/src/libs/database/impl/objects/Release.cpp
index 8117f978..71c5f402 100644
--- a/src/libs/database/impl/objects/Release.cpp
+++ b/src/libs/database/impl/objects/Release.cpp
@@ -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()));
diff --git a/src/libs/database/impl/objects/ReleaseArtistLink.cpp b/src/libs/database/impl/objects/ReleaseArtistLink.cpp
index c6eeeb7a..b6616f9b 100644
--- a/src/libs/database/impl/objects/ReleaseArtistLink.cpp
+++ b/src/libs/database/impl/objects/ReleaseArtistLink.cpp
@@ -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"
diff --git a/src/libs/database/impl/objects/StarredTrack.cpp b/src/libs/database/impl/objects/StarredTrack.cpp
index 74230219..ed6ebecf 100644
--- a/src/libs/database/impl/objects/StarredTrack.cpp
+++ b/src/libs/database/impl/objects/StarredTrack.cpp
@@ -23,6 +23,10 @@
#include
#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"
diff --git a/src/libs/database/impl/objects/Track.cpp b/src/libs/database/impl/objects/Track.cpp
index ff8ad52d..028b68f9 100644
--- a/src/libs/database/impl/objects/Track.cpp
+++ b/src/libs/database/impl/objects/Track.cpp
@@ -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 Track::getGenres() const
+ {
+ return utils::fetchQueryResults(_genres.find());
+ }
+
+ std::vector Track::getGenreIds() const
+ {
+ assert(session());
+
+ const auto query{ session()->query("SELECT t_g.genre_id FROM track_genre t_g").where("t_g.track_id = ?").bind(getId()) };
+
+ return utils::fetchQueryResults(query);
+ }
+
+ std::vector Track::getGroupings() const
+ {
+ return utils::fetchQueryResults(_groupings.find());
+ }
+
+ std::vector Track::getGroupingIds() const
+ {
+ assert(session());
+
+ const auto query{ session()->query("SELECT t_gr.grouping_id FROM track_grouping t_gr").where("t_gr.track_id = ?").bind(getId()) };
+
+ return utils::fetchQueryResults(query);
+ }
+
+ std::vector Track::getLanguages() const
+ {
+ return utils::fetchQueryResults(_languages.find());
+ }
+
+ std::vector Track::getLanguageIds() const
+ {
+ assert(session());
+
+ const auto query{ session()->query("SELECT t_l.language_id FROM track_language t_l").where("t_l.track_id = ?").bind(getId()) };
+
+ return utils::fetchQueryResults(query);
+ }
+
+ std::vector Track::getMoods() const
+ {
+ return utils::fetchQueryResults(_moods.find());
+ }
+
+ std::vector Track::getMoodIds() const
+ {
+ assert(session());
+
+ const auto query{ session()->query("SELECT t_m.mood_id FROM track_mood t_m").where("t_m.track_id = ?").bind(getId()) };
+
+ return utils::fetchQueryResults(query);
+ }
+
ObjectPtr Track::getMediaLibrary() const
{
return _mediaLibrary;
@@ -566,6 +654,34 @@ namespace lms::db
_clusters.insert(getDboPtr(cluster));
}
+ void Track::setGenres(std::span> genres)
+ {
+ _genres.clear();
+ for (const ObjectPtr& genre : genres)
+ _genres.insert(getDboPtr(genre));
+ }
+
+ void Track::setGroupings(std::span> groupings)
+ {
+ _groupings.clear();
+ for (const ObjectPtr& grouping : groupings)
+ _groupings.insert(getDboPtr(grouping));
+ }
+
+ void Track::setLanguages(std::span> languages)
+ {
+ _languages.clear();
+ for (const ObjectPtr& language : languages)
+ _languages.insert(getDboPtr(language));
+ }
+
+ void Track::setMoods(std::span> moods)
+ {
+ _moods.clear();
+ for (const ObjectPtr& mood : moods)
+ _moods.insert(getDboPtr(mood));
+ }
+
void Track::clearLyrics()
{
_trackLyrics.clear();
diff --git a/src/libs/database/impl/objects/TrackArtistLink.cpp b/src/libs/database/impl/objects/TrackArtistLink.cpp
index 8ad07317..e4e1a238 100644
--- a/src/libs/database/impl/objects/TrackArtistLink.cpp
+++ b/src/libs/database/impl/objects/TrackArtistLink.cpp
@@ -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"
diff --git a/src/libs/database/impl/objects/TrackBookmark.cpp b/src/libs/database/impl/objects/TrackBookmark.cpp
index aa358bd7..7da5f4fc 100644
--- a/src/libs/database/impl/objects/TrackBookmark.cpp
+++ b/src/libs/database/impl/objects/TrackBookmark.cpp
@@ -22,6 +22,10 @@
#include
#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"
diff --git a/src/libs/database/impl/objects/TrackEmbeddedImage.cpp b/src/libs/database/impl/objects/TrackEmbeddedImage.cpp
index 507ca4bb..5a75f034 100644
--- a/src/libs/database/impl/objects/TrackEmbeddedImage.cpp
+++ b/src/libs/database/impl/objects/TrackEmbeddedImage.cpp
@@ -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"
diff --git a/src/libs/database/impl/objects/TrackEmbeddedImageLink.cpp b/src/libs/database/impl/objects/TrackEmbeddedImageLink.cpp
index 4cd05a08..92d6fdd2 100644
--- a/src/libs/database/impl/objects/TrackEmbeddedImageLink.cpp
+++ b/src/libs/database/impl/objects/TrackEmbeddedImageLink.cpp
@@ -23,6 +23,10 @@
#include
#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"
diff --git a/src/libs/database/impl/objects/TrackList.cpp b/src/libs/database/impl/objects/TrackList.cpp
index 2f35e473..bfdf6b5a 100644
--- a/src/libs/database/impl/objects/TrackList.cpp
+++ b/src/libs/database/impl/objects/TrackList.cpp
@@ -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"
diff --git a/src/libs/database/impl/objects/TrackLyrics.cpp b/src/libs/database/impl/objects/TrackLyrics.cpp
index e580efbe..666492a2 100644
--- a/src/libs/database/impl/objects/TrackLyrics.cpp
+++ b/src/libs/database/impl/objects/TrackLyrics.cpp
@@ -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"
diff --git a/src/libs/database/impl/objects/TrackMusicNNEmbeddings.cpp b/src/libs/database/impl/objects/TrackMusicNNEmbeddings.cpp
index 4ea93deb..214afc43 100644
--- a/src/libs/database/impl/objects/TrackMusicNNEmbeddings.cpp
+++ b/src/libs/database/impl/objects/TrackMusicNNEmbeddings.cpp
@@ -22,6 +22,10 @@
#include
#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"
diff --git a/src/libs/database/impl/objects/User.cpp b/src/libs/database/impl/objects/User.cpp
index 1bc03388..1de617f3 100644
--- a/src/libs/database/impl/objects/User.cpp
+++ b/src/libs/database/impl/objects/User.cpp
@@ -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"
diff --git a/src/libs/database/include/database/objects/Cluster.hpp b/src/libs/database/include/database/objects/Cluster.hpp
index 01a60648..6a53e9eb 100644
--- a/src/libs/database/include/database/objects/Cluster.hpp
+++ b/src/libs/database/include/database/objects/Cluster.hpp
@@ -104,21 +104,13 @@ namespace lms::db
// Accessors
std::string_view getName() const { return _name; }
ObjectPtr getType() const { return _clusterType; }
- std::size_t getTrackCount() const { return _trackCount; }
RangeResults getTracks(std::optional 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