diff --git a/README.md b/README.md
index 742ff106..83ca6c09 100644
--- a/README.md
+++ b/README.md
@@ -40,13 +40,12 @@ __Note__: depending on your database size and/or your hardware, the tag-based re
_LMS_ relies exclusively on tags to organize your music collection.
### Filtering
-You can specify the tags you want to use to filter your collection. By default, `genre`, `albumgrouping`, `mood` and `albummood` tags are used.
-In the administration settings, you can set whatever tags you want, even custom tags.
+It is possible to apply global filters on your collection using `genre`, `mood`, `grouping` and `language` tags. More tags can be added in the database administration settings, even custom tags.
__Note__: you can use the `lms-metadata` tool to have an idea of the tags parsed by _LMS_ using [TagLib](https://github.com/taglib/taglib).
### Multiple album artists
-_LMS_ requires the `ALBUMARTISTS` and `ALBUMARTISTSSORT` tags to properly handle multiple album artists on the same album. As they are a custom tags, you may need to setup your favorite tagger to add them.
+_LMS_ requires the `albumartists` and `albumartistssort` tags to properly handle multiple album artists on the same album. As they are a custom tags, you may need to setup your favorite tagger to add them.
__Note__: if you use [Picard](https://picard.musicbrainz.org/), add the following script to include these tags:
```
diff --git a/approot/admin-database.xml b/approot/admin-database.xml
index dcd81c0f..4f298605 100644
--- a/approot/admin-database.xml
+++ b/approot/admin-database.xml
@@ -44,12 +44,12 @@
-
- ${tr:Lms.clusters}
+
+ ${tr:Lms.Admin.Database.extra-tags-to-scan}
- ${clusters class="form-control"}
+ ${extra-tags-to-scan class="form-control"}
- ${clusters-info}
+ ${extra-tags-to-scan-info}
diff --git a/approot/messages.xml b/approot/messages.xml
index c7c44486..bedb9a47 100644
--- a/approot/messages.xml
+++ b/approot/messages.xml
@@ -12,6 +12,7 @@
Delete
Discard
Edit
+
This field must be in upper case
Loading...
Login
Logout
@@ -46,6 +47,7 @@
Daily
Music collection
+
Hourly
Scan now!
Monthly
diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml
index 769bc1e9..9358a0a9 100644
--- a/approot/messages_fr.xml
+++ b/approot/messages_fr.xml
@@ -12,6 +12,7 @@
Supprimer
Annuler
Editer
+
Ce champ doit être en majuscule
Chargement...
Login
Quitter
@@ -46,6 +47,7 @@
Tous les jours
Collection de musiques
+
Toutes les heures
Scanner maintenant !
Tous les mois
diff --git a/approot/messages_it.xml b/approot/messages_it.xml
index 92d4bcfc..da41aa51 100644
--- a/approot/messages_it.xml
+++ b/approot/messages_it.xml
@@ -12,6 +12,7 @@
Elimina
Annulla
Modifica
+
Caricamento...
Login
Logout
@@ -46,6 +47,7 @@
Giornaliera
Raccolta musicale
+
Ogni ora
Scansiona ora!
Mensile
diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml
index 17c96ec7..9c718905 100644
--- a/approot/messages_zh.xml
+++ b/approot/messages_zh.xml
@@ -12,6 +12,7 @@
删除
撤销
编辑
+
加载中...
登陆
注销
@@ -46,6 +47,7 @@
每日
音乐收藏
+
每小时
立即扫描!
每月
diff --git a/src/libs/metadata/impl/AvFormatParser.cpp b/src/libs/metadata/impl/AvFormatParser.cpp
index 2db802cd..dc9aee4e 100644
--- a/src/libs/metadata/impl/AvFormatParser.cpp
+++ b/src/libs/metadata/impl/AvFormatParser.cpp
@@ -29,234 +29,222 @@
namespace MetaData
{
+ namespace
+ {
+ template
+ std::optional findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list tags)
+ {
+ auto it = std::find_first_of(std::cbegin(metadataMap), std::cend(metadataMap), std::cbegin(tags), std::cend(tags), [](const auto& it, const auto& str) { return it.first == str; });
+ if (it == std::cend(metadataMap))
+ return std::nullopt;
-template
-std::optional
-findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list tags)
-{
- auto it = std::find_first_of(std::cbegin(metadataMap), std::cend(metadataMap), std::cbegin(tags), std::cend(tags), [](const auto& it, const auto& str) { return it.first == str; });
- if (it == std::cend(metadataMap))
- return std::nullopt;
+ return StringUtils::readAs(StringUtils::stringTrim(it->second));
+ }
- return StringUtils::readAs(StringUtils::stringTrim(it->second));
-}
+ template <>
+ std::optional> findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list tags)
+ {
+ std::optional str{ findFirstValueOfAs(metadataMap, tags) };
+ if (!str)
+ return std::nullopt;
-template <>
-std::optional>
-findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list tags)
-{
- std::optional str {findFirstValueOfAs(metadataMap, tags)};
- if (!str)
- return std::nullopt;
+ const std::vector strUuids{ StringUtils::splitString(*str, "/") };
+ std::vector res;
- const std::vector strUuids {StringUtils::splitString(*str, "/")};
- std::vector res;
+ for (std::string_view strUuid : strUuids)
+ {
+ std::optional uuid{ UUID::fromString(strUuid) };
+ if (!uuid)
+ return std::nullopt;
- for (std::string_view strUuid : strUuids)
- {
- std::optional uuid {UUID::fromString(strUuid)};
- if (!uuid)
- return std::nullopt;
+ res.push_back(std::move(*uuid));
+ }
- res.push_back(std::move(*uuid));
- }
+ return res;
+ }
- return res;
-}
+ std::vector getReleaseArtists(const Av::IAudioFile::MetadataMap& metadataMap)
+ {
+ std::vector res;
-static
-std::vector
-getReleaseArtists(const Av::IAudioFile::MetadataMap& metadataMap)
-{
- std::vector res;
+ auto name{ findFirstValueOfAs(metadataMap, {"ALBUM_ARTIST"}) };
+ if (!name)
+ return res;
- auto name {findFirstValueOfAs(metadataMap, {"ALBUM_ARTIST"})};
- if (!name)
- return res;
+ auto mbid{ findFirstValueOfAs(metadataMap, {"MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"}) };
- auto mbid {findFirstValueOfAs(metadataMap, {"MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"})};
+ return { Artist {mbid, *name, std::nullopt} };
+ }
- return {Artist {mbid, *name, std::nullopt} };
-}
+ std::vector getArtists(const Av::IAudioFile::MetadataMap& metadataMap)
+ {
+ std::vector artists;
-static
-std::vector
-getArtists(const Av::IAudioFile::MetadataMap& metadataMap)
-{
- std::vector artists;
+ std::vector artistNames;
+ if (metadataMap.find("ARTISTS") != metadataMap.end())
+ {
+ artistNames = StringUtils::splitString(metadataMap.find("ARTISTS")->second, "/;");
+ }
+ else if (metadataMap.find("ARTIST") != metadataMap.end())
+ {
+ artistNames = { metadataMap.find("ARTIST")->second };
+ }
- std::vector artistNames;
- if (metadataMap.find("ARTISTS") != metadataMap.end())
- {
- artistNames = StringUtils::splitString(metadataMap.find("ARTISTS")->second, "/;");
- }
- else if (metadataMap.find("ARTIST") != metadataMap.end())
- {
- artistNames = {metadataMap.find("ARTIST")->second};
- }
+ auto artistMBIDs{ findFirstValueOfAs>(metadataMap, {"MUSICBRAINZ ARTIST ID", "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ/ARTIST ID"}) };
- auto artistMBIDs {findFirstValueOfAs>(metadataMap, {"MUSICBRAINZ ARTIST ID", "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ/ARTIST ID"})};
+ for (std::size_t i{}; i < artistNames.size(); ++i)
+ {
+ if (artistMBIDs && artistNames.size() == artistMBIDs->size())
+ artists.emplace_back(Artist{ (*artistMBIDs)[i], artistNames[i], std::nullopt });
+ else
+ artists.emplace_back(Artist{ std::nullopt, artistNames[i], std::nullopt });
+ }
- for (std::size_t i {}; i < artistNames.size(); ++i)
- {
- if (artistMBIDs && artistNames.size() == artistMBIDs->size())
- artists.emplace_back(Artist {(*artistMBIDs)[i], artistNames[i], std::nullopt});
- else
- artists.emplace_back(Artist {std::nullopt, artistNames[i], std::nullopt});
- }
+ return artists;
+ }
- return artists;
-}
+ std::optional getRelease(const Av::IAudioFile::MetadataMap& metadataMap)
+ {
+ std::optional res;
-static
-std::optional
-getRelease(const Av::IAudioFile::MetadataMap& metadataMap)
-{
- std::optional res;
+ std::optional releaseName{ findFirstValueOfAs(metadataMap, {"ALBUM", "TALB", "WM/ALBUMTITLE"}) };
+ if (!releaseName)
+ return res;
- std::optional releaseName {findFirstValueOfAs(metadataMap, {"ALBUM", "TALB", "WM/ALBUMTITLE"})};
- if (!releaseName)
- return res;
+ res.emplace();
+ res->name = std::move(*releaseName);
+ res->mbid = findFirstValueOfAs(metadataMap, { "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ/ALBUM ID" });
+ res->artists = getReleaseArtists(metadataMap);
+ res->mediumCount = findFirstValueOfAs(metadataMap, { "TOTALDISCS", "DISCTOTAL" });
+ if (!res->mediumCount)
+ {
+ // mediumCount may be encoded as position/count
+ if (const auto value{ findFirstValueOfAs(metadataMap, {"TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET"}) })
+ {
+ // Expecting 'Number/Total'
+ const std::vector strings{ StringUtils::splitString(*value, "/") };
+ if (strings.size() == 2)
+ res->mediumCount = StringUtils::readAs(strings[1]);
+ }
+ }
- res.emplace();
- res->name = std::move(*releaseName);
- res->mbid = findFirstValueOfAs(metadataMap, {"MUSICBRAINZ ALBUM ID", "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ/ALBUM ID"});
- res->artists = getReleaseArtists(metadataMap);
- res->mediumCount = findFirstValueOfAs(metadataMap, {"TOTALDISCS", "DISCTOTAL"});
- if (!res->mediumCount)
- {
- // mediumCount may be encoded as position/count
- if (const auto value {findFirstValueOfAs(metadataMap, {"TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET"})})
- {
- // Expecting 'Number/Total'
- const std::vector strings {StringUtils::splitString(*value, "/") };
- if (strings.size() == 2)
- res->mediumCount = StringUtils::readAs(strings[1]);
- }
- }
+ return res;
+ }
- return res;
-}
+ std::optional getMedium(const Av::IAudioFile::MetadataMap& metadataMap)
+ {
+ std::optional res;
+ res.emplace();
-static
-std::optional
-getMedium(const Av::IAudioFile::MetadataMap& metadataMap)
-{
- std::optional res;
- res.emplace();
+ res->type = findFirstValueOfAs(metadataMap, { "TMED", "MEDIA", "WM/MEDIA" }).value_or("");
+ res->name = findFirstValueOfAs(metadataMap, { "TSST", "DISCSUBTITLE", "SETSUBTITLE" }).value_or("");
+ res->trackCount = findFirstValueOfAs(metadataMap, { "TOTALTRACKS", "TRACKTOTAL" });
+ if (!res->trackCount)
+ {
+ // totalTracks may be encoded as "position/count"
+ if (const auto value{ findFirstValueOfAs(metadataMap, {"TRCK", "TRACK", "TRACKNUMBER", "TRKN", "WM/TRACKNUMBER"}) })
+ {
+ // Expecting 'Number/Total'
+ const std::vector strings{ StringUtils::splitString(*value, "/") };
+ if (strings.size() == 2)
+ res->trackCount = StringUtils::readAs(strings[1]);
+ }
+ }
- res->type = findFirstValueOfAs(metadataMap, {"TMED", "MEDIA", "WM/MEDIA"}).value_or("");
- res->name = findFirstValueOfAs(metadataMap, {"TSST", "DISCSUBTITLE", "SETSUBTITLE"}).value_or("");
- res->trackCount = findFirstValueOfAs(metadataMap, {"TOTALTRACKS", "TRACKTOTAL"});
- if (!res->trackCount)
- {
- // totalTracks may be encoded as "position/count"
- if (const auto value {findFirstValueOfAs(metadataMap, {"TRCK", "TRACK", "TRACKNUMBER", "TRKN", "WM/TRACKNUMBER"})})
- {
- // Expecting 'Number/Total'
- const std::vector strings {StringUtils::splitString(*value, "/") };
- if (strings.size() == 2)
- res->trackCount = StringUtils::readAs(strings[1]);
- }
- }
+ // position may be encoded in TPOS/DISC/DISK as "position/count". Expecting 'Number[/Total]'
+ res->position = findFirstValueOfAs(metadataMap, { "TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET" });
+ res->release = getRelease(metadataMap);
- // position may be encoded in TPOS/DISC/DISK as "position/count". Expecting 'Number[/Total]'
- res->position = findFirstValueOfAs(metadataMap, {"TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET"});
- res->release = getRelease(metadataMap);
+ if (res->type.empty()
+ && res->name.empty()
+ && !res->trackCount
+ && !res->position
+ && !res->release
+ && !res->replayGain)
+ {
+ res.reset();
+ }
- if (res->type.empty()
- && res->name.empty()
- && !res->trackCount
- && !res->position
- && !res->release
- && !res->replayGain)
- {
- res.reset();
- }
+ return res;
+ }
+ }
- return res;
-}
+ std::optional AvFormatParser::parse(const std::filesystem::path& p, bool debug)
+ {
+ Track track;
-std::optional
-AvFormatParser::parse(const std::filesystem::path& p, bool debug)
-{
- Track track;
+ try
+ {
+ const auto mediaFile{ Av::parseAudioFile(p) };
- try
- {
- const auto mediaFile {Av::parseAudioFile(p)};
+ Av::ContainerInfo info{ mediaFile->getContainerInfo() };
+ track.duration = info.duration;
+ track.bitrate = info.bitrate;
+ track.hasCover = mediaFile->hasAttachedPictures();
- Av::ContainerInfo info{ mediaFile->getContainerInfo() };
- track.duration = info.duration;
- track.bitrate = info.bitrate;
- track.hasCover = mediaFile->hasAttachedPictures();
+ MetaData::Tags tags;
- MetaData::Tags tags;
+ const Av::IAudioFile::MetadataMap metadataMap{ mediaFile->getMetaData() };
- const Av::IAudioFile::MetadataMap metadataMap {mediaFile->getMetaData()};
+ track.artists = getArtists(metadataMap);
+ track.medium = getMedium(metadataMap);
- track.artists = getArtists(metadataMap);
- track.medium = getMedium(metadataMap);
+ for (const auto& [tag, value] : metadataMap)
+ {
+ if (debug)
+ std::cout << "TAG = " << tag << ", VAL = " << value << std::endl;
- for (const auto& [tag, value] : metadataMap)
- {
- if (debug)
- std::cout << "TAG = " << tag << ", VAL = " << value << std::endl;
+ if (tag == "TITLE")
+ track.title = value;
+ else if (tag == "TRACK")
+ {
+ // Expecting 'Number/Total'
+ track.position = StringUtils::readAs(value);
+ }
+ else if (tag == "DATE"
+ || tag == "YEAR"
+ || tag == "WM/YEAR")
+ {
+ track.date = Utils::parseDate(value);
+ }
+ else if (tag == "TDOR" // Original release time (ID3v2 2.4)
+ || tag == "TORY") // Original release year
+ {
+ track.originalDate = Utils::parseDate(value);
+ }
+ else if (tag == "ACOUSTID ID")
+ {
+ track.acoustID = UUID::fromString(value);
+ }
+ else if (tag == "MUSICBRAINZ RELEASE TRACK ID"
+ || tag == "MUSICBRAINZ_RELEASETRACKID")
+ {
+ track.mbid = UUID::fromString(value);
+ }
+ else if (tag == "MUSICBRAINZ_TRACKID"
+ || tag == "MUSICBRAINZ/TRACK ID")
+ {
+ track.recordingMBID = UUID::fromString(value);
+ }
+ else if (std::find(std::cbegin(_extraTags), std::cend(_extraTags), tag) != std::cend(_extraTags))
+ {
+ const std::vector tagValues{ StringUtils::splitString(value, "/,;") };
- if (tag == "TITLE")
- track.title = value;
- else if (tag == "TRACK")
- {
- // Expecting 'Number/Total'
- track.position = StringUtils::readAs(value);
- }
- else if (tag == "DATE"
- || tag == "YEAR"
- || tag == "WM/YEAR")
- {
- track.date = Utils::parseDate(value);
- }
- else if (tag == "TDOR" // Original release time (ID3v2 2.4)
- || tag == "TORY") // Original release year
- {
- track.originalDate = Utils::parseDate(value);
- }
- else if (tag == "ACOUSTID ID")
- {
- track.acoustID = UUID::fromString(value);
- }
- else if (tag == "MUSICBRAINZ RELEASE TRACK ID"
- || tag == "MUSICBRAINZ_RELEASETRACKID")
- {
- track.mbid = UUID::fromString(value);
- }
- else if (tag == "MUSICBRAINZ_TRACKID"
- || tag == "MUSICBRAINZ/TRACK ID")
- {
- track.recordingMBID = UUID::fromString(value);
- }
- else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
- {
- const std::vector clusterNames {StringUtils::splitString(value, "/,;")};
+ if (!tagValues.empty())
+ {
+ std::set values;
+ std::transform(std::cbegin(tagValues), std::cend(tagValues), std::inserter(values, std::begin(values)), [](std::string_view v) { return std::string{ v }; });
+ track.tags[tag] = std::move(values);
+ }
+ }
+ }
+ }
+ catch (Av::Exception& e)
+ {
+ return std::nullopt;
+ }
- if (!clusterNames.empty())
- {
- std::set values;
- std::transform(std::cbegin(clusterNames), std::cend(clusterNames),
- std::inserter(values, std::begin(values)),
- [](std::string_view clusterName) { return std::string {clusterName}; });
- track.tags[tag] = std::move(values);
- }
- }
- }
- }
- catch(Av::Exception& e)
- {
- return std::nullopt;
- }
-
- return track;
-}
+ return track;
+ }
} // namespace MetaData
-
diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp
index cb4fa0c6..62f1ab1e 100644
--- a/src/libs/metadata/impl/TagLibParser.cpp
+++ b/src/libs/metadata/impl/TagLibParser.cpp
@@ -368,18 +368,18 @@ namespace MetaData
track.replayGain = StringUtils::readAs(value);
else if (tag == "ARTIST")
track.artistDisplayName = value;
- else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
+ else if (std::find(std::cbegin(_extraTags), std::cend(_extraTags), tag) != std::cend(_extraTags))
{
- std::set clusterNames;
+ std::set tagValues;
for (std::string_view valueList : values)
{
- const std::vector splittedValues{ splitAndTrimString(valueList, "/,;") };
+ const std::vector splittedValues{ splitAndTrimString(valueList, "/,;") }; // handle possibily bad split tags
for (std::string_view value : splittedValues)
- clusterNames.insert(std::string{ value });
+ tagValues.insert(std::string{ value });
}
- if (!clusterNames.empty())
- track.tags[tag] = std::move(clusterNames);
+ if (!tagValues.empty())
+ track.tags[tag] = std::move(tagValues);
}
}
diff --git a/src/libs/metadata/include/metadata/IParser.hpp b/src/libs/metadata/include/metadata/IParser.hpp
index 6db82619..ce09f4a7 100644
--- a/src/libs/metadata/include/metadata/IParser.hpp
+++ b/src/libs/metadata/include/metadata/IParser.hpp
@@ -34,7 +34,7 @@
namespace MetaData
{
- using Tags = std::map /* names */>;
+ using Tags = std::map /* values */>;
// Very simplified version of https://musicbrainz.org/doc/MusicBrainz_Database/Schema
@@ -131,10 +131,10 @@ namespace MetaData
virtual std::optional parse(const std::filesystem::path& p, bool debug = false) = 0;
- void setClusterTypeNames(const std::set& clusterTypeNames) { _clusterTypeNames = clusterTypeNames; }
+ void setExtraTags(const std::vector& extraTags) { _extraTags = std::set(extraTags.cbegin(), extraTags.cend()); }
protected:
- std::set _clusterTypeNames;
+ std::set _extraTags;
};
enum class ParserType
diff --git a/src/libs/services/database/impl/Artist.cpp b/src/libs/services/database/impl/Artist.cpp
index f8f233de..02b5052a 100644
--- a/src/libs/services/database/impl/Artist.cpp
+++ b/src/libs/services/database/impl/Artist.cpp
@@ -288,7 +288,7 @@ namespace Database
return Utils::execQuery(query, range);
}
- std::vector> Artist::getClusterGroups(std::vector clusterTypes, std::size_t size) const
+ std::vector> Artist::getClusterGroups(std::vector clusterTypeIds, std::size_t size) const
{
assert(session());
@@ -300,8 +300,8 @@ namespace Database
where.And(WhereClause("a.id = ?")).bind(getId().toString());
{
WhereClause clusterClause;
- for (auto clusterType : clusterTypes)
- clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
+ for (ClusterTypeId clusterTypeId : clusterTypeIds)
+ clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString());
where.And(clusterClause);
}
diff --git a/src/libs/services/database/impl/Cluster.cpp b/src/libs/services/database/impl/Cluster.cpp
index 4833d8c5..843375ee 100644
--- a/src/libs/services/database/impl/Cluster.cpp
+++ b/src/libs/services/database/impl/Cluster.cpp
@@ -44,14 +44,19 @@ namespace Database
query.join("track_cluster t_c ON t_c.cluster_id = c.id");
query.join("track t ON t.id = t_c.track_id");
}
+ if (!params.clusterTypeName.empty())
+ query.join("cluster_type c_t ON c_t.id = c.cluster_type_id");
if (params.track.isValid())
query.where("t.id = ?").bind(params.track);
if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release);
+ assert(!params.clusterType.isValid() || params.clusterTypeName.empty());
if (params.clusterType.isValid())
query.where("c.cluster_type_id = ?").bind(params.clusterType);
+ else if (!params.clusterTypeName.empty())
+ query.where("c_t.name = ?").bind(params.clusterTypeName);
return query;
}
@@ -170,7 +175,7 @@ namespace Database
}
- RangeResults ClusterType::findOrphans(Session& session, std::optional range)
+ RangeResults ClusterType::findOrphanIds(Session& session, std::optional range)
{
session.checkReadTransaction();
@@ -207,7 +212,7 @@ namespace Database
return session.getDboSession().find().where("id = ?").bind(id).resultValue();
}
- RangeResults ClusterType::find(Session& session, std::optional range)
+ RangeResults ClusterType::findIds(Session& session, std::optional range)
{
session.checkReadTransaction();
diff --git a/src/libs/services/database/impl/Migration.cpp b/src/libs/services/database/impl/Migration.cpp
index 1da6bae2..cb6a9169 100644
--- a/src/libs/services/database/impl/Migration.cpp
+++ b/src/libs/services/database/impl/Migration.cpp
@@ -249,6 +249,24 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
session.getDboSession().execute("ALTER TABLE user ADD subsonic_enable_transcoding_by_default INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*User::defaultSubsonicEnableTranscodingByDefault*/0)) + ")");
}
+ void migrateFromV46(Session& session)
+ {
+ // add extra tags to parse
+ session.getDboSession().execute(R"(CREATE TABLE IF NOT EXISTS "cluster_type_backup" (
+ "id" integer primary key autoincrement,
+ "version" integer not null,
+ "name" text not null
+);)");
+ session.getDboSession().execute("INSERT INTO cluster_type_backup SELECT id, version, name FROM cluster_type");
+ session.getDboSession().execute("DROP TABLE cluster_type");
+ session.getDboSession().execute("ALTER TABLE cluster_type_backup RENAME TO cluster_type");
+
+ session.getDboSession().execute("ALTER TABLE scan_settings ADD COLUMN extra_tags_to_scan TEXT");
+
+ // Just increment the scan version of the settings to make the next scheduled scan rescan everything
+ ScanSettings::get(session).modify()->incScanVersion();
+ }
+
void doDbMigration(Session& session)
{
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
@@ -273,6 +291,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
{43, migrateFromV43},
{44, migrateFromV44},
{45, migrateFromV45},
+ {46, migrateFromV46},
};
{
diff --git a/src/libs/services/database/impl/Migration.hpp b/src/libs/services/database/impl/Migration.hpp
index d431713d..3c351bad 100644
--- a/src/libs/services/database/impl/Migration.hpp
+++ b/src/libs/services/database/impl/Migration.hpp
@@ -26,7 +26,7 @@ namespace Database
class Session;
using Version = std::size_t;
- static constexpr Version LMS_DATABASE_VERSION{ 46 };
+ static constexpr Version LMS_DATABASE_VERSION{ 47 };
class VersionInfo
{
public:
diff --git a/src/libs/services/database/impl/Release.cpp b/src/libs/services/database/impl/Release.cpp
index d5f397b8..cc55a317 100644
--- a/src/libs/services/database/impl/Release.cpp
+++ b/src/libs/services/database/impl/Release.cpp
@@ -481,7 +481,7 @@ namespace Database
return query.resultValue();
}
- std::vector> Release::getClusterGroups(const std::vector& clusterTypes, std::size_t size) const
+ std::vector> Release::getClusterGroups(const std::vector& clusterTypeIds, std::size_t size) const
{
assert(session());
@@ -494,8 +494,8 @@ namespace Database
where.And(WhereClause("r.id = ?")).bind(getId().toString());
{
WhereClause clusterClause;
- for (auto clusterType : clusterTypes)
- clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
+ for (const ClusterTypeId clusterTypeId : clusterTypeIds)
+ clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString());
where.And(clusterClause);
}
oss << " " << where.get();
diff --git a/src/libs/services/database/impl/ScanSettings.cpp b/src/libs/services/database/impl/ScanSettings.cpp
index ad0563c9..26c9ed50 100644
--- a/src/libs/services/database/impl/ScanSettings.cpp
+++ b/src/libs/services/database/impl/ScanSettings.cpp
@@ -30,30 +30,6 @@
namespace Database
{
- namespace
- {
-
- const std::set defaultClusterTypeNames =
- {
- "GENRE",
- "ALBUMGROUPING",
- "MOOD",
- "ALBUMMOOD",
- };
-
- }
- void ScanSettings::init(Session& session)
- {
- session.checkWriteTransaction();
-
- pointer settings{ get(session) };
- if (settings)
- return;
-
- settings = session.getDboSession().add(std::make_unique());
- settings.modify()->setClusterTypes(session, defaultClusterTypeNames);
- }
-
ScanSettings::pointer ScanSettings::get(Session& session)
{
session.checkReadTransaction();
@@ -77,9 +53,9 @@ namespace Database
_audioFileExtensions += " " + ext.string();
}
- std::vector ScanSettings::getClusterTypes() const
+ std::vector ScanSettings::getExtraTagsToScan() const
{
- return std::vector(std::cbegin(_clusterTypes), std::cend(_clusterTypes));
+ return StringUtils::splitString(_extraTagsToScan, ";");
}
void ScanSettings::setMediaDirectory(const std::filesystem::path& p)
@@ -87,58 +63,17 @@ namespace Database
_mediaDirectory = StringUtils::stringTrimEnd(p.string(), "/\\");
}
- template
- std::set getNames(It begin, It end)
+ void ScanSettings::setExtraTagsToScan(const std::vector& extraTags)
{
- std::set names;
- std::transform(begin, end, std::inserter(names, std::cbegin(names)),
- [](const ClusterType::pointer& clusterType)
- {
- return clusterType->getName();
- });
+ std::string newTagsToScan{ StringUtils::joinStrings(extraTags, ";") };
+ if (newTagsToScan != _extraTagsToScan)
+ incScanVersion();
- return names;
+ _extraTagsToScan = std::move(newTagsToScan);
}
- void ScanSettings::setClusterTypes(Session& session, const std::set& clusterTypeNames)
- {
- session.checkWriteTransaction();
-
- bool needRescan{};
-
- // Create any missing cluster type
- for (const std::string_view clusterTypeName : clusterTypeNames)
- {
- ClusterType::pointer clusterType{ ClusterType::find(session, clusterTypeName) };
- if (!clusterType)
- {
- LMS_LOG(DB, INFO, "Creating cluster type " << clusterTypeName);
- clusterType = session.create(clusterTypeName);
- _clusterTypes.insert(getDboPtr(clusterType));
-
- needRescan = true;
- }
- }
-
- // Delete no longer existing cluster types
- for (Wt::Dbo::ptr clusterType : _clusterTypes)
- {
- if (std::none_of(clusterTypeNames.begin(), clusterTypeNames.end(),
- [clusterType](std::string_view name) { return name == clusterType->getName(); }))
- {
- LMS_LOG(DB, INFO, "Deleting cluster type " << clusterType->getName());
- clusterType.remove();
- }
- }
-
- if (needRescan)
- _scanVersion += 1;
- }
-
- void
- ScanSettings::incScanVersion()
+ void ScanSettings::incScanVersion()
{
_scanVersion += 1;
}
-
} // namespace Database
diff --git a/src/libs/services/database/impl/Session.cpp b/src/libs/services/database/impl/Session.cpp
index 4386d0e8..84c970ff 100644
--- a/src/libs/services/database/impl/Session.cpp
+++ b/src/libs/services/database/impl/Session.cpp
@@ -171,13 +171,6 @@ namespace Database
_session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_backend_idx ON starred_track(user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_track_track_user_backend_idx ON starred_track(track_id,user_id,backend)");
}
-
- // Initial settings tables
- {
- auto uniqueTransaction{ createWriteTransaction() };
-
- ScanSettings::init(*this);
- }
}
void Session::analyze()
diff --git a/src/libs/services/database/impl/SqlQuery.cpp b/src/libs/services/database/impl/SqlQuery.cpp
index 0be0511f..643f8939 100644
--- a/src/libs/services/database/impl/SqlQuery.cpp
+++ b/src/libs/services/database/impl/SqlQuery.cpp
@@ -67,11 +67,11 @@ WhereClause::get() const
}
WhereClause&
-WhereClause::bind(const std::string& bindArg)
+WhereClause::bind(std::string_view bindArg)
{
assert(_bindArgs.size() < static_cast(std::count(_clause.begin(), _clause.end(), '?')));
- _bindArgs.push_back(bindArg);
+ _bindArgs.push_back(std::string{ bindArg });
return *this;
}
@@ -102,8 +102,8 @@ SelectStatement::And(const std::string& statement)
{
_statement.push_back(statement);
- _statement.sort();
- _statement.unique();
+ std::sort(_statement.begin(), _statement.end());
+ _statement.erase(std::unique(_statement.begin(), _statement.end()), _statement.end());
return *this;
}
@@ -113,7 +113,7 @@ SelectStatement::get() const
{
std::string res = "SELECT ";
- for (std::list::const_iterator it = _statement.begin(); it != _statement.end(); ++it)
+ for (auto it = _statement.begin(); it != _statement.end(); ++it)
{
if (it != _statement.begin())
res += ",";
@@ -150,8 +150,8 @@ FromClause::And(const FromClause& clause)
_clause.push_back(fromClause);
}
- _clause.sort();
- _clause.unique();
+ std::sort(_clause.begin(), _clause.end());
+ _clause.erase(std::unique(_clause.begin(), _clause.end()), _clause.end());
return *this;
}
@@ -164,7 +164,7 @@ FromClause::get() const
if (!_clause.empty())
{
oss << "FROM ";
- for (std::list::const_iterator it = _clause.begin(); it != _clause.end(); ++it) {
+ for (auto it = _clause.begin(); it != _clause.end(); ++it) {
if (it != _clause.begin())
oss << ",";
diff --git a/src/libs/services/database/impl/SqlQuery.hpp b/src/libs/services/database/impl/SqlQuery.hpp
index 21e20fee..e6625f7c 100644
--- a/src/libs/services/database/impl/SqlQuery.hpp
+++ b/src/libs/services/database/impl/SqlQuery.hpp
@@ -19,7 +19,7 @@
#pragma once
-#include
+#include
#include
@@ -33,14 +33,14 @@ class WhereClause
WhereClause& Or(const WhereClause& clause);
// Arguments binding (for each '?' in where clause)
- WhereClause& bind(const std::string& arg);
+ WhereClause& bind(std::string_view arg);
std::string get() const;
- const std::list& getBindArgs() const {return _bindArgs;}
+ const std::vector& getBindArgs() const {return _bindArgs;}
private:
std::string _clause; // WHERE clause
- std::list _bindArgs;
+ std::vector _bindArgs;
};
class InnerJoinClause
@@ -81,7 +81,7 @@ class SelectStatement
std::string get() const;
private:
- std::list _statement;
+ std::vector _statement;
};
class FromClause
@@ -95,7 +95,7 @@ class FromClause
std::string get() const;
private:
- std::list _clause;
+ std::vector _clause;
};
class SqlQuery
diff --git a/src/libs/services/database/impl/Track.cpp b/src/libs/services/database/impl/Track.cpp
index 71229eee..71ec3932 100644
--- a/src/libs/services/database/impl/Track.cpp
+++ b/src/libs/services/database/impl/Track.cpp
@@ -479,7 +479,7 @@ namespace Database
return std::vector(_trackArtistLinks.begin(), _trackArtistLinks.end());
}
- std::vector> Track::getClusterGroups(const std::vector& clusterTypes, std::size_t size) const
+ std::vector> Track::getClusterGroups(const std::vector& clusterTypeIds, std::size_t size) const
{
assert(self());
assert(session());
@@ -493,8 +493,8 @@ namespace Database
where.And(WhereClause("t.id = ?")).bind(getId().toString());
{
WhereClause clusterClause;
- for (auto clusterType : clusterTypes)
- clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
+ for (ClusterTypeId clusterTypeId : clusterTypeIds)
+ clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString());
where.And(clusterClause);
}
oss << " " << where.get();
@@ -514,8 +514,8 @@ namespace Database
}
std::vector> res;
- for (auto cluster_list : clusters)
- res.push_back(cluster_list.second);
+ for (const auto& [type, clusters] : clusters)
+ res.push_back(clusters);
return res;
}
diff --git a/src/libs/services/database/impl/TrackList.cpp b/src/libs/services/database/impl/TrackList.cpp
index 1f571c03..cc2e3b72 100644
--- a/src/libs/services/database/impl/TrackList.cpp
+++ b/src/libs/services/database/impl/TrackList.cpp
@@ -187,12 +187,12 @@ namespace Database
return std::vector(res.begin(), res.end());
}
- std::vector> TrackList::getClusterGroups(const std::vector& clusterTypes, std::size_t size) const
+ std::vector> TrackList::getClusterGroups(const std::vector& clusterTypeIds, std::size_t size) const
{
assert(session());
std::vector> res;
- if (clusterTypes.empty())
+ if (clusterTypeIds.empty())
return res;
auto query{ session()->query>("SELECT c from cluster c") };
@@ -208,12 +208,12 @@ namespace Database
std::ostringstream oss;
oss << "c_type.id IN (";
bool first{ true };
- for (auto clusterType : clusterTypes)
+ for (ClusterTypeId clusterTypeId : clusterTypeIds)
{
if (!first)
oss << ", ";
oss << "?";
- query.bind(clusterType->getId());
+ query.bind(clusterTypeId);
first = false;
}
oss << ")";
diff --git a/src/libs/services/database/include/services/database/Artist.hpp b/src/libs/services/database/include/services/database/Artist.hpp
index e561df6b..b64b0226 100644
--- a/src/libs/services/database/include/services/database/Artist.hpp
+++ b/src/libs/services/database/include/services/database/Artist.hpp
@@ -100,7 +100,7 @@ namespace Database
// Get the cluster of the tracks made by this artist
// Each clusters are grouped by cluster type, sorted by the number of occurence
// size is the max number of cluster per cluster type
- std::vector>> getClusterGroups(std::vector> clusterTypes, std::size_t size) const;
+ std::vector>> getClusterGroups(std::vector clusterTypeIds, std::size_t size) const;
void setName(std::string_view name) { _name = name; }
void setMBID(const std::optional& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
diff --git a/src/libs/services/database/include/services/database/Cluster.hpp b/src/libs/services/database/include/services/database/Cluster.hpp
index f2cabf82..efdedc5b 100644
--- a/src/libs/services/database/include/services/database/Cluster.hpp
+++ b/src/libs/services/database/include/services/database/Cluster.hpp
@@ -38,7 +38,6 @@ namespace Database {
class Track;
class ClusterType;
- class ScanSettings;
class Session;
class Cluster final : public Object
@@ -47,12 +46,14 @@ namespace Database {
struct FindParameters
{
std::optional range;
- ClusterTypeId clusterType; // if non empty, clusters that belong to this cluster type
+ ClusterTypeId clusterType; // if non empty, clusters that belong to this cluster type
+ std::string clusterTypeName; // if non empty, clusters that belong to this cluster type
TrackId track; // if set, clusters involved in this track
ReleaseId release; // if set, clusters involved in this release
FindParameters& setRange(std::optional _range) { range = _range; return *this; }
FindParameters& setClusterType(ClusterTypeId _clusterType) { clusterType = _clusterType; return *this; }
+ FindParameters& setClusterTypeName(std::string_view _name) { clusterTypeName = _name; return *this; }
FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
};
@@ -117,10 +118,10 @@ namespace Database {
// Getters
static std::size_t getCount(Session& session);
- static RangeResults find(Session& session, std::optional range = std::nullopt);
+ static RangeResults findIds(Session& session, std::optional range = std::nullopt);
static pointer find(Session& session, std::string_view name);
static pointer find(Session& session, ClusterTypeId id);
- static RangeResults findOrphans(Session& session, std::optional range = std::nullopt);
+ static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt);
static RangeResults findUsed(Session& session, std::optional range = std::nullopt);
static void remove(Session& session, const std::string& name);
@@ -135,7 +136,6 @@ namespace Database {
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToOne, "cluster_type");
- Wt::Dbo::belongsTo(a, _scanSettings, "scan_settings", Wt::Dbo::OnDeleteCascade);
}
private:
@@ -147,7 +147,6 @@ namespace Database {
std::string _name;
Wt::Dbo::collection< Wt::Dbo::ptr > _clusters;
- Wt::Dbo::ptr _scanSettings;
};
} // namespace Database
diff --git a/src/libs/services/database/include/services/database/Release.hpp b/src/libs/services/database/include/services/database/Release.hpp
index ccf191bb..6c41c0c0 100644
--- a/src/libs/services/database/include/services/database/Release.hpp
+++ b/src/libs/services/database/include/services/database/Release.hpp
@@ -99,7 +99,7 @@ namespace Database
// Get the cluster of the tracks that belong to this release
// Each clusters are grouped by cluster type, sorted by the number of occurence (max to min)
// size is the max number of cluster per cluster type
- std::vector>> getClusterGroups(const std::vector>& clusterTypes, std::size_t size) const;
+ std::vector>> getClusterGroups(const std::vector& clusterTypeIds, std::size_t size) const;
// Utility functions (if all tracks have the same values, which is legit to not be the case)
Wt::WDate getReleaseDate() const;
diff --git a/src/libs/services/database/include/services/database/ScanSettings.hpp b/src/libs/services/database/include/services/database/ScanSettings.hpp
index 8aec15b1..8b1a2dbe 100644
--- a/src/libs/services/database/include/services/database/ScanSettings.hpp
+++ b/src/libs/services/database/include/services/database/ScanSettings.hpp
@@ -32,9 +32,8 @@
LMS_DECLARE_IDTYPE(ScanSettingsId)
-namespace Database {
-
- class ClusterType;
+namespace Database
+{
class Session;
class ScanSettings final : public Object
@@ -67,7 +66,7 @@ namespace Database {
std::filesystem::path getMediaDirectory() const { return _mediaDirectory; }
Wt::WTime getUpdateStartTime() const { return _startTime; }
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
- std::vector> getClusterTypes() const;
+ std::vector getExtraTagsToScan() const;
std::vector getAudioFileExtensions() const;
SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
@@ -76,7 +75,7 @@ namespace Database {
void setMediaDirectory(const std::filesystem::path& p);
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
- void setClusterTypes(Session& session, const std::set& clusterTypeNames);
+ void setExtraTagsToScan(const std::vector& extraTags);
void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
void incScanVersion();
@@ -89,20 +88,17 @@ namespace Database {
Wt::Dbo::field(a, _updatePeriod, "update_period");
Wt::Dbo::field(a, _audioFileExtensions, "audio_file_extensions");
Wt::Dbo::field(a, _similarityEngineType, "similarity_engine_type");
- Wt::Dbo::hasMany(a, _clusterTypes, Wt::Dbo::ManyToOne, "scan_settings");
+ Wt::Dbo::field(a, _extraTagsToScan, "extra_tags_to_scan");
}
private:
- int _scanVersion{};
- std::string _mediaDirectory;
- Wt::WTime _startTime = Wt::WTime{ 0,0,0 };
- UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
+ int _scanVersion{};
+ std::string _mediaDirectory;
+ Wt::WTime _startTime = Wt::WTime{ 0,0,0 };
+ UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
SimilarityEngineType _similarityEngineType{ SimilarityEngineType::Clusters };
std::string _audioFileExtensions{ ".alac .mp3 .ogg .oga .aac .m4a .m4b .flac .wav .wma .aif .aiff .ape .mpc .shn .opus .wv" };
-
- Wt::Dbo::collection> _clusterTypes;
+ std::string _extraTagsToScan;
};
-
-
} // namespace Database
diff --git a/src/libs/services/database/include/services/database/Track.hpp b/src/libs/services/database/include/services/database/Track.hpp
index 3828672e..5865316a 100644
--- a/src/libs/services/database/include/services/database/Track.hpp
+++ b/src/libs/services/database/include/services/database/Track.hpp
@@ -176,7 +176,7 @@ namespace Database {
std::vector> getClusters() const;
std::vector getClusterIds() const;
- std::vector>> getClusterGroups(const std::vector>& clusterTypes, std::size_t size) const;
+ std::vector>> getClusterGroups(const std::vector& clusterTypes, std::size_t size) const;
template
void persist(Action& a)
diff --git a/src/libs/services/database/include/services/database/TrackList.hpp b/src/libs/services/database/include/services/database/TrackList.hpp
index dc29ef33..944cb1e6 100644
--- a/src/libs/services/database/include/services/database/TrackList.hpp
+++ b/src/libs/services/database/include/services/database/TrackList.hpp
@@ -99,7 +99,7 @@ namespace Database {
// Get clusters, order by occurence
std::vector> getClusters() const;
- std::vector>> getClusterGroups(const std::vector>& clusterTypes, std::size_t size) const;
+ std::vector>> getClusterGroups(const std::vector& clusterTypeIds, std::size_t size) const;
bool hasTrack(TrackId trackId) const;
diff --git a/src/libs/services/database/test/Cluster.cpp b/src/libs/services/database/test/Cluster.cpp
index 103f610d..bea3519a 100644
--- a/src/libs/services/database/test/Cluster.cpp
+++ b/src/libs/services/database/test/Cluster.cpp
@@ -60,7 +60,7 @@ TEST_F(DatabaseFixture, Cluster)
EXPECT_EQ(clusters.results.front(), cluster.getId());
}
- auto clusterTypes{ ClusterType::find(session) };
+ auto clusterTypes{ ClusterType::findIds(session) };
ASSERT_EQ(clusterTypes.results.size(), 1);
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
@@ -68,7 +68,7 @@ TEST_F(DatabaseFixture, Cluster)
ASSERT_EQ(clusterTypes.results.size(), 1);
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
- clusterTypes = ClusterType::findOrphans(session);
+ clusterTypes = ClusterType::findOrphanIds(session);
EXPECT_TRUE(clusterTypes.results.empty());
}
}
@@ -76,7 +76,7 @@ TEST_F(DatabaseFixture, Cluster)
{
auto transaction{ session.createWriteTransaction() };
- auto clusterTypes{ ClusterType::findOrphans(session) };
+ auto clusterTypes{ ClusterType::findOrphanIds(session) };
ASSERT_EQ(clusterTypes.results.size(), 1);
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
@@ -92,7 +92,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
{
auto transaction{ session.createReadTransaction() };
EXPECT_TRUE(Cluster::findOrphanIds(session).results.empty());
- auto clusterTypes{ ClusterType::findOrphans(session) };
+ auto clusterTypes{ ClusterType::findOrphanIds(session) };
ASSERT_EQ(clusterTypes.results.size(), 1);
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
}
@@ -131,7 +131,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
ASSERT_EQ(clusters.results.size(), 1);
EXPECT_EQ(clusters.results.front(), cluster2.getId());
- EXPECT_TRUE(ClusterType::findOrphans(session).results.empty());
+ EXPECT_TRUE(ClusterType::findOrphanIds(session).results.empty());
}
{
@@ -237,6 +237,38 @@ TEST_F(DatabaseFixture, Cluster_multiTracks)
}
}
+
+TEST_F(DatabaseFixture, ClusterType_singleTrack)
+{
+
+ {
+ auto transaction{ session.createReadTransaction() };
+ EXPECT_TRUE(Cluster::find(session, Cluster::FindParameters{}).results.empty());
+ EXPECT_TRUE(Cluster::find(session, Cluster::FindParameters{}.setClusterTypeName("Foo")).results.empty());
+ }
+
+ ScopedClusterType clusterType{ session, "MyClusterType" };
+ ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
+
+ {
+ auto transaction{ session.createReadTransaction() };
+ auto clusters {Cluster::findIds(session, Cluster::FindParameters{}).results};
+ ASSERT_EQ(clusters.size(), 1);
+ EXPECT_EQ(clusters.front(), cluster.getId());
+
+ clusters = Cluster::findIds(session, Cluster::FindParameters{}.setClusterType(clusterType.getId())).results;
+ ASSERT_EQ(clusters.size(), 1);
+ EXPECT_EQ(clusters.front(), cluster.getId());
+
+ clusters = Cluster::findIds(session, Cluster::FindParameters{}.setClusterTypeName("Foo")).results;
+ EXPECT_EQ(clusters.size(), 0);
+
+ clusters = Cluster::findIds(session, Cluster::FindParameters{}.setClusterTypeName("MyClusterType")).results;
+ ASSERT_EQ(clusters.size(), 1);
+ EXPECT_EQ(clusters.front(), cluster.getId());
+ }
+}
+
TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
{
ScopedTrack track{ session, "MyTrackFile" };
@@ -329,7 +361,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters)
{
auto transaction{ session.createReadTransaction() };
- EXPECT_TRUE(ClusterType::findOrphans(session).results.empty());
+ EXPECT_TRUE(ClusterType::findOrphanIds(session).results.empty());
EXPECT_EQ(Cluster::findOrphanIds(session).results.size(), 2);
EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
@@ -542,7 +574,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster)
auto transaction{ session.createReadTransaction() };
EXPECT_TRUE(Cluster::findOrphanIds(session).results.empty());
- EXPECT_TRUE(ClusterType::findOrphans(session).results.empty());
+ EXPECT_TRUE(ClusterType::findOrphanIds(session).results.empty());
EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
}
diff --git a/src/libs/services/database/test/Common.cpp b/src/libs/services/database/test/Common.cpp
index a914ea2b..7f37462e 100644
--- a/src/libs/services/database/test/Common.cpp
+++ b/src/libs/services/database/test/Common.cpp
@@ -59,17 +59,6 @@ void DatabaseFixture::SetUpTestCase()
Database::Session s{ _tmpDb->getDb() };
s.prepareTables();
s.analyze();
-
- // remove default created entries
- {
- auto transaction{ s.createWriteTransaction() };
-
- for (const Database::ClusterTypeId clusterTypeId : Database::ClusterType::find(s).results)
- {
- auto clusterType{ Database::ClusterType::find(s, clusterTypeId) };
- clusterType.remove();
- }
- }
}
}
diff --git a/src/libs/services/database/test/Release.cpp b/src/libs/services/database/test/Release.cpp
index 9a8ede14..dec592df 100644
--- a/src/libs/services/database/test/Release.cpp
+++ b/src/libs/services/database/test/Release.cpp
@@ -151,7 +151,6 @@ TEST_F(DatabaseFixture, Release_findByNameAndPath)
{
auto transaction{ session.createReadTransaction() };
- std::cout << "OK HERE" << std::endl;
{
const auto releases{ Release::find(session, "MyRelease", "/tmp/foo") };
ASSERT_EQ(releases.size(), 1);
diff --git a/src/libs/services/recommendation/impl/features/FeaturesEngineCache.cpp b/src/libs/services/recommendation/impl/features/FeaturesEngineCache.cpp
index 5985cc80..ca2e3587 100644
--- a/src/libs/services/recommendation/impl/features/FeaturesEngineCache.cpp
+++ b/src/libs/services/recommendation/impl/features/FeaturesEngineCache.cpp
@@ -65,7 +65,7 @@ namespace Recommendation
const auto& refVector = network.getRefVector({ x, y });
boost::property_tree::ptree node;
- for (auto value : refVector)
+ for (const auto& value : refVector)
node.add("values.value", value);
node.put("coord_x", x);
diff --git a/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.cpp b/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.cpp
index 78f12180..530f8532 100644
--- a/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.cpp
+++ b/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.cpp
@@ -74,6 +74,7 @@ namespace Scanner
{
removeOrphanTracks(context);
removeOrphanClusters();
+ removeOrphanClusterTypes();
removeOrphanArtists();
removeOrphanReleases();
}
@@ -152,6 +153,12 @@ namespace Scanner
removeOrphanEntries(_db.getTLSSession(), _abortScan);
}
+ void ScanStepRemoveOrphanDbFiles::removeOrphanClusterTypes()
+ {
+ LMS_LOG(DBUPDATER, DEBUG, "Checking orphan cluster types...");
+ removeOrphanEntries(_db.getTLSSession(), _abortScan);
+ }
+
void ScanStepRemoveOrphanDbFiles::removeOrphanArtists()
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan artists...");
diff --git a/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.hpp b/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.hpp
index fb3ff3b7..11f78385 100644
--- a/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.hpp
+++ b/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.hpp
@@ -37,6 +37,7 @@ namespace Scanner
void removeOrphanTracks(ScanContext& context);
void removeOrphanClusters();
+ void removeOrphanClusterTypes();
void removeOrphanArtists();
void removeOrphanReleases();
bool checkFile(const std::filesystem::path& p);
diff --git a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp
index a0b008a7..90ab509d 100644
--- a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp
+++ b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp
@@ -241,9 +241,9 @@ namespace Scanner
{
auto clusterType = ClusterType::find(session, tag);
if (!clusterType)
- continue;
+ clusterType = session.create(tag);
- for (auto clusterName : values)
+ for (const auto& clusterName : values)
{
auto cluster = clusterType->getCluster(clusterName);
if (!cluster)
@@ -279,7 +279,10 @@ namespace Scanner
void ScanStepScanFiles::process(ScanContext& context)
{
- _metadataParser->setClusterTypeNames(_settings.clusterTypeNames);
+ std::vector tagsToParse{ _tagsToParse };
+ tagsToParse.insert(std::end(tagsToParse), std::cbegin(_settings.extraTags), std::cend(_settings.extraTags));
+
+ _metadataParser->setExtraTags(tagsToParse);
context.currentStepStats.totalElems = context.stats.filesScanned;
diff --git a/src/libs/services/scanner/impl/ScanStepScanFiles.hpp b/src/libs/services/scanner/impl/ScanStepScanFiles.hpp
index d74d7c79..298ca9bb 100644
--- a/src/libs/services/scanner/impl/ScanStepScanFiles.hpp
+++ b/src/libs/services/scanner/impl/ScanStepScanFiles.hpp
@@ -20,24 +20,27 @@
#pragma once
#include
+#include
+#include
#include "metadata/IParser.hpp"
#include "ScanStepBase.hpp"
namespace Scanner
{
- class ScanStepScanFiles : public ScanStepBase
- {
- public:
- ScanStepScanFiles(InitParams& initParams);
+ class ScanStepScanFiles : public ScanStepBase
+ {
+ public:
+ ScanStepScanFiles(InitParams& initParams);
- private:
- ScanStep getStep() const override { return ScanStep::ScanningFiles; }
- std::string_view getStepName() const override { return "Scanning files"; }
- void process(ScanContext& context) override;
+ private:
+ ScanStep getStep() const override { return ScanStep::ScanningFiles; }
+ std::string_view getStepName() const override { return "Scanning files"; }
+ void process(ScanContext& context) override;
- void scanAudioFile(const std::filesystem::path& file, ScanContext& context);
+ void scanAudioFile(const std::filesystem::path& file, ScanContext& context);
- std::unique_ptr _metadataParser;
- };
+ std::unique_ptr _metadataParser;
+ const std::vector _tagsToParse{ "GENRE", "MOOD", "LANGUAGE", "ALBUMGROUPING" };
+ };
}
diff --git a/src/libs/services/scanner/impl/ScannerService.cpp b/src/libs/services/scanner/impl/ScannerService.cpp
index f648df17..e882c165 100644
--- a/src/libs/services/scanner/impl/ScannerService.cpp
+++ b/src/libs/services/scanner/impl/ScannerService.cpp
@@ -361,14 +361,10 @@ namespace Scanner
}
newSettings.mediaDirectory = scanSettings->getMediaDirectory();
- const auto clusterTypes = scanSettings->getClusterTypes();
- std::set clusterTypeNames;
-
- std::transform(std::cbegin(clusterTypes), std::cend(clusterTypes),
- std::inserter(clusterTypeNames, clusterTypeNames.begin()),
- [](const ClusterType::pointer& clusterType) { return std::string{ clusterType->getName() }; });
-
- newSettings.clusterTypeNames = std::move(clusterTypeNames);
+ {
+ const auto& tags{ scanSettings->getExtraTagsToScan() };
+ std::transform(std::cbegin(tags), std::cend(tags), std::back_inserter(newSettings.extraTags), [](std::string_view tag) { return std::string{ tag };});
+ }
}
return newSettings;
diff --git a/src/libs/services/scanner/impl/ScannerSettings.hpp b/src/libs/services/scanner/impl/ScannerSettings.hpp
index 9a469c78..0d347edf 100644
--- a/src/libs/services/scanner/impl/ScannerSettings.hpp
+++ b/src/libs/services/scanner/impl/ScannerSettings.hpp
@@ -20,7 +20,6 @@
#pragma once
#include
-#include
#include
#include
#include
@@ -36,7 +35,7 @@ namespace Scanner
std::vector supportedExtensions;
std::filesystem::path mediaDirectory;
bool skipDuplicateMBID {};
- std::set clusterTypeNames;
+ std::vector extraTags;
bool operator==(const ScannerSettings& rhs) const
{
@@ -46,7 +45,7 @@ namespace Scanner
&& supportedExtensions == rhs.supportedExtensions
&& mediaDirectory == rhs.mediaDirectory
&& skipDuplicateMBID == rhs.skipDuplicateMBID
- && clusterTypeNames == rhs.clusterTypeNames;
+ && extraTags == rhs.extraTags;
}
};
}
diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp
index b9bff4f5..01e2a3fa 100644
--- a/src/libs/subsonic/impl/responses/Album.cpp
+++ b/src/libs/subsonic/impl/responses/Album.cpp
@@ -131,7 +131,7 @@ namespace API::Subsonic
const ClusterType::pointer genreClusterType{ ClusterType::find(context.dbSession, "GENRE") };
if (genreClusterType)
{
- auto clusters{ release->getClusterGroups({genreClusterType}, 1) };
+ auto clusters{ release->getClusterGroups({genreClusterType->getId()}, 1) };
if (!clusters.empty() && !clusters.front().empty())
albumNode.setAttribute("genre", clusters.front().front()->getName());
}
@@ -160,16 +160,12 @@ namespace API::Subsonic
{
albumNode.createEmptyArrayValue(field);
- ClusterType::pointer clusterType{ ClusterType::find(context.dbSession, clusterTypeName) };
- if (clusterType)
- {
- Cluster::FindParameters params;
- params.setRelease(release->getId());
- params.setClusterType(clusterType->getId());
+ Cluster::FindParameters params;
+ params.setRelease(release->getId());
+ params.setClusterTypeName(clusterTypeName);
- for (const auto& cluster : Cluster::find(context.dbSession, params).results)
- albumNode.addArrayValue(field, cluster->getName());
- }
+ for (const auto& cluster : Cluster::find(context.dbSession, params).results)
+ albumNode.addArrayValue(field, cluster->getName());
} };
addClusters("moods", "MOOD");
diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp
index 66ef8f57..e01e6584 100644
--- a/src/libs/subsonic/impl/responses/Song.cpp
+++ b/src/libs/subsonic/impl/responses/Song.cpp
@@ -162,12 +162,15 @@ namespace API::Subsonic
trackResponse.setAttribute("starred", StringUtils::toISO8601String(dateTime));
// Report the first GENRE for this track
- const ClusterType::pointer genreClusterType{ ClusterType::find(context.dbSession, "GENRE") };
- if (genreClusterType)
+ std::vector genres;
{
- auto clusters{ track->getClusterGroups({genreClusterType}, 1) };
- if (!clusters.empty() && !clusters.front().empty())
- trackResponse.setAttribute("genre", clusters.front().front()->getName());
+ Cluster::FindParameters params;
+ params.setTrack(track->getId());
+ params.setClusterTypeName("GENRE");
+
+ genres = Cluster::find(context.dbSession, params).results;
+ if (!genres.empty())
+ trackResponse.setAttribute("genre", genres.front()->getName());
}
// OpenSubsonic specific fields (must always be set)
@@ -227,31 +230,20 @@ namespace API::Subsonic
{
trackResponse.createEmptyArrayValue(field);
- ClusterType::pointer clusterType{ ClusterType::find(context.dbSession, clusterTypeName) };
- if (clusterType)
- {
- Cluster::FindParameters params;
- params.setTrack(track->getId());
- params.setClusterType(clusterType->getId());
+ Cluster::FindParameters params;
+ params.setTrack(track->getId());
+ params.setClusterTypeName(clusterTypeName);
- for (const auto& cluster : Cluster::find(context.dbSession, params).results)
- trackResponse.addArrayValue(field, cluster->getName());
- }
+ for (const auto& cluster : Cluster::find(context.dbSession, params).results)
+ trackResponse.addArrayValue(field, cluster->getName());
} };
addClusters("moods", "MOOD");
// Genres
trackResponse.createEmptyArrayChild("genres");
- if (genreClusterType)
- {
- Cluster::FindParameters params;
- params.setTrack(track->getId());
- params.setClusterType(genreClusterType->getId());
-
- for (const auto& cluster : Cluster::find(context.dbSession, params).results)
- trackResponse.addArrayChild("genres", createItemGenreNode(cluster->getName()));
- }
+ for (const auto& genre : genres)
+ trackResponse.addArrayChild("genres", createItemGenreNode(genre->getName()));
trackResponse.addChild("replayGain", createReplayGainNode(track));
diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt
index 494d727e..d1d5db58 100644
--- a/src/lms/CMakeLists.txt
+++ b/src/lms/CMakeLists.txt
@@ -24,6 +24,7 @@ add_executable(lms
ui/common/MandatoryValidator.cpp
ui/common/PasswordValidator.cpp
ui/common/Template.cpp
+ ui/common/UppercaseValidator.cpp
ui/common/UUIDValidator.cpp
ui/explore/ArtistCollector.cpp
ui/explore/ArtistListHelpers.cpp
diff --git a/src/lms/ui/Utils.cpp b/src/lms/ui/Utils.cpp
index 1590fd5f..98297f7c 100644
--- a/src/lms/ui/Utils.cpp
+++ b/src/lms/ui/Utils.cpp
@@ -285,7 +285,8 @@ namespace UserInterface::Utils
std::unique_ptr clusterContainer{ std::make_unique() };
- const auto clusterTypes{ ScanSettings::get(LmsApp->getDbSession())->getClusterTypes() };
+ // TODO: optimize this
+ const auto clusterTypes{ ClusterType::findIds(LmsApp->getDbSession()).results };
const auto clusterGroups{ track->getClusterGroups(clusterTypes, 3) };
for (const auto& clusters : clusterGroups)
diff --git a/src/lms/ui/admin/DatabaseSettingsView.cpp b/src/lms/ui/admin/DatabaseSettingsView.cpp
index 87534651..671bb7fc 100644
--- a/src/lms/ui/admin/DatabaseSettingsView.cpp
+++ b/src/lms/ui/admin/DatabaseSettingsView.cpp
@@ -37,226 +37,221 @@
#include "common/DirectoryValidator.hpp"
#include "common/MandatoryValidator.hpp"
+#include "common/UppercaseValidator.hpp"
#include "common/ValueStringModel.hpp"
#include "ScannerController.hpp"
#include "LmsApplication.hpp"
-namespace UserInterface {
-
-using namespace Database;
-
-class DatabaseSettingsModel : public Wt::WFormModel
+namespace UserInterface
{
- public:
- // Associate each field with a unique string literal.
- static inline constexpr Field MediaDirectoryField {"media-directory"};
- static inline constexpr Field UpdatePeriodField {"update-period"};
- static inline constexpr Field UpdateStartTimeField {"update-start-time"};
- static inline constexpr Field SimilarityEngineTypeField {"similarity-engine-type"};
- static inline constexpr Field ClustersField {"clusters"};
+ using namespace Database;
- using UpdatePeriodModel = ValueStringModel;
+ class DatabaseSettingsModel : public Wt::WFormModel
+ {
+ public:
+ // Associate each field with a unique string literal.
+ static inline constexpr Field MediaDirectoryField{ "media-directory" };
+ static inline constexpr Field UpdatePeriodField{ "update-period" };
+ static inline constexpr Field UpdateStartTimeField{ "update-start-time" };
+ static inline constexpr Field SimilarityEngineTypeField{ "similarity-engine-type" };
+ static inline constexpr Field ExtraTagsField{ "extra-tags-to-scan" };
- DatabaseSettingsModel()
- {
- initializeModels();
+ using UpdatePeriodModel = ValueStringModel;
- addField(MediaDirectoryField);
- addField(UpdatePeriodField);
- addField(UpdateStartTimeField);
- addField(SimilarityEngineTypeField);
- addField(ClustersField);
+ static inline constexpr std::string_view extraTagsDelimiter{ ";" };
- auto dirValidator {createDirectoryValidator()};
- dirValidator->setMandatory(true);
- setValidator(MediaDirectoryField, std::move(dirValidator));
+ DatabaseSettingsModel()
+ {
+ initializeModels();
- setValidator(UpdatePeriodField, createMandatoryValidator());
- setValidator(UpdateStartTimeField, createMandatoryValidator());
- setValidator(SimilarityEngineTypeField, createMandatoryValidator());
+ addField(MediaDirectoryField);
+ addField(UpdatePeriodField);
+ addField(UpdateStartTimeField);
+ addField(SimilarityEngineTypeField);
+ addField(ExtraTagsField);
- // populate the model with initial data
- loadData();
- }
+ auto dirValidator{ createDirectoryValidator() };
+ dirValidator->setMandatory(true);
+ setValidator(MediaDirectoryField, std::move(dirValidator));
- std::shared_ptr updatePeriodModel() { return _updatePeriodModel; }
- std::shared_ptr updateStartTimeModel() { return _updateStartTimeModel; }
- std::shared_ptr similarityEngineTypeModel() { return _similarityEngineTypeModel; }
+ setValidator(UpdatePeriodField, createMandatoryValidator());
+ setValidator(UpdateStartTimeField, createMandatoryValidator());
+ setValidator(SimilarityEngineTypeField, createMandatoryValidator());
+ setValidator(ExtraTagsField, createUppercaseValidator(extraTagsDelimiter));
- void loadData()
- {
- auto transaction {LmsApp->getDbSession().createReadTransaction()};
+ // populate the model with initial data
+ loadData();
+ }
- const ScanSettings::pointer scanSettings {ScanSettings::get(LmsApp->getDbSession())};
+ std::shared_ptr updatePeriodModel() { return _updatePeriodModel; }
+ std::shared_ptr updateStartTimeModel() { return _updateStartTimeModel; }
+ std::shared_ptr similarityEngineTypeModel() { return _similarityEngineTypeModel; }
- setValue(MediaDirectoryField, scanSettings->getMediaDirectory().string());
+ void loadData()
+ {
+ auto transaction{ LmsApp->getDbSession().createReadTransaction() };
- auto periodRow {_updatePeriodModel->getRowFromValue(scanSettings->getUpdatePeriod())};
- if (periodRow)
- setValue(UpdatePeriodField, _updatePeriodModel->getString(*periodRow));
+ const ScanSettings::pointer scanSettings{ ScanSettings::get(LmsApp->getDbSession()) };
- auto startTimeRow {_updateStartTimeModel->getRowFromValue(scanSettings->getUpdateStartTime())};
- if (startTimeRow)
- setValue(UpdateStartTimeField, _updateStartTimeModel->getString(*startTimeRow));
+ setValue(MediaDirectoryField, scanSettings->getMediaDirectory().string());
- if (scanSettings->getUpdatePeriod() == ScanSettings::UpdatePeriod::Hourly
- || scanSettings->getUpdatePeriod() == ScanSettings::UpdatePeriod::Never)
- {
- setReadOnly(DatabaseSettingsModel::UpdateStartTimeField, true);
- }
+ auto periodRow{ _updatePeriodModel->getRowFromValue(scanSettings->getUpdatePeriod()) };
+ if (periodRow)
+ setValue(UpdatePeriodField, _updatePeriodModel->getString(*periodRow));
- auto similarityEngineTypeRow {_similarityEngineTypeModel->getRowFromValue(scanSettings->getSimilarityEngineType())};
- if (similarityEngineTypeRow)
- setValue(SimilarityEngineTypeField, _similarityEngineTypeModel->getString(*similarityEngineTypeRow));
+ auto startTimeRow{ _updateStartTimeModel->getRowFromValue(scanSettings->getUpdateStartTime()) };
+ if (startTimeRow)
+ setValue(UpdateStartTimeField, _updateStartTimeModel->getString(*startTimeRow));
- auto clusterTypes {scanSettings->getClusterTypes()};
- if (!clusterTypes.empty())
- {
- std::vector names;
- std::transform(clusterTypes.begin(), clusterTypes.end(), std::back_inserter(names), [](const auto& clusterType) { return clusterType->getName(); });
- setValue(ClustersField, StringUtils::joinStrings(names, " "));
- }
- }
+ if (scanSettings->getUpdatePeriod() == ScanSettings::UpdatePeriod::Hourly
+ || scanSettings->getUpdatePeriod() == ScanSettings::UpdatePeriod::Never)
+ {
+ setReadOnly(DatabaseSettingsModel::UpdateStartTimeField, true);
+ }
- void saveData()
- {
- auto transaction {LmsApp->getDbSession().createWriteTransaction()};
+ auto similarityEngineTypeRow{ _similarityEngineTypeModel->getRowFromValue(scanSettings->getSimilarityEngineType()) };
+ if (similarityEngineTypeRow)
+ setValue(SimilarityEngineTypeField, _similarityEngineTypeModel->getString(*similarityEngineTypeRow));
- ScanSettings::pointer scanSettings {ScanSettings::get(LmsApp->getDbSession())};
+ auto extraTags{ scanSettings->getExtraTagsToScan() };
+ setValue(ExtraTagsField, StringUtils::joinStrings(scanSettings->getExtraTagsToScan(), extraTagsDelimiter));
+ }
- scanSettings.modify()->setMediaDirectory(valueText(MediaDirectoryField).toUTF8());
+ void saveData()
+ {
+ auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
- auto updatePeriodRow {_updatePeriodModel->getRowFromString(valueText(UpdatePeriodField))};
- if (updatePeriodRow)
- scanSettings.modify()->setUpdatePeriod(_updatePeriodModel->getValue(*updatePeriodRow));
+ ScanSettings::pointer scanSettings{ ScanSettings::get(LmsApp->getDbSession()) };
- auto startTimeRow {_updateStartTimeModel->getRowFromString(valueText(UpdateStartTimeField))};
- if (startTimeRow)
- scanSettings.modify()->setUpdateStartTime(_updateStartTimeModel->getValue(*startTimeRow));
+ scanSettings.modify()->setMediaDirectory(valueText(MediaDirectoryField).toUTF8());
- auto similarityEngineTypeRow {_similarityEngineTypeModel->getRowFromString(valueText(SimilarityEngineTypeField))};
- if (similarityEngineTypeRow)
- scanSettings.modify()->setSimilarityEngineType(_similarityEngineTypeModel->getValue(*similarityEngineTypeRow));
+ auto updatePeriodRow{ _updatePeriodModel->getRowFromString(valueText(UpdatePeriodField)) };
+ if (updatePeriodRow)
+ scanSettings.modify()->setUpdatePeriod(_updatePeriodModel->getValue(*updatePeriodRow));
- const std::vector clusterTypes {StringUtils::splitString(valueText(ClustersField).toUTF8(), " ")};
- scanSettings.modify()->setClusterTypes(LmsApp->getDbSession(), std::set(clusterTypes.begin(), clusterTypes.end()));
- }
+ auto startTimeRow{ _updateStartTimeModel->getRowFromString(valueText(UpdateStartTimeField)) };
+ if (startTimeRow)
+ scanSettings.modify()->setUpdateStartTime(_updateStartTimeModel->getValue(*startTimeRow));
- private:
- void initializeModels()
- {
- _updatePeriodModel = std::make_shared>();
- _updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.never"), ScanSettings::UpdatePeriod::Never);
- _updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.hourly"), ScanSettings::UpdatePeriod::Hourly);
- _updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.daily"), ScanSettings::UpdatePeriod::Daily);
- _updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.weekly"), ScanSettings::UpdatePeriod::Weekly);
- _updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.monthly"), ScanSettings::UpdatePeriod::Monthly);
+ auto similarityEngineTypeRow{ _similarityEngineTypeModel->getRowFromString(valueText(SimilarityEngineTypeField)) };
+ if (similarityEngineTypeRow)
+ scanSettings.modify()->setSimilarityEngineType(_similarityEngineTypeModel->getValue(*similarityEngineTypeRow));
- _updateStartTimeModel = std::make_shared>();
- for (std::size_t i = 0; i < 24; ++i)
- {
- Wt::WTime time {static_cast(i), 0};
- _updateStartTimeModel->add(time.toString(), time);
- }
+ scanSettings.modify()->setExtraTagsToScan(StringUtils::splitString(valueText(ExtraTagsField).toUTF8(), extraTagsDelimiter));
+ }
- _similarityEngineTypeModel = std::make_shared>();
- _similarityEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.similarity-engine-type.clusters"), ScanSettings::SimilarityEngineType::Clusters);
- _similarityEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.similarity-engine-type.none"), ScanSettings::SimilarityEngineType::None);
- }
+ private:
+ void initializeModels()
+ {
+ _updatePeriodModel = std::make_shared>();
+ _updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.never"), ScanSettings::UpdatePeriod::Never);
+ _updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.hourly"), ScanSettings::UpdatePeriod::Hourly);
+ _updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.daily"), ScanSettings::UpdatePeriod::Daily);
+ _updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.weekly"), ScanSettings::UpdatePeriod::Weekly);
+ _updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.monthly"), ScanSettings::UpdatePeriod::Monthly);
- std::shared_ptr _updatePeriodModel;
- std::shared_ptr> _updateStartTimeModel;
- std::shared_ptr> _similarityEngineTypeModel;
-};
+ _updateStartTimeModel = std::make_shared>();
+ for (std::size_t i = 0; i < 24; ++i)
+ {
+ Wt::WTime time{ static_cast(i), 0 };
+ _updateStartTimeModel->add(time.toString(), time);
+ }
-DatabaseSettingsView::DatabaseSettingsView()
-{
- wApp->internalPathChanged().connect(this, [this]
- {
- refreshView();
- });
+ _similarityEngineTypeModel = std::make_shared>();
+ _similarityEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.similarity-engine-type.clusters"), ScanSettings::SimilarityEngineType::Clusters);
+ _similarityEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.similarity-engine-type.none"), ScanSettings::SimilarityEngineType::None);
+ }
- refreshView();
-}
+ std::shared_ptr _updatePeriodModel;
+ std::shared_ptr> _updateStartTimeModel;
+ std::shared_ptr> _similarityEngineTypeModel;
+ };
-void
-DatabaseSettingsView::refreshView()
-{
- if (!wApp->internalPathMatches("/admin/database"))
- return;
+ DatabaseSettingsView::DatabaseSettingsView()
+ {
+ wApp->internalPathChanged().connect(this, [this]
+ {
+ refreshView();
+ });
- clear();
+ refreshView();
+ }
- auto t {addNew(Wt::WString::tr("Lms.Admin.Database.template"))};
- auto model {std::make_shared()};
+ void DatabaseSettingsView::refreshView()
+ {
+ if (!wApp->internalPathMatches("/admin/database"))
+ return;
- // Media Directory
- t->setFormWidget(DatabaseSettingsModel::MediaDirectoryField, std::make_unique());
+ clear();
- // Update Period
- auto updatePeriod {std::make_unique()};
- updatePeriod->setModel(model->updatePeriodModel());
- updatePeriod->activated().connect([=](int row)
- {
- const ScanSettings::UpdatePeriod period {model->updatePeriodModel()->getValue(row)};
- model->setReadOnly(DatabaseSettingsModel::UpdateStartTimeField, period == ScanSettings::UpdatePeriod::Hourly || period == ScanSettings::UpdatePeriod::Never);
- t->updateModel(model.get());
- t->updateView(model.get());
- });
- t->setFormWidget(DatabaseSettingsModel::UpdatePeriodField, std::move(updatePeriod));
+ auto t{ addNew(Wt::WString::tr("Lms.Admin.Database.template")) };
+ auto model{ std::make_shared() };
- // Update Start Time
- auto updateStartTime {std::make_unique()};
- updateStartTime->setModel(model->updateStartTimeModel());
- t->setFormWidget(DatabaseSettingsModel::UpdateStartTimeField, std::move(updateStartTime));
+ // Media Directory
+ t->setFormWidget(DatabaseSettingsModel::MediaDirectoryField, std::make_unique());
- // Similarity engine type
- auto similarityEngineType {std::make_unique()};
- similarityEngineType->setModel(model->similarityEngineTypeModel());
- t->setFormWidget(DatabaseSettingsModel::SimilarityEngineTypeField, std::move(similarityEngineType));
+ // Update Period
+ auto updatePeriod{ std::make_unique() };
+ updatePeriod->setModel(model->updatePeriodModel());
+ updatePeriod->activated().connect([=](int row)
+ {
+ const ScanSettings::UpdatePeriod period{ model->updatePeriodModel()->getValue(row) };
+ model->setReadOnly(DatabaseSettingsModel::UpdateStartTimeField, period == ScanSettings::UpdatePeriod::Hourly || period == ScanSettings::UpdatePeriod::Never);
+ t->updateModel(model.get());
+ t->updateView(model.get());
+ });
+ t->setFormWidget(DatabaseSettingsModel::UpdatePeriodField, std::move(updatePeriod));
- // Clusters
- t->setFormWidget(DatabaseSettingsModel::ClustersField, std::make_unique