Extracted genre, mood, language and grouping from generic clusters

This commit is contained in:
emeric
2026-06-17 21:34:29 +02:00
parent 77f9b033ed
commit dbd6e57160
82 changed files with 3648 additions and 275 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ _LMS_ provides several ways to help you find the music you like:
* ...
__Note__: the recommendation engine supports two modes:
* **Tag-based**: uses metadata tags `genre` and `grouping`.
* **Tag-based**: uses metadata tags `genre`, `mood`, `grouping`, and `language`.
* **Audio similarity**: uses MusicNN embeddings. Extraction throughput depends heavily on both the number of scanner threads and the speed of your storage. Typical reported speeds range from ~1k tracks/hour on a Raspberry Pi 4 (3 threads) to ~25k tracks/hour on an Intel Core i5-13500 (10 threads). To speed up extraction, consider tweaking `scanner-thread-count` in `lms.conf` (defaults to half the number of logical CPUs).
## About tags
+17 -1
View File
@@ -152,7 +152,7 @@
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Checking for duplicate files... {1} files</message>
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Checking for removed files... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Compacting database...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Computing stats... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compute-genre-stats">Computing stats... {1}%</message>
<message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">Extracting MusicNN embeddings: {1} of {2} files ({3}%)</message>
<message id="Lms.Admin.ScannerController.step-optimize">Optimizing database... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliating artists: {1} entries...</message>
@@ -221,7 +221,19 @@
<message id="Lms.Explore.duration">Duration</message>
<message id="Lms.Explore.filter-added">Filter added</message>
<message id="Lms.Explore.filters">Filters</message>
<message id="Lms.Explore.genre">
<plural case="0">Genre</plural>
<plural case="1">Genres</plural>
</message>
<message id="Lms.Explore.grouping">
<plural case="0">Grouping</plural>
<plural case="1">Groupings</plural>
</message>
<message id="Lms.Explore.label">Label</message>
<message id="Lms.Explore.language">
<plural case="0">Language</plural>
<plural case="1">Languages</plural>
</message>
<message id="Lms.Explore.lyricist">
<plural case="0">Lyricist</plural>
<plural case="1">Lyricists</plural>
@@ -231,6 +243,10 @@
<plural case="0">Mixer</plural>
<plural case="1">Mixers</plural>
</message>
<message id="Lms.Explore.mood">
<plural case="0">Mood</plural>
<plural case="1">Moods</plural>
</message>
<message id="Lms.Explore.most-played">Most played</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message>
<message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</message>
+17 -1
View File
@@ -152,7 +152,7 @@
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Comprobando ficheros duplicados ... {1} ficheros</message>
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Comprobando ficheros eliminados... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Compactando la base de datos...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calculando estadísticas... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compute-genre-stats">Calculando estadísticas... {1}%</message>
<message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">Extracción de embeddings de MusicNN: {1} de {2} archivos ({3}%)</message>
<message id="Lms.Admin.ScannerController.step-optimize">Optimizando la base de datos... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliando artistas: {1} entradas...</message>
@@ -221,7 +221,19 @@
<message id="Lms.Explore.duration">Duración</message>
<message id="Lms.Explore.filter-added">Filtro añadido</message>
<message id="Lms.Explore.filters">Filtros</message>
<message id="Lms.Explore.genre">
<plural case="0">Género</plural>
<plural case="1">Géneros</plural>
</message>
<message id="Lms.Explore.grouping">
<plural case="0">Agrupación</plural>
<plural case="1">Agrupaciones</plural>
</message>
<message id="Lms.Explore.label">Sello discográfico</message>
<message id="Lms.Explore.language">
<plural case="0">Idioma</plural>
<plural case="1">Idiomas</plural>
</message>
<message id="Lms.Explore.lyricist">
<plural case="0">Letrista</plural>
<plural case="1">Letristas</plural>
@@ -231,6 +243,10 @@
<plural case="0">Mezclador</plural>
<plural case="1">Mezcladores</plural>
</message>
<message id="Lms.Explore.mood">
<plural case="0">Estado de ánimo</plural>
<plural case="1">Estados de ánimo</plural>
</message>
<message id="Lms.Explore.most-played">Reproducidos frecuentemente</message>
<message id="Lms.Explore.musicbrainz-artist">Artista de MusicBrainz</message>
<message id="Lms.Explore.musicbrainz-release">Álbum de MusicBrainz</message>
+17 -1
View File
@@ -152,7 +152,7 @@
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Vérification des fichiers dupliqués... {1} fichiers</message>
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Vérification des fichiers supprimés... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Compactage de la base de données...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcul des statistiques... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compute-genre-stats">Calcul des statistiques... {1}%</message>
<message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">Extraction des embeddings MusicNN : {1} sur {2} fichiers ({3}%)</message>
<message id="Lms.Admin.ScannerController.step-optimize">Optimisation de la base de données... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliation des artistes: {1} entrées...</message>
@@ -221,7 +221,19 @@
<message id="Lms.Explore.duration">Durée</message>
<message id="Lms.Explore.filter-added">Filtre ajouté</message>
<message id="Lms.Explore.filters">Filtres</message>
<message id="Lms.Explore.genre">
<plural case="0">Genre</plural>
<plural case="1">Genres</plural>
</message>
<message id="Lms.Explore.grouping">
<plural case="0">Regroupement</plural>
<plural case="1">Regroupements</plural>
</message>
<message id="Lms.Explore.label">Label</message>
<message id="Lms.Explore.language">
<plural case="0">Langue</plural>
<plural case="1">Langues</plural>
</message>
<message id="Lms.Explore.lyricist">
<plural case="0">Parolier</plural>
<plural case="1">Paroliers</plural>
@@ -231,6 +243,10 @@
<plural case="0">Mixeur</plural>
<plural case="1">Mixeurs</plural>
</message>
<message id="Lms.Explore.mood">
<plural case="0">Ambiance</plural>
<plural case="1">Ambiances</plural>
</message>
<message id="Lms.Explore.most-played">Plus joués</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message>
<message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</message>
+17 -1
View File
@@ -152,7 +152,7 @@
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Controllo duplicati... {1} files</message>
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Controllo file... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Compattazione del database...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcolo statistiche... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compute-genre-stats">Calcolo statistiche... {1}%</message>
<message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">Estrazione degli embeddings MusicNN: {1} di {2} file ({3}%)</message>
<message id="Lms.Admin.ScannerController.step-optimize">Ottimizzazione del database... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Riconciliazione artisti: {1} voci...</message>
@@ -221,7 +221,19 @@
<message id="Lms.Explore.duration">Durata</message>
<message id="Lms.Explore.filter-added">Filtro aggiunto</message>
<message id="Lms.Explore.filters">Filtri</message>
<message id="Lms.Explore.genre">
<plural case="0">Genere</plural>
<plural case="1">Generi</plural>
</message>
<message id="Lms.Explore.grouping">
<plural case="0">Raggruppamento</plural>
<plural case="1">Raggruppamenti</plural>
</message>
<message id="Lms.Explore.label">Etichetta</message>
<message id="Lms.Explore.language">
<plural case="0">Lingua</plural>
<plural case="1">Lingue</plural>
</message>
<message id="Lms.Explore.lyricist">
<plural case="0">Paroliere</plural>
<plural case="1">Parolieri</plural>
@@ -231,6 +243,10 @@
<plural case="0">Tecnico del mixaggio</plural>
<plural case="1">Tecnici del mixaggio</plural>
</message>
<message id="Lms.Explore.mood">
<plural case="0">Umore</plural>
<plural case="1">Umori</plural>
</message>
<message id="Lms.Explore.most-played">Più ascoltati</message>
<message id="Lms.Explore.musicbrainz-artist">Artista MusicBrainz</message>
<message id="Lms.Explore.musicbrainz-release">Album MusicBrainz</message>
+21 -1
View File
@@ -165,7 +165,7 @@
</message>
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Sprawdzanie plików... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Prasowanie bazy danych...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Obliczanie statystyk... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compute-genre-stats">Obliczanie statystyk... {1}%</message>
<message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">Ekstrakcja embeddingów MusicNN: {1} z {2} plików ({3}%)</message>
<message id="Lms.Admin.ScannerController.step-optimize">Optymalizowanie bazy danych... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Uzgodnianie artystów: {1} wpisów...</message>
@@ -240,7 +240,22 @@
<message id="Lms.Explore.duration">Długość</message>
<message id="Lms.Explore.filter-added">Filtr dodany</message>
<message id="Lms.Explore.filters">Filtry</message>
<message id="Lms.Explore.genre">
<plural case="0">Gatunek</plural>
<plural case="1">Gatunki</plural>
<plural case="2">Gatunków</plural>
</message>
<message id="Lms.Explore.grouping">
<plural case="0">Grupowanie</plural>
<plural case="1">Grupowania</plural>
<plural case="2">Grupowań</plural>
</message>
<message id="Lms.Explore.label">Wytwórnia</message>
<message id="Lms.Explore.language">
<plural case="0">Język</plural>
<plural case="1">Języki</plural>
<plural case="2">Języków</plural>
</message>
<message id="Lms.Explore.lyricist">
<plural case="0">Tekściarz</plural>
<plural case="1">Tekściarze</plural>
@@ -252,6 +267,11 @@
<plural case="1">Mikserzy</plural>
<plural case="2">Mikserów</plural>
</message>
<message id="Lms.Explore.mood">
<plural case="0">Nastrój</plural>
<plural case="1">Nastroje</plural>
<plural case="2">Nastrojów</plural>
</message>
<message id="Lms.Explore.most-played">Najczęściej odtwarzane</message>
<message id="Lms.Explore.musicbrainz-artist">Artysta w MusicBrainz</message>
<message id="Lms.Explore.musicbrainz-release">Wydanie w MusicBrainz</message>
+5 -1
View File
@@ -150,7 +150,7 @@
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">检查重复文件... {1} 个文件</message>
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">检查文件中... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">正在压缩数据库...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">正在计算统计信息... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compute-genre-stats">正在计算统计信息... {1}%</message>
<message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">正在提取 MusicNN 嵌入:{1} / {2} 个文件({3}%</message>
<message id="Lms.Admin.ScannerController.step-optimize">正在优化数据库... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">整理艺术家:{1} 条记录...</message>
@@ -213,10 +213,14 @@
<message id="Lms.Explore.duration">时长</message>
<message id="Lms.Explore.filter-added">过滤器已添加</message>
<message id="Lms.Explore.filters">过滤器</message>
<message id="Lms.Explore.genre">流派</message>
<message id="Lms.Explore.grouping">分组</message>
<message id="Lms.Explore.label">厂牌</message>
<message id="Lms.Explore.language">语言</message>
<message id="Lms.Explore.lyricist">作词者</message>
<message id="Lms.Explore.media-library">音乐库</message>
<message id="Lms.Explore.mixer">混音师</message>
<message id="Lms.Explore.mood">情绪</message>
<message id="Lms.Explore.most-played">播放最多</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz 歌手</message>
<message id="Lms.Explore.musicbrainz-release">MusicBrainz 专辑</message>
+40
View File
@@ -80,6 +80,46 @@
</div>
<div class="modal-body">
<div class="container-fluid d-grid gap-2">
${<if-has-genres>}
<div class="row align-items-center">
<div class="col-4">
${genres-label}
</div>
<div class="col">
${genres}
</div>
</div>
${</if-has-genres>}
${<if-has-groupings>}
<div class="row align-items-center">
<div class="col-4">
${groupings-label}
</div>
<div class="col">
${groupings}
</div>
</div>
${</if-has-groupings>}
${<if-has-languages>}
<div class="row align-items-center">
<div class="col-4">
${languages-label}
</div>
<div class="col">
${languages}
</div>
</div>
${</if-has-languages>}
${<if-has-moods>}
<div class="row align-items-center">
<div class="col-4">
${moods-label}
</div>
<div class="col">
${moods}
</div>
</div>
${</if-has-moods>}
${<if-has-clusters>}
<div class="row align-items-center">
<div class="col-4">
+4
View File
@@ -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
+123 -1
View File
@@ -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{};
+13
View File
@@ -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)");
+36
View File
@@ -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"
+187
View File
@@ -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
+175
View File
@@ -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
+175
View File
@@ -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
+81 -1
View File
@@ -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"
+175
View File
@@ -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"
+116
View File
@@ -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"
+4
View File
@@ -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,
+4
View File
@@ -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
-2
View File
@@ -59,8 +59,6 @@ namespace lms::db::tests
void DatabaseFixture::testDatabaseEmpty()
{
using namespace db;
auto transaction{ session.createReadTransaction() };
EXPECT_TRUE(session.areAllTablesEmpty());
+4
View File
@@ -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"
+233
View File
@@ -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
+190
View File
@@ -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
+190
View File
@@ -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
+190
View File
@@ -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
+33
View File
@@ -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:
@@ -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 {};
@@ -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
+1 -1
View File
@@ -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);
@@ -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
@@ -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;
}
+27 -24
View File
@@ -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()));
});
}
+9 -9
View File
@@ -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
+2 -2
View File
@@ -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
+13 -24
View File
@@ -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");
+48
View File
@@ -30,7 +30,11 @@
#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/Image.hpp"
#include "database/objects/Language.hpp"
#include "database/objects/Mood.hpp"
#include "database/objects/Release.hpp"
#include "database/objects/ReleaseArtistLink.hpp"
#include "database/objects/ScanSettings.hpp"
@@ -175,6 +179,50 @@ namespace lms::ui::utils
return createFilter(Wt::WString::fromUTF8(std::string{ cluster->getName() }), Wt::WString::fromUTF8(std::string{ cluster->getType()->getName() }), getStyleClass(cluster), canDelete);
}
std::unique_ptr<Wt::WInteractWidget> createFilterGenre(db::GenreId genreId, bool canDelete)
{
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
const db::Genre::pointer genre{ db::Genre::find(LmsApp->getDbSession(), genreId) };
if (!genre)
return {};
return createFilter(Wt::WString::fromUTF8(std::string{ genre->getName() }), Wt::WString::trn("Lms.Explore.genre", 1), "bg-info text-dark", canDelete);
}
std::unique_ptr<Wt::WInteractWidget> createFilterGrouping(db::GroupingId groupingId, bool canDelete)
{
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
const db::Grouping::pointer grouping{ db::Grouping::find(LmsApp->getDbSession(), groupingId) };
if (!grouping)
return {};
return createFilter(Wt::WString::fromUTF8(std::string{ grouping->getName() }), Wt::WString::trn("Lms.Explore.grouping", 1), "bg-secondary", canDelete);
}
std::unique_ptr<Wt::WInteractWidget> createFilterLanguage(db::LanguageId languageId, bool canDelete)
{
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
const db::Language::pointer language{ db::Language::find(LmsApp->getDbSession(), languageId) };
if (!language)
return {};
return createFilter(Wt::WString::fromUTF8(std::string{ language->getName() }), Wt::WString::trn("Lms.Explore.language", 1), "bg-primary", canDelete);
}
std::unique_ptr<Wt::WInteractWidget> createFilterMood(db::MoodId moodId, bool canDelete)
{
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
const db::Mood::pointer mood{ db::Mood::find(LmsApp->getDbSession(), moodId) };
if (!mood)
return {};
return createFilter(Wt::WString::fromUTF8(std::string{ mood->getName() }), Wt::WString::trn("Lms.Explore.mood", 1), "bg-warning text-dark", canDelete);
}
std::unique_ptr<Wt::WContainerWidget> createFilterClustersForTrack(db::Track::pointer track, Filters& filters)
{
using namespace db;
+8
View File
@@ -34,6 +34,10 @@
#include "database/Object.hpp"
#include "database/objects/ClusterId.hpp"
#include "database/objects/GenreId.hpp"
#include "database/objects/GroupingId.hpp"
#include "database/objects/LanguageId.hpp"
#include "database/objects/MoodId.hpp"
#include "database/objects/Types.hpp"
#include "resource/ArtworkResource.hpp"
@@ -63,6 +67,10 @@ namespace lms::ui::utils
std::unique_ptr<Wt::WInteractWidget> createFilter(const Wt::WString& name, const Wt::WString& tooltip, std::string_view colorStyleClass, bool canDelete = false);
std::unique_ptr<Wt::WInteractWidget> createFilterCluster(db::ClusterId clusterId, bool canDelete = false);
std::unique_ptr<Wt::WInteractWidget> createFilterGenre(db::GenreId genreId, bool canDelete = false);
std::unique_ptr<Wt::WInteractWidget> createFilterGrouping(db::GroupingId groupingId, bool canDelete = false);
std::unique_ptr<Wt::WInteractWidget> createFilterLanguage(db::LanguageId languageId, bool canDelete = false);
std::unique_ptr<Wt::WInteractWidget> createFilterMood(db::MoodId moodId, bool canDelete = false);
std::unique_ptr<Wt::WContainerWidget> createFilterClustersForTrack(db::ObjectPtr<db::Track> track, Filters& filters);
std::unique_ptr<Wt::WAnchor> createArtistAnchor(db::ObjectPtr<db::Artist> artist, bool setText = true);
+2 -2
View File
@@ -244,8 +244,8 @@ namespace lms::ui
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-compact"));
break;
case ScanStep::ComputeClusterStats:
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-compute-cluster-stats")
case ScanStep::ComputeGenreStats:
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-compute-genre-stats")
.arg(stepStats.progress()));
break;
+38
View File
@@ -31,6 +31,10 @@
#include "database/objects/ArtistInfo.hpp"
#include "database/objects/ArtworkId.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/ScanSettings.hpp"
#include "database/objects/Track.hpp"
@@ -56,6 +60,8 @@ namespace lms::ui
{
namespace
{
constexpr std::size_t maxTagCloudItems{ 3 };
std::optional<db::ArtistId> extractArtistIdFromInternalPath()
{
if (wApp->internalPathMatches("/artist/mbid/"))
@@ -137,6 +143,38 @@ namespace lms::ui
Wt::WContainerWidget* clusterContainers{ bindNew<Wt::WContainerWidget>("clusters") };
{
db::Genre::find(LmsApp->getDbSession(), db::Genre::FindParameters{}.setArtist(_artistId).setSortMethod(db::GenreSortMethod::TrackCountDesc).setRange(db::Range{ 0, maxTagCloudItems }), [&](const db::Genre::pointer& genre) {
const db::GenreId genreId{ genre->getId() };
Wt::WInteractWidget* entry{ clusterContainers->addWidget(utils::createFilterGenre(genreId)) };
entry->clicked().connect([this, genreId] {
_filters.set(genreId);
});
});
db::Grouping::find(LmsApp->getDbSession(), db::Grouping::FindParameters{}.setArtist(_artistId).setSortMethod(db::GroupingSortMethod::TrackCountDesc).setRange(db::Range{ 0, maxTagCloudItems }), [&](const db::Grouping::pointer& grouping) {
const db::GroupingId groupingId{ grouping->getId() };
Wt::WInteractWidget* entry{ clusterContainers->addWidget(utils::createFilterGrouping(groupingId)) };
entry->clicked().connect([this, groupingId] {
_filters.set(groupingId);
});
});
db::Language::find(LmsApp->getDbSession(), db::Language::FindParameters{}.setArtist(_artistId).setSortMethod(db::LanguageSortMethod::TrackCountDesc).setRange(db::Range{ 0, maxTagCloudItems }), [&](const db::Language::pointer& language) {
const db::LanguageId languageId{ language->getId() };
Wt::WInteractWidget* entry{ clusterContainers->addWidget(utils::createFilterLanguage(languageId)) };
entry->clicked().connect([this, languageId] {
_filters.set(languageId);
});
});
db::Mood::find(LmsApp->getDbSession(), db::Mood::FindParameters{}.setArtist(_artistId).setSortMethod(db::MoodSortMethod::TrackCountDesc).setRange(db::Range{ 0, maxTagCloudItems }), [&](const db::Mood::pointer& mood) {
const db::MoodId moodId{ mood->getId() };
Wt::WInteractWidget* entry{ clusterContainers->addWidget(utils::createFilterMood(moodId)) };
entry->clicked().connect([this, moodId] {
_filters.set(moodId);
});
});
auto clusterTypes{ db::ClusterType::findIds(LmsApp->getDbSession()).results };
auto clusterGroups{ artist->getClusterGroups(clusterTypes, 3) };
+211 -6
View File
@@ -31,8 +31,12 @@
#include "core/media/Codec.hpp"
#include "database/Session.hpp"
#include "database/objects/Cluster.hpp"
#include "database/objects/Genre.hpp"
#include "database/objects/Grouping.hpp"
#include "database/objects/LabelId.hpp"
#include "database/objects/Language.hpp"
#include "database/objects/MediaLibrary.hpp"
#include "database/objects/Mood.hpp"
#include "database/objects/Release.hpp"
#include "database/objects/ReleaseTypeId.hpp"
@@ -62,13 +66,34 @@ namespace lms::ui
{
};
using TypeVariant = std::variant<db::ClusterTypeId, MediaLibraryTag, LabelTag, ReleaseTypeTag, CodecTag>;
struct GenreTag
{
};
struct GroupingTag
{
};
struct LanguageTag
{
};
struct MoodTag
{
};
using TypeVariant = std::variant<db::ClusterTypeId, MediaLibraryTag, LabelTag, ReleaseTypeTag, CodecTag, GenreTag, GroupingTag, LanguageTag, MoodTag>;
using TypeModel = ValueStringModel<TypeVariant>;
std::unique_ptr<TypeModel> createTypeModel()
{
auto typeModel{ std::make_unique<TypeModel>() };
typeModel->add(Wt::WString::trn("Lms.Explore.genre", 1), GenreTag{});
typeModel->add(Wt::WString::trn("Lms.Explore.grouping", 1), GroupingTag{});
typeModel->add(Wt::WString::trn("Lms.Explore.language", 1), LanguageTag{});
typeModel->add(Wt::WString::trn("Lms.Explore.mood", 1), MoodTag{});
{
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
db::ClusterType::find(LmsApp->getDbSession(), [&](const db::ClusterType::pointer& clusterType) {
@@ -84,7 +109,7 @@ namespace lms::ui
return typeModel;
}
using ValueVariant = std::variant<db::ClusterId, db::MediaLibraryId, db::LabelId, db::ReleaseTypeId, core::media::Codec>;
using ValueVariant = std::variant<db::ClusterId, db::MediaLibraryId, db::LabelId, db::ReleaseTypeId, core::media::Codec, db::GenreId, db::GroupingId, db::LanguageId, db::MoodId>;
using ValueModel = ValueStringModel<ValueVariant>;
std::unique_ptr<ValueModel> createValueModel(TypeVariant type)
@@ -116,6 +141,26 @@ namespace lms::ui
valueModel->add(Wt::WString::fromUTF8(desc.longName.c_str()), desc.type);
});
},
[&](const GenreTag&) {
db::Genre::find(session, db::Genre::FindParameters{}.setSortMethod(db::GenreSortMethod::Name), [&](const db::Genre::pointer& genre) {
valueModel->add(Wt::WString::fromUTF8(std::string{ genre->getName() }), genre->getId());
});
},
[&](const GroupingTag&) {
db::Grouping::find(session, db::Grouping::FindParameters{}.setSortMethod(db::GroupingSortMethod::Name), [&](const db::Grouping::pointer& grouping) {
valueModel->add(Wt::WString::fromUTF8(std::string{ grouping->getName() }), grouping->getId());
});
},
[&](const LanguageTag&) {
db::Language::find(session, db::Language::FindParameters{}.setSortMethod(db::LanguageSortMethod::Name), [&](const db::Language::pointer& language) {
valueModel->add(Wt::WString::fromUTF8(std::string{ language->getName() }), language->getId());
});
},
[&](const MoodTag&) {
db::Mood::find(session, db::Mood::FindParameters{}.setSortMethod(db::MoodSortMethod::Name), [&](const db::Mood::pointer& mood) {
valueModel->add(Wt::WString::fromUTF8(std::string{ mood->getName() }), mood->getId());
});
},
[&](db::ClusterTypeId clusterTypeId) {
db::Cluster::FindParameters params;
params.setClusterType(clusterTypeId);
@@ -151,22 +196,30 @@ namespace lms::ui
std::visit(core::utils::overloads{
[&](db::MediaLibraryId mediaLibraryId) {
set(mediaLibraryId);
state::writeValue<db::MediaLibraryId::ValueType>("filters_media_library_id", mediaLibraryId.getValue());
},
[&](db::LabelId labelId) {
set(labelId);
state::writeValue<db::LabelId::ValueType>("filters_label_id", labelId.getValue());
},
[&](db::ReleaseTypeId releaseTypeId) {
set(releaseTypeId);
state::writeValue<db::ReleaseTypeId::ValueType>("filters_release_type_id", releaseTypeId.getValue());
},
[&](db::ClusterId clusterId) {
add(clusterId);
},
[&](core::media::Codec codec) {
set(codec);
state::writeValue<core::media::Codec>("filters_codec", codec);
},
[&](db::GenreId genreId) {
set(genreId);
},
[&](db::GroupingId groupingId) {
set(groupingId);
},
[&](db::LanguageId languageId) {
set(languageId);
},
[&](db::MoodId moodId) {
set(moodId);
},
},
value);
@@ -212,6 +265,14 @@ namespace lms::ui
set(db::ReleaseTypeId{ *releaseTypeId });
if (const std::optional<core::media::Codec> codec{ state::readValue<core::media::Codec>("filters_codec") })
set(*codec);
if (const std::optional<db::GenreId::ValueType> genreId{ state::readValue<db::GenreId::ValueType>("filters_genre_id") })
set(db::GenreId{ *genreId });
if (const std::optional<db::GroupingId::ValueType> groupingId{ state::readValue<db::GroupingId::ValueType>("filters_grouping_id") })
set(db::GroupingId{ *groupingId });
if (const std::optional<db::LanguageId::ValueType> languageId{ state::readValue<db::LanguageId::ValueType>("filters_language_id") })
set(db::LanguageId{ *languageId });
if (const std::optional<db::MoodId::ValueType> moodId{ state::readValue<db::MoodId::ValueType>("filters_mood_id") })
set(db::MoodId{ *moodId });
}
void Filters::add(db::ClusterId clusterId)
@@ -240,6 +301,142 @@ namespace lms::ui
emitFilterAddedNotification();
}
void Filters::set(db::GenreId genreId)
{
if (_dbFilters.genre == genreId)
return;
if (_genreFilter)
{
_filters->removeWidget(_genreFilter);
_genreFilter = nullptr;
_dbFilters.genre = db::GenreId{};
}
auto genre{ utils::createFilterGenre(genreId, true) };
if (!genre)
{
_sigUpdated.emit();
return;
}
_dbFilters.genre = genreId;
state::writeValue<db::GenreId::ValueType>("filters_genre_id", genreId.getValue());
_genreFilter = _filters->addWidget(std::move(genre));
_genreFilter->clicked().connect([this] {
_filters->removeWidget(_genreFilter);
_genreFilter = nullptr;
_dbFilters.genre = db::GenreId{};
state::writeValue<db::GenreId::ValueType>("filters_genre_id", std::nullopt);
_sigUpdated.emit();
});
emitFilterAddedNotification();
}
void Filters::set(db::GroupingId groupingId)
{
if (_dbFilters.grouping == groupingId)
return;
if (_groupingFilter)
{
_filters->removeWidget(_groupingFilter);
_groupingFilter = nullptr;
_dbFilters.grouping = db::GroupingId{};
}
auto grouping{ utils::createFilterGrouping(groupingId, true) };
if (!grouping)
{
_sigUpdated.emit();
return;
}
_dbFilters.grouping = groupingId;
state::writeValue<db::GroupingId::ValueType>("filters_grouping_id", groupingId.getValue());
_groupingFilter = _filters->addWidget(std::move(grouping));
_groupingFilter->clicked().connect([this] {
_filters->removeWidget(_groupingFilter);
_groupingFilter = nullptr;
_dbFilters.grouping = db::GroupingId{};
state::writeValue<db::GroupingId::ValueType>("filters_grouping_id", std::nullopt);
_sigUpdated.emit();
});
emitFilterAddedNotification();
}
void Filters::set(db::LanguageId languageId)
{
if (_dbFilters.language == languageId)
return;
if (_languageFilter)
{
_filters->removeWidget(_languageFilter);
_languageFilter = nullptr;
_dbFilters.language = db::LanguageId{};
}
auto language{ utils::createFilterLanguage(languageId, true) };
if (!language)
{
_sigUpdated.emit();
return;
}
_dbFilters.language = languageId;
state::writeValue<db::LanguageId::ValueType>("filters_language_id", languageId.getValue());
_languageFilter = _filters->addWidget(std::move(language));
_languageFilter->clicked().connect([this] {
_filters->removeWidget(_languageFilter);
_languageFilter = nullptr;
_dbFilters.language = db::LanguageId{};
state::writeValue<db::LanguageId::ValueType>("filters_language_id", std::nullopt);
_sigUpdated.emit();
});
emitFilterAddedNotification();
}
void Filters::set(db::MoodId moodId)
{
if (_dbFilters.mood == moodId)
return;
if (_moodFilter)
{
_filters->removeWidget(_moodFilter);
_moodFilter = nullptr;
_dbFilters.mood = db::MoodId{};
}
auto mood{ utils::createFilterMood(moodId, true) };
if (!mood)
{
_sigUpdated.emit();
return;
}
_dbFilters.mood = moodId;
state::writeValue<db::MoodId::ValueType>("filters_mood_id", moodId.getValue());
_moodFilter = _filters->addWidget(std::move(mood));
_moodFilter->clicked().connect([this] {
_filters->removeWidget(_moodFilter);
_moodFilter = nullptr;
_dbFilters.mood = db::MoodId{};
state::writeValue<db::MoodId::ValueType>("filters_mood_id", std::nullopt);
_sigUpdated.emit();
});
emitFilterAddedNotification();
}
void Filters::set(db::MediaLibraryId mediaLibraryId)
{
if (_mediaLibraryFilter)
@@ -261,6 +458,8 @@ namespace lms::ui
}
_dbFilters.mediaLibrary = mediaLibraryId;
state::writeValue<db::MediaLibraryId::ValueType>("filters_media_library_id", mediaLibraryId.getValue());
_mediaLibraryFilter = _filters->addWidget(utils::createFilter(Wt::WString::fromUTF8(libraryName), Wt::WString::tr("Lms.Explore.media-library"), "bg-primary", true));
_mediaLibraryFilter->clicked().connect(_mediaLibraryFilter, [this] {
_filters->removeWidget(_mediaLibraryFilter);
@@ -294,6 +493,8 @@ namespace lms::ui
}
_dbFilters.label = labelId;
state::writeValue<db::LabelId::ValueType>("filters_label_id", labelId.getValue());
_labelFilter = _filters->addWidget(utils::createFilter(Wt::WString::fromUTF8(name), Wt::WString::tr("Lms.Explore.label"), "bg-secondary", true));
_labelFilter->clicked().connect(_labelFilter, [this] {
_filters->removeWidget(_labelFilter);
@@ -327,6 +528,8 @@ namespace lms::ui
}
_dbFilters.releaseType = releaseTypeId;
state::writeValue<db::ReleaseTypeId::ValueType>("filters_release_type_id", releaseTypeId.getValue());
_releaseTypeFilter = _filters->addWidget(utils::createFilter(Wt::WString::fromUTF8(name), Wt::WString::tr("Lms.Explore.release-type"), "bg-dark", true));
_releaseTypeFilter->clicked().connect(_releaseTypeFilter, [this] {
_filters->removeWidget(_releaseTypeFilter);
@@ -349,6 +552,8 @@ namespace lms::ui
}
_dbFilters.codec = codec;
state::writeValue<core::media::Codec>("filters_codec", codec);
_codecFilter = _filters->addWidget(utils::createFilter(Wt::WString::fromUTF8(core::media::getCodecDesc(codec).longName.c_str()), Wt::WString::tr("Lms.Explore.codec"), "bg-success", true));
_codecFilter->clicked().connect(_codecFilter, [this] {
_filters->removeWidget(_codecFilter);
+12
View File
@@ -26,6 +26,10 @@
#include "core/media/Codec.hpp"
#include "database/objects/Filters.hpp"
#include "database/objects/GenreId.hpp"
#include "database/objects/GroupingId.hpp"
#include "database/objects/LanguageId.hpp"
#include "database/objects/MoodId.hpp"
namespace lms::ui
{
@@ -37,6 +41,10 @@ namespace lms::ui
const db::Filters& getDbFilters() const { return _dbFilters; }
void add(db::ClusterId clusterId);
void set(db::GenreId genreId);
void set(db::GroupingId groupingId);
void set(db::LanguageId languageId);
void set(db::MoodId moodId);
Wt::Signal<>& updated() { return _sigUpdated; }
@@ -53,6 +61,10 @@ namespace lms::ui
Wt::WInteractWidget* _labelFilter{};
Wt::WInteractWidget* _releaseTypeFilter{};
Wt::WInteractWidget* _codecFilter{};
Wt::WInteractWidget* _genreFilter{};
Wt::WInteractWidget* _groupingFilter{};
Wt::WInteractWidget* _languageFilter{};
Wt::WInteractWidget* _moodFilter{};
Wt::Signal<> _sigUpdated;
+39 -1
View File
@@ -35,7 +35,11 @@
#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/Medium.hpp"
#include "database/objects/Mood.hpp"
#include "database/objects/Release.hpp"
#include "database/objects/ScanSettings.hpp"
#include "database/objects/Track.hpp"
@@ -273,10 +277,44 @@ namespace lms::ui
refreshReleaseArtists(release);
refreshArtwork(release->getPreferredArtworkId());
constexpr std::size_t maxTagCloudItemCount{ 3 };
Wt::WContainerWidget* clusterContainers{ bindNew<Wt::WContainerWidget>("clusters") };
{
db::Genre::find(session, db::Genre::FindParameters{}.setRelease(release->getId()).setSortMethod(db::GenreSortMethod::TrackCountDesc).setRange(db::Range{ 0, maxTagCloudItemCount }), [&](const db::Genre::pointer& genre) {
const db::GenreId genreId{ genre->getId() };
Wt::WInteractWidget* entry{ clusterContainers->addWidget(utils::createFilterGenre(genreId)) };
entry->clicked().connect([this, genreId] {
_filters.set(genreId);
});
});
db::Grouping::find(session, db::Grouping::FindParameters{}.setRelease(release->getId()).setSortMethod(db::GroupingSortMethod::TrackCountDesc).setRange(db::Range{ 0, maxTagCloudItemCount }), [&](const db::Grouping::pointer& grouping) {
const db::GroupingId groupingId{ grouping->getId() };
Wt::WInteractWidget* entry{ clusterContainers->addWidget(utils::createFilterGrouping(groupingId)) };
entry->clicked().connect([this, groupingId] {
_filters.set(groupingId);
});
});
db::Language::find(session, db::Language::FindParameters{}.setRelease(release->getId()).setSortMethod(db::LanguageSortMethod::TrackCountDesc).setRange(db::Range{ 0, maxTagCloudItemCount }), [&](const db::Language::pointer& language) {
const db::LanguageId languageId{ language->getId() };
Wt::WInteractWidget* entry{ clusterContainers->addWidget(utils::createFilterLanguage(languageId)) };
entry->clicked().connect([this, languageId] {
_filters.set(languageId);
});
});
db::Mood::find(session, db::Mood::FindParameters{}.setRelease(release->getId()).setSortMethod(db::MoodSortMethod::TrackCountDesc).setRange(db::Range{ 0, maxTagCloudItemCount }), [&](const db::Mood::pointer& mood) {
const db::MoodId moodId{ mood->getId() };
Wt::WInteractWidget* entry{ clusterContainers->addWidget(utils::createFilterMood(moodId)) };
entry->clicked().connect([this, moodId] {
_filters.set(moodId);
});
});
const auto clusterTypeIds{ db::ClusterType::findIds(session).results };
const auto clusterGroups{ release->getClusterGroups(clusterTypeIds, 3) };
const auto clusterGroups{ release->getClusterGroups(clusterTypeIds, maxTagCloudItemCount) };
for (const auto& clusters : clusterGroups)
{
+65
View File
@@ -28,6 +28,10 @@
#include "database/Session.hpp"
#include "database/Types.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"
#include "database/objects/TrackArtistLink.hpp"
@@ -41,6 +45,7 @@
#include "ModalManager.hpp"
#include "Utils.hpp"
#include "common/Template.hpp"
#include "explore/Filters.hpp"
#include "explore/PlayQueueController.hpp"
#include "resource/ArtworkResource.hpp"
#include "resource/DownloadResource.hpp"
@@ -102,6 +107,66 @@ namespace lms::ui::TrackListHelpers
trackInfo->bindWidget("copyright", std::move(copyrightWidget));
}
{
Wt::WContainerWidget* genreContainer{ trackInfo->bindNew<Wt::WContainerWidget>("genres") };
for (const db::Genre::pointer& genre : track->getGenres())
{
const db::GenreId genreId{ genre->getId() };
Wt::WInteractWidget* entry{ genreContainer->addWidget(utils::createFilterGenre(genreId)) };
entry->clicked().connect([&filters, genreId] { filters.set(genreId); });
}
if (const int count{ genreContainer->count() }; count > 0)
{
trackInfo->bindString("genres-label", Wt::WString::trn("Lms.Explore.genre", count));
trackInfo->setCondition("if-has-genres", true);
}
}
{
Wt::WContainerWidget* groupingContainer{ trackInfo->bindNew<Wt::WContainerWidget>("groupings") };
for (const db::Grouping::pointer& grouping : track->getGroupings())
{
const db::GroupingId groupingId{ grouping->getId() };
Wt::WInteractWidget* entry{ groupingContainer->addWidget(utils::createFilterGrouping(groupingId)) };
entry->clicked().connect([&filters, groupingId] { filters.set(groupingId); });
}
if (const int count{ groupingContainer->count() }; count > 0)
{
trackInfo->bindString("groupings-label", Wt::WString::trn("Lms.Explore.grouping", count));
trackInfo->setCondition("if-has-groupings", true);
}
}
{
Wt::WContainerWidget* languageContainer{ trackInfo->bindNew<Wt::WContainerWidget>("languages") };
for (const db::Language::pointer& language : track->getLanguages())
{
const db::LanguageId languageId{ language->getId() };
Wt::WInteractWidget* entry{ languageContainer->addWidget(utils::createFilterLanguage(languageId)) };
entry->clicked().connect([&filters, languageId] { filters.set(languageId); });
}
if (const int count{ languageContainer->count() }; count > 0)
{
trackInfo->bindString("languages-label", Wt::WString::trn("Lms.Explore.language", count));
trackInfo->setCondition("if-has-languages", true);
}
}
{
Wt::WContainerWidget* moodContainer{ trackInfo->bindNew<Wt::WContainerWidget>("moods") };
for (const db::Mood::pointer& mood : track->getMoods())
{
const db::MoodId moodId{ mood->getId() };
Wt::WInteractWidget* entry{ moodContainer->addWidget(utils::createFilterMood(moodId)) };
entry->clicked().connect([&filters, moodId] { filters.set(moodId); });
}
if (const int count{ moodContainer->count() }; count > 0)
{
trackInfo->bindString("moods-label", Wt::WString::trn("Lms.Explore.mood", count));
trackInfo->setCondition("if-has-moods", true);
}
}
Wt::WContainerWidget* clusterContainer{ trackInfo->bindWidget("clusters", utils::createFilterClustersForTrack(track, filters)) };
if (clusterContainer->count() > 0)
trackInfo->setCondition("if-has-clusters", true);
+54 -27
View File
@@ -17,9 +17,11 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <algorithm>
#include <chrono>
#include <filesystem>
#include <iostream>
#include <numeric>
#include <optional>
#include <stdexcept>
#include <stdlib.h>
@@ -38,8 +40,12 @@
#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/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"
@@ -58,9 +64,13 @@ namespace lms
std::size_t trackCountPerRelease{ 10 };
float compilationRatio{ 0.1 };
std::size_t genreCountPerTrack{ 3 };
std::size_t groupingCountPerTrack{ 1 };
std::size_t languageCountPerTrack{ 1 };
std::size_t moodCountPerTrack{ 3 };
std::size_t trackEmbeddedImagePerRelease{ 1 }; // usual case: one same image saved on each track
std::size_t genreCount{ 50 };
std::size_t groupingCount{ 10 };
std::size_t languageCount{ 10 };
std::size_t moodCount{ 25 };
bool generateMusicNNEmbeddings{ false };
std::filesystem::path trackPath;
@@ -70,18 +80,14 @@ namespace lms
{
db::Session& session;
std::vector<db::MediaLibrary::pointer> mediaLibraries;
std::vector<db::Cluster::pointer> genres;
std::vector<db::Cluster::pointer> moods;
std::vector<db::Genre::pointer> genres;
std::vector<db::Grouping::pointer> groupings;
std::vector<db::Language::pointer> languages;
std::vector<db::Mood::pointer> moods;
GenerationContext(db::Session& _session)
: session{ _session } {}
};
db::Cluster::pointer generateCluster(db::Session& session, db::ClusterType::pointer clusterType)
{
const std::string clusterName{ std::string{ clusterType->getName() } + "-" + core::UUID::generate().toString() };
return session.create<db::Cluster>(clusterType, clusterName);
}
db::Artist::pointer generateArtist(db::Session& session)
{
const core::UUID artistMBID{ core::UUID::generate() };
@@ -129,12 +135,27 @@ namespace lms
if (!trackEmbeddedImages.empty())
context.session.create<TrackEmbeddedImageLink>(track, *core::random::pickRandom(trackEmbeddedImages));
std::vector<ObjectPtr<Cluster>> clusters;
auto pickN = [](const auto& pool, std::size_t n) {
using T = typename std::decay_t<decltype(pool)>::value_type;
std::vector<std::size_t> indices(pool.size());
std::iota(indices.begin(), indices.end(), std::size_t{});
std::shuffle(indices.begin(), indices.end(), core::random::getRandGenerator());
const std::size_t count{ std::min(n, pool.size()) };
std::vector<T> picked;
picked.reserve(count);
for (std::size_t k{}; k < count; ++k)
picked.push_back(pool[indices[k]]);
return picked;
};
if (!context.genres.empty())
clusters.push_back(*core::random::pickRandom(context.genres));
track.modify()->setGenres(pickN(context.genres, params.genreCountPerTrack));
if (!context.groupings.empty())
track.modify()->setGroupings(pickN(context.groupings, params.groupingCountPerTrack));
if (!context.languages.empty())
track.modify()->setLanguages(pickN(context.languages, params.languageCountPerTrack));
if (!context.moods.empty())
clusters.push_back(*core::random::pickRandom(context.moods));
track.modify()->setClusters(clusters);
track.modify()->setMoods(pickN(context.moods, params.moodCountPerTrack));
if (params.generateMusicNNEmbeddings)
{
@@ -172,24 +193,18 @@ namespace lms
for (std::size_t i{}; i < params.mediaLibraryCount; ++i)
context.mediaLibraries.push_back(context.session.create<db::MediaLibrary>("Library" + std::to_string(i), "/root" + std::to_string(i)));
// create some random genres/moods
{
db::ClusterType::pointer genre{ db::ClusterType::find(context.session, "GENRE") };
if (!genre)
genre = context.session.create<db::ClusterType>("GENRE");
// create some random genres/groupings/languages/moods
for (std::size_t i{}; i < params.genreCount; ++i)
context.genres.push_back(context.session.create<db::Genre>("Genre-" + core::UUID::generate().toString()));
for (std::size_t i{}; i < params.genreCount; ++i)
context.genres.push_back(generateCluster(context.session, genre));
}
for (std::size_t i{}; i < params.groupingCount; ++i)
context.groupings.push_back(context.session.create<db::Grouping>("Grouping-" + core::UUID::generate().toString()));
{
db::ClusterType::pointer mood{ db::ClusterType::find(context.session, "MOOD") };
if (!mood)
mood = context.session.create<db::ClusterType>("MOOD");
for (std::size_t i{}; i < params.languageCount; ++i)
context.languages.push_back(context.session.create<db::Language>("Language-" + core::UUID::generate().toString()));
for (std::size_t i{}; i < params.moodCount; ++i)
context.moods.push_back(generateCluster(context.session, mood));
}
for (std::size_t i{}; i < params.moodCount; ++i)
context.moods.push_back(context.session.create<db::Mood>("Mood-" + core::UUID::generate().toString()));
}
} // namespace lms
@@ -219,6 +234,10 @@ int main(int argc, char* argv[])
("track-path",program_options::value<std::string>()->required(), "Path of a valid track file, that will be used for all generated tracks")
("genre-count", program_options::value<unsigned>()->default_value(defaultParams.genreCount), "Number of genres to generate")
("genre-count-per-track", program_options::value<unsigned>()->default_value(defaultParams.genreCountPerTrack), "Number of genres to assign to each track")
("grouping-count", program_options::value<unsigned>()->default_value(defaultParams.groupingCount), "Number of groupings to generate")
("grouping-count-per-track", program_options::value<unsigned>()->default_value(defaultParams.groupingCountPerTrack), "Number of groupings to assign to each track")
("language-count", program_options::value<unsigned>()->default_value(defaultParams.languageCount), "Number of languages to generate")
("language-count-per-track", program_options::value<unsigned>()->default_value(defaultParams.languageCountPerTrack), "Number of languages to assign to each track")
("mood-count", program_options::value<unsigned>()->default_value(defaultParams.moodCount), "Number of moods to generate")
("mood-count-per-track", program_options::value<unsigned>()->default_value(defaultParams.moodCountPerTrack), "Number of moods to assign to each track")
("musicnn-embeddings", program_options::bool_switch()->default_value(false), "Generate fake MusicNN embeddings for each track")
@@ -246,6 +265,14 @@ int main(int argc, char* argv[])
genParams.trackEmbeddedImagePerRelease = vm["track-embedded-image-count"].as<unsigned>();
genParams.generateMusicNNEmbeddings = vm["musicnn-embeddings"].as<bool>();
genParams.trackPath = std::filesystem::path{ vm["track-path"].as<std::string>() };
genParams.genreCount = vm["genre-count"].as<unsigned>();
genParams.genreCountPerTrack = vm["genre-count-per-track"].as<unsigned>();
genParams.groupingCount = vm["grouping-count"].as<unsigned>();
genParams.groupingCountPerTrack = vm["grouping-count-per-track"].as<unsigned>();
genParams.languageCount = vm["language-count"].as<unsigned>();
genParams.languageCountPerTrack = vm["language-count-per-track"].as<unsigned>();
genParams.moodCount = vm["mood-count"].as<unsigned>();
genParams.moodCountPerTrack = vm["mood-count-per-track"].as<unsigned>();
if (!std::filesystem::exists(genParams.trackPath))
throw std::runtime_error{ "File '" + genParams.trackPath.string() + "' does not exist!" };