Refactored how filter tags are handled in the scan settings

This commit is contained in:
emeric
2023-11-25 15:52:21 +01:00
parent b4107be599
commit ece1d4e3d2
47 changed files with 645 additions and 605 deletions
+2 -3
View File
@@ -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. _LMS_ relies exclusively on tags to organize your music collection.
### Filtering ### Filtering
You can specify the tags you want to use to filter your collection. By default, `genre`, `albumgrouping`, `mood` and `albummood` tags are used. 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.
In the administration settings, you can set whatever tags you want, 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). __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 ### 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: __Note__: if you use [Picard](https://picard.musicbrainz.org/), add the following script to include these tags:
``` ```
+4 -4
View File
@@ -44,12 +44,12 @@
</div> </div>
</div> </div>
<div class="col-12"> <div class="col-12">
<label class="form-label" for="${id:clusters}"> <label class="form-label" for="${id:extra-tags-to-scan}">
${tr:Lms.clusters} ${tr:Lms.Admin.Database.extra-tags-to-scan}
</label> </label>
${clusters class="form-control"} ${extra-tags-to-scan class="form-control"}
<div class="invalid-feedback"> <div class="invalid-feedback">
${clusters-info} ${extra-tags-to-scan-info}
</div> </div>
</div> </div>
<div class="col-12"> <div class="col-12">
+2
View File
@@ -12,6 +12,7 @@
<message id="Lms.delete">Delete</message> <message id="Lms.delete">Delete</message>
<message id="Lms.discard">Discard</message> <message id="Lms.discard">Discard</message>
<message id="Lms.edit">Edit</message> <message id="Lms.edit">Edit</message>
<message id="Lms.field-must-be-in-upper-case">This field must be in upper case</message>
<message id="Lms.loading">Loading...</message> <message id="Lms.loading">Loading...</message>
<message id="Lms.login">Login</message> <message id="Lms.login">Login</message>
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Logout</message> <message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Logout</message>
@@ -46,6 +47,7 @@
<!--Administration--> <!--Administration-->
<message id="Lms.Admin.Database.daily">Daily</message> <message id="Lms.Admin.Database.daily">Daily</message>
<message id="Lms.Admin.Database.database">Music collection</message> <message id="Lms.Admin.Database.database">Music collection</message>
<message id="Lms.Admin.Database.extra-tags-to-scan">Extra tags to scan (use <kbd>;</kbd> as separator)</message>
<message id="Lms.Admin.Database.hourly">Hourly</message> <message id="Lms.Admin.Database.hourly">Hourly</message>
<message id="Lms.Admin.Database.immediate-scan">Scan now!</message> <message id="Lms.Admin.Database.immediate-scan">Scan now!</message>
<message id="Lms.Admin.Database.monthly">Monthly</message> <message id="Lms.Admin.Database.monthly">Monthly</message>
+2
View File
@@ -12,6 +12,7 @@
<message id="Lms.delete">Supprimer</message> <message id="Lms.delete">Supprimer</message>
<message id="Lms.discard">Annuler</message> <message id="Lms.discard">Annuler</message>
<message id="Lms.edit">Editer</message> <message id="Lms.edit">Editer</message>
<message id="Lms.field-must-be-in-upper-case">Ce champ doit être en majuscule</message>
<message id="Lms.loading">Chargement...</message> <message id="Lms.loading">Chargement...</message>
<message id="Lms.login">Login</message> <message id="Lms.login">Login</message>
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Quitter</message> <message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Quitter</message>
@@ -46,6 +47,7 @@
<!--Administration--> <!--Administration-->
<message id="Lms.Admin.Database.daily">Tous les jours</message> <message id="Lms.Admin.Database.daily">Tous les jours</message>
<message id="Lms.Admin.Database.database">Collection de musiques</message> <message id="Lms.Admin.Database.database">Collection de musiques</message>
<message id="Lms.Admin.Database.extra-tags-to-scan">Tags supplémentaires à scanner (utiliser <kbd>;</kbd> comme séparateur)</message>
<message id="Lms.Admin.Database.hourly">Toutes les heures</message> <message id="Lms.Admin.Database.hourly">Toutes les heures</message>
<message id="Lms.Admin.Database.immediate-scan">Scanner maintenant !</message> <message id="Lms.Admin.Database.immediate-scan">Scanner maintenant !</message>
<message id="Lms.Admin.Database.monthly">Tous les mois</message> <message id="Lms.Admin.Database.monthly">Tous les mois</message>
+2
View File
@@ -12,6 +12,7 @@
<message id="Lms.delete">Elimina</message> <message id="Lms.delete">Elimina</message>
<message id="Lms.discard">Annulla</message> <message id="Lms.discard">Annulla</message>
<message id="Lms.edit">Modifica</message> <message id="Lms.edit">Modifica</message>
<message id="Lms.loading">Caricamento...</message> <message id="Lms.loading">Caricamento...</message>
<message id="Lms.login">Login</message> <message id="Lms.login">Login</message>
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Logout</message> <message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Logout</message>
@@ -46,6 +47,7 @@
<!--Administration--> <!--Administration-->
<message id="Lms.Admin.Database.daily">Giornaliera</message> <message id="Lms.Admin.Database.daily">Giornaliera</message>
<message id="Lms.Admin.Database.database">Raccolta musicale</message> <message id="Lms.Admin.Database.database">Raccolta musicale</message>
<message id="Lms.Admin.Database.hourly">Ogni ora</message> <message id="Lms.Admin.Database.hourly">Ogni ora</message>
<message id="Lms.Admin.Database.immediate-scan">Scansiona ora!</message> <message id="Lms.Admin.Database.immediate-scan">Scansiona ora!</message>
<message id="Lms.Admin.Database.monthly">Mensile</message> <message id="Lms.Admin.Database.monthly">Mensile</message>
+2
View File
@@ -12,6 +12,7 @@
<message id="Lms.delete">删除</message> <message id="Lms.delete">删除</message>
<message id="Lms.discard">撤销</message> <message id="Lms.discard">撤销</message>
<message id="Lms.edit">编辑</message> <message id="Lms.edit">编辑</message>
<message id="Lms.loading">加载中...</message> <message id="Lms.loading">加载中...</message>
<message id="Lms.login">登陆</message> <message id="Lms.login">登陆</message>
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> 注销</message> <message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> 注销</message>
@@ -46,6 +47,7 @@
<!--Administration--> <!--Administration-->
<message id="Lms.Admin.Database.daily">每日</message> <message id="Lms.Admin.Database.daily">每日</message>
<message id="Lms.Admin.Database.database">音乐收藏</message> <message id="Lms.Admin.Database.database">音乐收藏</message>
<message id="Lms.Admin.Database.hourly">每小时</message> <message id="Lms.Admin.Database.hourly">每小时</message>
<message id="Lms.Admin.Database.immediate-scan">立即扫描!</message> <message id="Lms.Admin.Database.immediate-scan">立即扫描!</message>
<message id="Lms.Admin.Database.monthly">每月</message> <message id="Lms.Admin.Database.monthly">每月</message>
+183 -195
View File
@@ -29,234 +29,222 @@
namespace MetaData namespace MetaData
{ {
namespace
{
template <typename T>
std::optional<T> findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list<std::string> 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 <typename T> return StringUtils::readAs<T>(StringUtils::stringTrim(it->second));
std::optional<T> }
findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list<std::string> 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<T>(StringUtils::stringTrim(it->second)); template <>
} std::optional<std::vector<UUID>> findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list<std::string> tags)
{
std::optional<std::string> str{ findFirstValueOfAs<std::string>(metadataMap, tags) };
if (!str)
return std::nullopt;
template <> const std::vector<std::string_view> strUuids{ StringUtils::splitString(*str, "/") };
std::optional<std::vector<UUID>> std::vector<UUID> res;
findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list<std::string> tags)
{
std::optional<std::string> str {findFirstValueOfAs<std::string>(metadataMap, tags)};
if (!str)
return std::nullopt;
const std::vector<std::string_view> strUuids {StringUtils::splitString(*str, "/")}; for (std::string_view strUuid : strUuids)
std::vector<UUID> res; {
std::optional<UUID> uuid{ UUID::fromString(strUuid) };
if (!uuid)
return std::nullopt;
for (std::string_view strUuid : strUuids) res.push_back(std::move(*uuid));
{ }
std::optional<UUID> uuid {UUID::fromString(strUuid)};
if (!uuid)
return std::nullopt;
res.push_back(std::move(*uuid)); return res;
} }
return res; std::vector<Artist> getReleaseArtists(const Av::IAudioFile::MetadataMap& metadataMap)
} {
std::vector<Artist> res;
static auto name{ findFirstValueOfAs<std::string>(metadataMap, {"ALBUM_ARTIST"}) };
std::vector<Artist> if (!name)
getReleaseArtists(const Av::IAudioFile::MetadataMap& metadataMap) return res;
{
std::vector<Artist> res;
auto name {findFirstValueOfAs<std::string>(metadataMap, {"ALBUM_ARTIST"})}; auto mbid{ findFirstValueOfAs<UUID>(metadataMap, {"MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"}) };
if (!name)
return res;
auto mbid {findFirstValueOfAs<UUID>(metadataMap, {"MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"})}; return { Artist {mbid, *name, std::nullopt} };
}
return {Artist {mbid, *name, std::nullopt} }; std::vector<Artist> getArtists(const Av::IAudioFile::MetadataMap& metadataMap)
} {
std::vector<Artist> artists;
static std::vector<std::string_view> artistNames;
std::vector<Artist> if (metadataMap.find("ARTISTS") != metadataMap.end())
getArtists(const Av::IAudioFile::MetadataMap& metadataMap) {
{ artistNames = StringUtils::splitString(metadataMap.find("ARTISTS")->second, "/;");
std::vector<Artist> artists; }
else if (metadataMap.find("ARTIST") != metadataMap.end())
{
artistNames = { metadataMap.find("ARTIST")->second };
}
std::vector<std::string_view> artistNames; auto artistMBIDs{ findFirstValueOfAs<std::vector<UUID>>(metadataMap, {"MUSICBRAINZ ARTIST ID", "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ/ARTIST ID"}) };
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<std::vector<UUID>>(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) return artists;
{ }
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; std::optional<Release> getRelease(const Av::IAudioFile::MetadataMap& metadataMap)
} {
std::optional<Release> res;
static std::optional<std::string> releaseName{ findFirstValueOfAs<std::string>(metadataMap, {"ALBUM", "TALB", "WM/ALBUMTITLE"}) };
std::optional<Release> if (!releaseName)
getRelease(const Av::IAudioFile::MetadataMap& metadataMap) return res;
{
std::optional<Release> res;
std::optional<std::string> releaseName {findFirstValueOfAs<std::string>(metadataMap, {"ALBUM", "TALB", "WM/ALBUMTITLE"})}; res.emplace();
if (!releaseName) res->name = std::move(*releaseName);
return res; res->mbid = findFirstValueOfAs<UUID>(metadataMap, { "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ/ALBUM ID" });
res->artists = getReleaseArtists(metadataMap);
res->mediumCount = findFirstValueOfAs<std::size_t>(metadataMap, { "TOTALDISCS", "DISCTOTAL" });
if (!res->mediumCount)
{
// mediumCount may be encoded as position/count
if (const auto value{ findFirstValueOfAs<std::string>(metadataMap, {"TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET"}) })
{
// Expecting 'Number/Total'
const std::vector<std::string_view> strings{ StringUtils::splitString(*value, "/") };
if (strings.size() == 2)
res->mediumCount = StringUtils::readAs<std::size_t>(strings[1]);
}
}
res.emplace(); return res;
res->name = std::move(*releaseName); }
res->mbid = findFirstValueOfAs<UUID>(metadataMap, {"MUSICBRAINZ ALBUM ID", "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ/ALBUM ID"});
res->artists = getReleaseArtists(metadataMap);
res->mediumCount = findFirstValueOfAs<std::size_t>(metadataMap, {"TOTALDISCS", "DISCTOTAL"});
if (!res->mediumCount)
{
// mediumCount may be encoded as position/count
if (const auto value {findFirstValueOfAs<std::string>(metadataMap, {"TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET"})})
{
// Expecting 'Number/Total'
const std::vector<std::string_view> strings {StringUtils::splitString(*value, "/") };
if (strings.size() == 2)
res->mediumCount = StringUtils::readAs<std::size_t>(strings[1]);
}
}
return res; std::optional<Medium> getMedium(const Av::IAudioFile::MetadataMap& metadataMap)
} {
std::optional<Medium> res;
res.emplace();
static res->type = findFirstValueOfAs<std::string>(metadataMap, { "TMED", "MEDIA", "WM/MEDIA" }).value_or("");
std::optional<Medium> res->name = findFirstValueOfAs<std::string>(metadataMap, { "TSST", "DISCSUBTITLE", "SETSUBTITLE" }).value_or("");
getMedium(const Av::IAudioFile::MetadataMap& metadataMap) res->trackCount = findFirstValueOfAs<std::size_t>(metadataMap, { "TOTALTRACKS", "TRACKTOTAL" });
{ if (!res->trackCount)
std::optional<Medium> res; {
res.emplace(); // totalTracks may be encoded as "position/count"
if (const auto value{ findFirstValueOfAs<std::string>(metadataMap, {"TRCK", "TRACK", "TRACKNUMBER", "TRKN", "WM/TRACKNUMBER"}) })
{
// Expecting 'Number/Total'
const std::vector<std::string_view> strings{ StringUtils::splitString(*value, "/") };
if (strings.size() == 2)
res->trackCount = StringUtils::readAs<std::size_t>(strings[1]);
}
}
res->type = findFirstValueOfAs<std::string>(metadataMap, {"TMED", "MEDIA", "WM/MEDIA"}).value_or(""); // position may be encoded in TPOS/DISC/DISK as "position/count". Expecting 'Number[/Total]'
res->name = findFirstValueOfAs<std::string>(metadataMap, {"TSST", "DISCSUBTITLE", "SETSUBTITLE"}).value_or(""); res->position = findFirstValueOfAs<std::size_t>(metadataMap, { "TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET" });
res->trackCount = findFirstValueOfAs<std::size_t>(metadataMap, {"TOTALTRACKS", "TRACKTOTAL"}); res->release = getRelease(metadataMap);
if (!res->trackCount)
{
// totalTracks may be encoded as "position/count"
if (const auto value {findFirstValueOfAs<std::string>(metadataMap, {"TRCK", "TRACK", "TRACKNUMBER", "TRKN", "WM/TRACKNUMBER"})})
{
// Expecting 'Number/Total'
const std::vector<std::string_view> strings {StringUtils::splitString(*value, "/") };
if (strings.size() == 2)
res->trackCount = StringUtils::readAs<std::size_t>(strings[1]);
}
}
// position may be encoded in TPOS/DISC/DISK as "position/count". Expecting 'Number[/Total]' if (res->type.empty()
res->position = findFirstValueOfAs<std::size_t>(metadataMap, {"TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET"}); && res->name.empty()
res->release = getRelease(metadataMap); && !res->trackCount
&& !res->position
&& !res->release
&& !res->replayGain)
{
res.reset();
}
if (res->type.empty() return res;
&& res->name.empty() }
&& !res->trackCount }
&& !res->position
&& !res->release
&& !res->replayGain)
{
res.reset();
}
return res; std::optional<Track> AvFormatParser::parse(const std::filesystem::path& p, bool debug)
} {
Track track;
std::optional<Track> try
AvFormatParser::parse(const std::filesystem::path& p, bool debug) {
{ const auto mediaFile{ Av::parseAudioFile(p) };
Track track;
try Av::ContainerInfo info{ mediaFile->getContainerInfo() };
{ track.duration = info.duration;
const auto mediaFile {Av::parseAudioFile(p)}; track.bitrate = info.bitrate;
track.hasCover = mediaFile->hasAttachedPictures();
Av::ContainerInfo info{ mediaFile->getContainerInfo() }; MetaData::Tags tags;
track.duration = info.duration;
track.bitrate = info.bitrate;
track.hasCover = mediaFile->hasAttachedPictures();
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); for (const auto& [tag, value] : metadataMap)
track.medium = getMedium(metadataMap); {
if (debug)
std::cout << "TAG = " << tag << ", VAL = " << value << std::endl;
for (const auto& [tag, value] : metadataMap) if (tag == "TITLE")
{ track.title = value;
if (debug) else if (tag == "TRACK")
std::cout << "TAG = " << tag << ", VAL = " << value << std::endl; {
// Expecting 'Number/Total'
track.position = StringUtils::readAs<std::size_t>(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<std::string_view> tagValues{ StringUtils::splitString(value, "/,;") };
if (tag == "TITLE") if (!tagValues.empty())
track.title = value; {
else if (tag == "TRACK") std::set<std::string> values;
{ std::transform(std::cbegin(tagValues), std::cend(tagValues), std::inserter(values, std::begin(values)), [](std::string_view v) { return std::string{ v }; });
// Expecting 'Number/Total' track.tags[tag] = std::move(values);
track.position = StringUtils::readAs<std::size_t>(value); }
} }
else if (tag == "DATE" }
|| tag == "YEAR" }
|| tag == "WM/YEAR") catch (Av::Exception& e)
{ {
track.date = Utils::parseDate(value); return std::nullopt;
} }
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<std::string_view> clusterNames {StringUtils::splitString(value, "/,;")};
if (!clusterNames.empty()) return track;
{ }
std::set<std::string> 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;
}
} // namespace MetaData } // namespace MetaData
+6 -6
View File
@@ -368,18 +368,18 @@ namespace MetaData
track.replayGain = StringUtils::readAs<float>(value); track.replayGain = StringUtils::readAs<float>(value);
else if (tag == "ARTIST") else if (tag == "ARTIST")
track.artistDisplayName = value; 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<std::string> clusterNames; std::set<std::string> tagValues;
for (std::string_view valueList : values) for (std::string_view valueList : values)
{ {
const std::vector<std::string_view> splittedValues{ splitAndTrimString(valueList, "/,;") }; const std::vector<std::string_view> splittedValues{ splitAndTrimString(valueList, "/,;") }; // handle possibily bad split tags
for (std::string_view value : splittedValues) for (std::string_view value : splittedValues)
clusterNames.insert(std::string{ value }); tagValues.insert(std::string{ value });
} }
if (!clusterNames.empty()) if (!tagValues.empty())
track.tags[tag] = std::move(clusterNames); track.tags[tag] = std::move(tagValues);
} }
} }
@@ -34,7 +34,7 @@
namespace MetaData namespace MetaData
{ {
using Tags = std::map<std::string /* type */, std::set<std::string> /* names */>; using Tags = std::map<std::string /* type */, std::set<std::string> /* values */>;
// Very simplified version of https://musicbrainz.org/doc/MusicBrainz_Database/Schema // Very simplified version of https://musicbrainz.org/doc/MusicBrainz_Database/Schema
@@ -131,10 +131,10 @@ namespace MetaData
virtual std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) = 0; virtual std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) = 0;
void setClusterTypeNames(const std::set<std::string>& clusterTypeNames) { _clusterTypeNames = clusterTypeNames; } void setExtraTags(const std::vector<std::string>& extraTags) { _extraTags = std::set(extraTags.cbegin(), extraTags.cend()); }
protected: protected:
std::set<std::string> _clusterTypeNames; std::set<std::string> _extraTags;
}; };
enum class ParserType enum class ParserType
+3 -3
View File
@@ -288,7 +288,7 @@ namespace Database
return Utils::execQuery<ArtistId>(query, range); return Utils::execQuery<ArtistId>(query, range);
} }
std::vector<std::vector<Cluster::pointer>> Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const std::vector<std::vector<Cluster::pointer>> Artist::getClusterGroups(std::vector<ClusterTypeId> clusterTypeIds, std::size_t size) const
{ {
assert(session()); assert(session());
@@ -300,8 +300,8 @@ namespace Database
where.And(WhereClause("a.id = ?")).bind(getId().toString()); where.And(WhereClause("a.id = ?")).bind(getId().toString());
{ {
WhereClause clusterClause; WhereClause clusterClause;
for (auto clusterType : clusterTypes) for (ClusterTypeId clusterTypeId : clusterTypeIds)
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString()); clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString());
where.And(clusterClause); where.And(clusterClause);
} }
+7 -2
View File
@@ -44,14 +44,19 @@ namespace Database
query.join("track_cluster t_c ON t_c.cluster_id = c.id"); query.join("track_cluster t_c ON t_c.cluster_id = c.id");
query.join("track t ON t.id = t_c.track_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()) if (params.track.isValid())
query.where("t.id = ?").bind(params.track); query.where("t.id = ?").bind(params.track);
if (params.release.isValid()) if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release); query.where("t.release_id = ?").bind(params.release);
assert(!params.clusterType.isValid() || params.clusterTypeName.empty());
if (params.clusterType.isValid()) if (params.clusterType.isValid())
query.where("c.cluster_type_id = ?").bind(params.clusterType); query.where("c.cluster_type_id = ?").bind(params.clusterType);
else if (!params.clusterTypeName.empty())
query.where("c_t.name = ?").bind(params.clusterTypeName);
return query; return query;
} }
@@ -170,7 +175,7 @@ namespace Database
} }
RangeResults<ClusterTypeId> ClusterType::findOrphans(Session& session, std::optional<Range> range) RangeResults<ClusterTypeId> ClusterType::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -207,7 +212,7 @@ namespace Database
return session.getDboSession().find<ClusterType>().where("id = ?").bind(id).resultValue(); return session.getDboSession().find<ClusterType>().where("id = ?").bind(id).resultValue();
} }
RangeResults<ClusterTypeId> ClusterType::find(Session& session, std::optional<Range> range) RangeResults<ClusterTypeId> ClusterType::findIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -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<int>(/*User::defaultSubsonicEnableTranscodingByDefault*/0)) + ")"); session.getDboSession().execute("ALTER TABLE user ADD subsonic_enable_transcoding_by_default INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*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) void doDbMigration(Session& session)
{ {
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; 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}, {43, migrateFromV43},
{44, migrateFromV44}, {44, migrateFromV44},
{45, migrateFromV45}, {45, migrateFromV45},
{46, migrateFromV46},
}; };
{ {
@@ -26,7 +26,7 @@ namespace Database
class Session; class Session;
using Version = std::size_t; using Version = std::size_t;
static constexpr Version LMS_DATABASE_VERSION{ 46 }; static constexpr Version LMS_DATABASE_VERSION{ 47 };
class VersionInfo class VersionInfo
{ {
public: public:
+3 -3
View File
@@ -481,7 +481,7 @@ namespace Database
return query.resultValue(); return query.resultValue();
} }
std::vector<std::vector<Cluster::pointer>> Release::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const std::vector<std::vector<Cluster::pointer>> Release::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const
{ {
assert(session()); assert(session());
@@ -494,8 +494,8 @@ namespace Database
where.And(WhereClause("r.id = ?")).bind(getId().toString()); where.And(WhereClause("r.id = ?")).bind(getId().toString());
{ {
WhereClause clusterClause; WhereClause clusterClause;
for (auto clusterType : clusterTypes) for (const ClusterTypeId clusterTypeId : clusterTypeIds)
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString()); clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString());
where.And(clusterClause); where.And(clusterClause);
} }
oss << " " << where.get(); oss << " " << where.get();
@@ -30,30 +30,6 @@
namespace Database namespace Database
{ {
namespace
{
const std::set<std::string_view> 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<ScanSettings>());
settings.modify()->setClusterTypes(session, defaultClusterTypeNames);
}
ScanSettings::pointer ScanSettings::get(Session& session) ScanSettings::pointer ScanSettings::get(Session& session)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -77,9 +53,9 @@ namespace Database
_audioFileExtensions += " " + ext.string(); _audioFileExtensions += " " + ext.string();
} }
std::vector<ClusterType::pointer> ScanSettings::getClusterTypes() const std::vector<std::string_view> ScanSettings::getExtraTagsToScan() const
{ {
return std::vector<ClusterType::pointer>(std::cbegin(_clusterTypes), std::cend(_clusterTypes)); return StringUtils::splitString(_extraTagsToScan, ";");
} }
void ScanSettings::setMediaDirectory(const std::filesystem::path& p) void ScanSettings::setMediaDirectory(const std::filesystem::path& p)
@@ -87,58 +63,17 @@ namespace Database
_mediaDirectory = StringUtils::stringTrimEnd(p.string(), "/\\"); _mediaDirectory = StringUtils::stringTrimEnd(p.string(), "/\\");
} }
template <typename It> void ScanSettings::setExtraTagsToScan(const std::vector<std::string_view>& extraTags)
std::set<std::string> getNames(It begin, It end)
{ {
std::set<std::string> names; std::string newTagsToScan{ StringUtils::joinStrings(extraTags, ";") };
std::transform(begin, end, std::inserter(names, std::cbegin(names)), if (newTagsToScan != _extraTagsToScan)
[](const ClusterType::pointer& clusterType) incScanVersion();
{
return clusterType->getName();
});
return names; _extraTagsToScan = std::move(newTagsToScan);
} }
void ScanSettings::setClusterTypes(Session& session, const std::set<std::string_view>& clusterTypeNames) void ScanSettings::incScanVersion()
{
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<ClusterType>(clusterTypeName);
_clusterTypes.insert(getDboPtr(clusterType));
needRescan = true;
}
}
// Delete no longer existing cluster types
for (Wt::Dbo::ptr<ClusterType> 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()
{ {
_scanVersion += 1; _scanVersion += 1;
} }
} // namespace Database } // namespace Database
@@ -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_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)"); _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() void Session::analyze()
+8 -8
View File
@@ -67,11 +67,11 @@ WhereClause::get() const
} }
WhereClause& WhereClause&
WhereClause::bind(const std::string& bindArg) WhereClause::bind(std::string_view bindArg)
{ {
assert(_bindArgs.size() < static_cast<std::size_t>(std::count(_clause.begin(), _clause.end(), '?'))); assert(_bindArgs.size() < static_cast<std::size_t>(std::count(_clause.begin(), _clause.end(), '?')));
_bindArgs.push_back(bindArg); _bindArgs.push_back(std::string{ bindArg });
return *this; return *this;
} }
@@ -102,8 +102,8 @@ SelectStatement::And(const std::string& statement)
{ {
_statement.push_back(statement); _statement.push_back(statement);
_statement.sort(); std::sort(_statement.begin(), _statement.end());
_statement.unique(); _statement.erase(std::unique(_statement.begin(), _statement.end()), _statement.end());
return *this; return *this;
} }
@@ -113,7 +113,7 @@ SelectStatement::get() const
{ {
std::string res = "SELECT "; std::string res = "SELECT ";
for (std::list<std::string>::const_iterator it = _statement.begin(); it != _statement.end(); ++it) for (auto it = _statement.begin(); it != _statement.end(); ++it)
{ {
if (it != _statement.begin()) if (it != _statement.begin())
res += ","; res += ",";
@@ -150,8 +150,8 @@ FromClause::And(const FromClause& clause)
_clause.push_back(fromClause); _clause.push_back(fromClause);
} }
_clause.sort(); std::sort(_clause.begin(), _clause.end());
_clause.unique(); _clause.erase(std::unique(_clause.begin(), _clause.end()), _clause.end());
return *this; return *this;
} }
@@ -164,7 +164,7 @@ FromClause::get() const
if (!_clause.empty()) if (!_clause.empty())
{ {
oss << "FROM "; oss << "FROM ";
for (std::list<std::string>::const_iterator it = _clause.begin(); it != _clause.end(); ++it) { for (auto it = _clause.begin(); it != _clause.end(); ++it) {
if (it != _clause.begin()) if (it != _clause.begin())
oss << ","; oss << ",";
+6 -6
View File
@@ -19,7 +19,7 @@
#pragma once #pragma once
#include <list> #include <vector>
#include <string> #include <string>
@@ -33,14 +33,14 @@ class WhereClause
WhereClause& Or(const WhereClause& clause); WhereClause& Or(const WhereClause& clause);
// Arguments binding (for each '?' in where clause) // Arguments binding (for each '?' in where clause)
WhereClause& bind(const std::string& arg); WhereClause& bind(std::string_view arg);
std::string get() const; std::string get() const;
const std::list<std::string>& getBindArgs() const {return _bindArgs;} const std::vector<std::string>& getBindArgs() const {return _bindArgs;}
private: private:
std::string _clause; // WHERE clause std::string _clause; // WHERE clause
std::list<std::string> _bindArgs; std::vector<std::string> _bindArgs;
}; };
class InnerJoinClause class InnerJoinClause
@@ -81,7 +81,7 @@ class SelectStatement
std::string get() const; std::string get() const;
private: private:
std::list<std::string> _statement; std::vector<std::string> _statement;
}; };
class FromClause class FromClause
@@ -95,7 +95,7 @@ class FromClause
std::string get() const; std::string get() const;
private: private:
std::list<std::string> _clause; std::vector<std::string> _clause;
}; };
class SqlQuery class SqlQuery
+5 -5
View File
@@ -479,7 +479,7 @@ namespace Database
return std::vector<TrackArtistLink::pointer>(_trackArtistLinks.begin(), _trackArtistLinks.end()); return std::vector<TrackArtistLink::pointer>(_trackArtistLinks.begin(), _trackArtistLinks.end());
} }
std::vector<std::vector<Cluster::pointer>> Track::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const std::vector<std::vector<Cluster::pointer>> Track::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const
{ {
assert(self()); assert(self());
assert(session()); assert(session());
@@ -493,8 +493,8 @@ namespace Database
where.And(WhereClause("t.id = ?")).bind(getId().toString()); where.And(WhereClause("t.id = ?")).bind(getId().toString());
{ {
WhereClause clusterClause; WhereClause clusterClause;
for (auto clusterType : clusterTypes) for (ClusterTypeId clusterTypeId : clusterTypeIds)
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString()); clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString());
where.And(clusterClause); where.And(clusterClause);
} }
oss << " " << where.get(); oss << " " << where.get();
@@ -514,8 +514,8 @@ namespace Database
} }
std::vector<std::vector<Cluster::pointer>> res; std::vector<std::vector<Cluster::pointer>> res;
for (auto cluster_list : clusters) for (const auto& [type, clusters] : clusters)
res.push_back(cluster_list.second); res.push_back(clusters);
return res; return res;
} }
@@ -187,12 +187,12 @@ namespace Database
return std::vector<Cluster::pointer>(res.begin(), res.end()); return std::vector<Cluster::pointer>(res.begin(), res.end());
} }
std::vector<std::vector<Cluster::pointer>> TrackList::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const std::vector<std::vector<Cluster::pointer>> TrackList::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const
{ {
assert(session()); assert(session());
std::vector<std::vector<Cluster::pointer>> res; std::vector<std::vector<Cluster::pointer>> res;
if (clusterTypes.empty()) if (clusterTypeIds.empty())
return res; return res;
auto query{ session()->query<Wt::Dbo::ptr<Cluster>>("SELECT c from cluster c") }; auto query{ session()->query<Wt::Dbo::ptr<Cluster>>("SELECT c from cluster c") };
@@ -208,12 +208,12 @@ namespace Database
std::ostringstream oss; std::ostringstream oss;
oss << "c_type.id IN ("; oss << "c_type.id IN (";
bool first{ true }; bool first{ true };
for (auto clusterType : clusterTypes) for (ClusterTypeId clusterTypeId : clusterTypeIds)
{ {
if (!first) if (!first)
oss << ", "; oss << ", ";
oss << "?"; oss << "?";
query.bind(clusterType->getId()); query.bind(clusterTypeId);
first = false; first = false;
} }
oss << ")"; oss << ")";
@@ -100,7 +100,7 @@ namespace Database
// Get the cluster of the tracks made by this artist // Get the cluster of the tracks made by this artist
// Each clusters are grouped by cluster type, sorted by the number of occurence // Each clusters are grouped by cluster type, sorted by the number of occurence
// size is the max number of cluster per cluster type // size is the max number of cluster per cluster type
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(std::vector<ObjectPtr<ClusterType>> clusterTypes, std::size_t size) const; std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(std::vector<ClusterTypeId> clusterTypeIds, std::size_t size) const;
void setName(std::string_view name) { _name = name; } void setName(std::string_view name) { _name = name; }
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; } void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
@@ -38,7 +38,6 @@ namespace Database {
class Track; class Track;
class ClusterType; class ClusterType;
class ScanSettings;
class Session; class Session;
class Cluster final : public Object<Cluster, ClusterId> class Cluster final : public Object<Cluster, ClusterId>
@@ -47,12 +46,14 @@ namespace Database {
struct FindParameters struct FindParameters
{ {
std::optional<Range> range; std::optional<Range> 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 TrackId track; // if set, clusters involved in this track
ReleaseId release; // if set, clusters involved in this release ReleaseId release; // if set, clusters involved in this release
FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; } FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
FindParameters& setClusterType(ClusterTypeId _clusterType) { clusterType = _clusterType; 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& setTrack(TrackId _track) { track = _track; return *this; }
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; } FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
}; };
@@ -117,10 +118,10 @@ namespace Database {
// Getters // Getters
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static RangeResults<ClusterTypeId> find(Session& session, std::optional<Range> range = std::nullopt); static RangeResults<ClusterTypeId> findIds(Session& session, std::optional<Range> range = std::nullopt);
static pointer find(Session& session, std::string_view name); static pointer find(Session& session, std::string_view name);
static pointer find(Session& session, ClusterTypeId id); static pointer find(Session& session, ClusterTypeId id);
static RangeResults<ClusterTypeId> findOrphans(Session& session, std::optional<Range> range = std::nullopt); static RangeResults<ClusterTypeId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
static RangeResults<ClusterTypeId> findUsed(Session& session, std::optional<Range> range = std::nullopt); static RangeResults<ClusterTypeId> findUsed(Session& session, std::optional<Range> range = std::nullopt);
static void remove(Session& session, const std::string& name); static void remove(Session& session, const std::string& name);
@@ -135,7 +136,6 @@ namespace Database {
{ {
Wt::Dbo::field(a, _name, "name"); Wt::Dbo::field(a, _name, "name");
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToOne, "cluster_type"); Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToOne, "cluster_type");
Wt::Dbo::belongsTo(a, _scanSettings, "scan_settings", Wt::Dbo::OnDeleteCascade);
} }
private: private:
@@ -147,7 +147,6 @@ namespace Database {
std::string _name; std::string _name;
Wt::Dbo::collection< Wt::Dbo::ptr<Cluster> > _clusters; Wt::Dbo::collection< Wt::Dbo::ptr<Cluster> > _clusters;
Wt::Dbo::ptr<ScanSettings> _scanSettings;
}; };
} // namespace Database } // namespace Database
@@ -99,7 +99,7 @@ namespace Database
// Get the cluster of the tracks that belong to this release // 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) // 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 // size is the max number of cluster per cluster type
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const; std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const;
// Utility functions (if all tracks have the same values, which is legit to not be the case) // Utility functions (if all tracks have the same values, which is legit to not be the case)
Wt::WDate getReleaseDate() const; Wt::WDate getReleaseDate() const;
@@ -32,9 +32,8 @@
LMS_DECLARE_IDTYPE(ScanSettingsId) LMS_DECLARE_IDTYPE(ScanSettingsId)
namespace Database { namespace Database
{
class ClusterType;
class Session; class Session;
class ScanSettings final : public Object<ScanSettings, ScanSettingsId> class ScanSettings final : public Object<ScanSettings, ScanSettingsId>
@@ -67,7 +66,7 @@ namespace Database {
std::filesystem::path getMediaDirectory() const { return _mediaDirectory; } std::filesystem::path getMediaDirectory() const { return _mediaDirectory; }
Wt::WTime getUpdateStartTime() const { return _startTime; } Wt::WTime getUpdateStartTime() const { return _startTime; }
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; } UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
std::vector<ObjectPtr<ClusterType>> getClusterTypes() const; std::vector<std::string_view> getExtraTagsToScan() const;
std::vector<std::filesystem::path> getAudioFileExtensions() const; std::vector<std::filesystem::path> getAudioFileExtensions() const;
SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; } SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
@@ -76,7 +75,7 @@ namespace Database {
void setMediaDirectory(const std::filesystem::path& p); void setMediaDirectory(const std::filesystem::path& p);
void setUpdateStartTime(Wt::WTime t) { _startTime = t; } void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; } void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
void setClusterTypes(Session& session, const std::set<std::string_view>& clusterTypeNames); void setExtraTagsToScan(const std::vector<std::string_view>& extraTags);
void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; } void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
void incScanVersion(); void incScanVersion();
@@ -89,20 +88,17 @@ namespace Database {
Wt::Dbo::field(a, _updatePeriod, "update_period"); Wt::Dbo::field(a, _updatePeriod, "update_period");
Wt::Dbo::field(a, _audioFileExtensions, "audio_file_extensions"); Wt::Dbo::field(a, _audioFileExtensions, "audio_file_extensions");
Wt::Dbo::field(a, _similarityEngineType, "similarity_engine_type"); 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: private:
int _scanVersion{}; int _scanVersion{};
std::string _mediaDirectory; std::string _mediaDirectory;
Wt::WTime _startTime = Wt::WTime{ 0,0,0 }; Wt::WTime _startTime = Wt::WTime{ 0,0,0 };
UpdatePeriod _updatePeriod{ UpdatePeriod::Never }; UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
SimilarityEngineType _similarityEngineType{ SimilarityEngineType::Clusters }; SimilarityEngineType _similarityEngineType{ SimilarityEngineType::Clusters };
std::string _audioFileExtensions{ ".alac .mp3 .ogg .oga .aac .m4a .m4b .flac .wav .wma .aif .aiff .ape .mpc .shn .opus .wv" }; std::string _audioFileExtensions{ ".alac .mp3 .ogg .oga .aac .m4a .m4b .flac .wav .wma .aif .aiff .ape .mpc .shn .opus .wv" };
std::string _extraTagsToScan;
Wt::Dbo::collection<Wt::Dbo::ptr<ClusterType>> _clusterTypes;
}; };
} // namespace Database } // namespace Database
@@ -176,7 +176,7 @@ namespace Database {
std::vector<ObjectPtr<Cluster>> getClusters() const; std::vector<ObjectPtr<Cluster>> getClusters() const;
std::vector<ClusterId> getClusterIds() const; std::vector<ClusterId> getClusterIds() const;
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const; std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypes, std::size_t size) const;
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
@@ -99,7 +99,7 @@ namespace Database {
// Get clusters, order by occurence // Get clusters, order by occurence
std::vector<ObjectPtr<Cluster>> getClusters() const; std::vector<ObjectPtr<Cluster>> getClusters() const;
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const; std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const;
bool hasTrack(TrackId trackId) const; bool hasTrack(TrackId trackId) const;
+39 -7
View File
@@ -60,7 +60,7 @@ TEST_F(DatabaseFixture, Cluster)
EXPECT_EQ(clusters.results.front(), cluster.getId()); EXPECT_EQ(clusters.results.front(), cluster.getId());
} }
auto clusterTypes{ ClusterType::find(session) }; auto clusterTypes{ ClusterType::findIds(session) };
ASSERT_EQ(clusterTypes.results.size(), 1); ASSERT_EQ(clusterTypes.results.size(), 1);
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
@@ -68,7 +68,7 @@ TEST_F(DatabaseFixture, Cluster)
ASSERT_EQ(clusterTypes.results.size(), 1); ASSERT_EQ(clusterTypes.results.size(), 1);
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
clusterTypes = ClusterType::findOrphans(session); clusterTypes = ClusterType::findOrphanIds(session);
EXPECT_TRUE(clusterTypes.results.empty()); EXPECT_TRUE(clusterTypes.results.empty());
} }
} }
@@ -76,7 +76,7 @@ TEST_F(DatabaseFixture, Cluster)
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
auto clusterTypes{ ClusterType::findOrphans(session) }; auto clusterTypes{ ClusterType::findOrphanIds(session) };
ASSERT_EQ(clusterTypes.results.size(), 1); ASSERT_EQ(clusterTypes.results.size(), 1);
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
@@ -92,7 +92,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
EXPECT_TRUE(Cluster::findOrphanIds(session).results.empty()); EXPECT_TRUE(Cluster::findOrphanIds(session).results.empty());
auto clusterTypes{ ClusterType::findOrphans(session) }; auto clusterTypes{ ClusterType::findOrphanIds(session) };
ASSERT_EQ(clusterTypes.results.size(), 1); ASSERT_EQ(clusterTypes.results.size(), 1);
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
} }
@@ -131,7 +131,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
ASSERT_EQ(clusters.results.size(), 1); ASSERT_EQ(clusters.results.size(), 1);
EXPECT_EQ(clusters.results.front(), cluster2.getId()); 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) TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
{ {
ScopedTrack track{ session, "MyTrackFile" }; ScopedTrack track{ session, "MyTrackFile" };
@@ -329,7 +361,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters)
{ {
auto transaction{ session.createReadTransaction() }; 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_EQ(Cluster::findOrphanIds(session).results.size(), 2);
EXPECT_TRUE(Release::findOrphanIds(session).results.empty()); EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
EXPECT_TRUE(Artist::findOrphanIds(session).results.empty()); EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
@@ -542,7 +574,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster)
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
EXPECT_TRUE(Cluster::findOrphanIds(session).results.empty()); 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(Artist::findOrphanIds(session).results.empty());
EXPECT_TRUE(Release::findOrphanIds(session).results.empty()); EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
} }
@@ -59,17 +59,6 @@ void DatabaseFixture::SetUpTestCase()
Database::Session s{ _tmpDb->getDb() }; Database::Session s{ _tmpDb->getDb() };
s.prepareTables(); s.prepareTables();
s.analyze(); 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();
}
}
} }
} }
@@ -151,7 +151,6 @@ TEST_F(DatabaseFixture, Release_findByNameAndPath)
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
std::cout << "OK HERE" << std::endl;
{ {
const auto releases{ Release::find(session, "MyRelease", "/tmp/foo") }; const auto releases{ Release::find(session, "MyRelease", "/tmp/foo") };
ASSERT_EQ(releases.size(), 1); ASSERT_EQ(releases.size(), 1);
@@ -65,7 +65,7 @@ namespace Recommendation
const auto& refVector = network.getRefVector({ x, y }); const auto& refVector = network.getRefVector({ x, y });
boost::property_tree::ptree node; boost::property_tree::ptree node;
for (auto value : refVector) for (const auto& value : refVector)
node.add("values.value", value); node.add("values.value", value);
node.put("coord_x", x); node.put("coord_x", x);
@@ -74,6 +74,7 @@ namespace Scanner
{ {
removeOrphanTracks(context); removeOrphanTracks(context);
removeOrphanClusters(); removeOrphanClusters();
removeOrphanClusterTypes();
removeOrphanArtists(); removeOrphanArtists();
removeOrphanReleases(); removeOrphanReleases();
} }
@@ -152,6 +153,12 @@ namespace Scanner
removeOrphanEntries<Database::Cluster>(_db.getTLSSession(), _abortScan); removeOrphanEntries<Database::Cluster>(_db.getTLSSession(), _abortScan);
} }
void ScanStepRemoveOrphanDbFiles::removeOrphanClusterTypes()
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan cluster types...");
removeOrphanEntries<Database::ClusterType>(_db.getTLSSession(), _abortScan);
}
void ScanStepRemoveOrphanDbFiles::removeOrphanArtists() void ScanStepRemoveOrphanDbFiles::removeOrphanArtists()
{ {
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan artists..."); LMS_LOG(DBUPDATER, DEBUG, "Checking orphan artists...");
@@ -37,6 +37,7 @@ namespace Scanner
void removeOrphanTracks(ScanContext& context); void removeOrphanTracks(ScanContext& context);
void removeOrphanClusters(); void removeOrphanClusters();
void removeOrphanClusterTypes();
void removeOrphanArtists(); void removeOrphanArtists();
void removeOrphanReleases(); void removeOrphanReleases();
bool checkFile(const std::filesystem::path& p); bool checkFile(const std::filesystem::path& p);
@@ -241,9 +241,9 @@ namespace Scanner
{ {
auto clusterType = ClusterType::find(session, tag); auto clusterType = ClusterType::find(session, tag);
if (!clusterType) if (!clusterType)
continue; clusterType = session.create<ClusterType>(tag);
for (auto clusterName : values) for (const auto& clusterName : values)
{ {
auto cluster = clusterType->getCluster(clusterName); auto cluster = clusterType->getCluster(clusterName);
if (!cluster) if (!cluster)
@@ -279,7 +279,10 @@ namespace Scanner
void ScanStepScanFiles::process(ScanContext& context) void ScanStepScanFiles::process(ScanContext& context)
{ {
_metadataParser->setClusterTypeNames(_settings.clusterTypeNames); std::vector<std::string> tagsToParse{ _tagsToParse };
tagsToParse.insert(std::end(tagsToParse), std::cbegin(_settings.extraTags), std::cend(_settings.extraTags));
_metadataParser->setExtraTags(tagsToParse);
context.currentStepStats.totalElems = context.stats.filesScanned; context.currentStepStats.totalElems = context.stats.filesScanned;
@@ -20,24 +20,27 @@
#pragma once #pragma once
#include <filesystem> #include <filesystem>
#include <string>
#include <vector>
#include "metadata/IParser.hpp" #include "metadata/IParser.hpp"
#include "ScanStepBase.hpp" #include "ScanStepBase.hpp"
namespace Scanner namespace Scanner
{ {
class ScanStepScanFiles : public ScanStepBase class ScanStepScanFiles : public ScanStepBase
{ {
public: public:
ScanStepScanFiles(InitParams& initParams); ScanStepScanFiles(InitParams& initParams);
private: private:
ScanStep getStep() const override { return ScanStep::ScanningFiles; } ScanStep getStep() const override { return ScanStep::ScanningFiles; }
std::string_view getStepName() const override { return "Scanning files"; } std::string_view getStepName() const override { return "Scanning files"; }
void process(ScanContext& context) override; 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<MetaData::IParser> _metadataParser; std::unique_ptr<MetaData::IParser> _metadataParser;
}; const std::vector<std::string> _tagsToParse{ "GENRE", "MOOD", "LANGUAGE", "ALBUMGROUPING" };
};
} }
@@ -361,14 +361,10 @@ namespace Scanner
} }
newSettings.mediaDirectory = scanSettings->getMediaDirectory(); newSettings.mediaDirectory = scanSettings->getMediaDirectory();
const auto clusterTypes = scanSettings->getClusterTypes(); {
std::set<std::string> 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 };});
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);
} }
return newSettings; return newSettings;
@@ -20,7 +20,6 @@
#pragma once #pragma once
#include <filesystem> #include <filesystem>
#include <set>
#include <string> #include <string>
#include <vector> #include <vector>
#include <Wt/WDateTime.h> #include <Wt/WDateTime.h>
@@ -36,7 +35,7 @@ namespace Scanner
std::vector<std::filesystem::path> supportedExtensions; std::vector<std::filesystem::path> supportedExtensions;
std::filesystem::path mediaDirectory; std::filesystem::path mediaDirectory;
bool skipDuplicateMBID {}; bool skipDuplicateMBID {};
std::set<std::string> clusterTypeNames; std::vector<std::string> extraTags;
bool operator==(const ScannerSettings& rhs) const bool operator==(const ScannerSettings& rhs) const
{ {
@@ -46,7 +45,7 @@ namespace Scanner
&& supportedExtensions == rhs.supportedExtensions && supportedExtensions == rhs.supportedExtensions
&& mediaDirectory == rhs.mediaDirectory && mediaDirectory == rhs.mediaDirectory
&& skipDuplicateMBID == rhs.skipDuplicateMBID && skipDuplicateMBID == rhs.skipDuplicateMBID
&& clusterTypeNames == rhs.clusterTypeNames; && extraTags == rhs.extraTags;
} }
}; };
} }
+6 -10
View File
@@ -131,7 +131,7 @@ namespace API::Subsonic
const ClusterType::pointer genreClusterType{ ClusterType::find(context.dbSession, "GENRE") }; const ClusterType::pointer genreClusterType{ ClusterType::find(context.dbSession, "GENRE") };
if (genreClusterType) if (genreClusterType)
{ {
auto clusters{ release->getClusterGroups({genreClusterType}, 1) }; auto clusters{ release->getClusterGroups({genreClusterType->getId()}, 1) };
if (!clusters.empty() && !clusters.front().empty()) if (!clusters.empty() && !clusters.front().empty())
albumNode.setAttribute("genre", clusters.front().front()->getName()); albumNode.setAttribute("genre", clusters.front().front()->getName());
} }
@@ -160,16 +160,12 @@ namespace API::Subsonic
{ {
albumNode.createEmptyArrayValue(field); albumNode.createEmptyArrayValue(field);
ClusterType::pointer clusterType{ ClusterType::find(context.dbSession, clusterTypeName) }; Cluster::FindParameters params;
if (clusterType) params.setRelease(release->getId());
{ params.setClusterTypeName(clusterTypeName);
Cluster::FindParameters params;
params.setRelease(release->getId());
params.setClusterType(clusterType->getId());
for (const auto& cluster : Cluster::find(context.dbSession, params).results) for (const auto& cluster : Cluster::find(context.dbSession, params).results)
albumNode.addArrayValue(field, cluster->getName()); albumNode.addArrayValue(field, cluster->getName());
}
} }; } };
addClusters("moods", "MOOD"); addClusters("moods", "MOOD");
+15 -23
View File
@@ -162,12 +162,15 @@ namespace API::Subsonic
trackResponse.setAttribute("starred", StringUtils::toISO8601String(dateTime)); trackResponse.setAttribute("starred", StringUtils::toISO8601String(dateTime));
// Report the first GENRE for this track // Report the first GENRE for this track
const ClusterType::pointer genreClusterType{ ClusterType::find(context.dbSession, "GENRE") }; std::vector<Cluster::pointer> genres;
if (genreClusterType)
{ {
auto clusters{ track->getClusterGroups({genreClusterType}, 1) }; Cluster::FindParameters params;
if (!clusters.empty() && !clusters.front().empty()) params.setTrack(track->getId());
trackResponse.setAttribute("genre", clusters.front().front()->getName()); 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) // OpenSubsonic specific fields (must always be set)
@@ -227,31 +230,20 @@ namespace API::Subsonic
{ {
trackResponse.createEmptyArrayValue(field); trackResponse.createEmptyArrayValue(field);
ClusterType::pointer clusterType{ ClusterType::find(context.dbSession, clusterTypeName) }; Cluster::FindParameters params;
if (clusterType) params.setTrack(track->getId());
{ params.setClusterTypeName(clusterTypeName);
Cluster::FindParameters params;
params.setTrack(track->getId());
params.setClusterType(clusterType->getId());
for (const auto& cluster : Cluster::find(context.dbSession, params).results) for (const auto& cluster : Cluster::find(context.dbSession, params).results)
trackResponse.addArrayValue(field, cluster->getName()); trackResponse.addArrayValue(field, cluster->getName());
}
} }; } };
addClusters("moods", "MOOD"); addClusters("moods", "MOOD");
// Genres // Genres
trackResponse.createEmptyArrayChild("genres"); trackResponse.createEmptyArrayChild("genres");
if (genreClusterType) for (const auto& genre : genres)
{ trackResponse.addArrayChild("genres", createItemGenreNode(genre->getName()));
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()));
}
trackResponse.addChild("replayGain", createReplayGainNode(track)); trackResponse.addChild("replayGain", createReplayGainNode(track));
+1
View File
@@ -24,6 +24,7 @@ add_executable(lms
ui/common/MandatoryValidator.cpp ui/common/MandatoryValidator.cpp
ui/common/PasswordValidator.cpp ui/common/PasswordValidator.cpp
ui/common/Template.cpp ui/common/Template.cpp
ui/common/UppercaseValidator.cpp
ui/common/UUIDValidator.cpp ui/common/UUIDValidator.cpp
ui/explore/ArtistCollector.cpp ui/explore/ArtistCollector.cpp
ui/explore/ArtistListHelpers.cpp ui/explore/ArtistListHelpers.cpp
+2 -1
View File
@@ -285,7 +285,8 @@ namespace UserInterface::Utils
std::unique_ptr<Wt::WContainerWidget> clusterContainer{ std::make_unique<Wt::WContainerWidget>() }; std::unique_ptr<Wt::WContainerWidget> clusterContainer{ std::make_unique<Wt::WContainerWidget>() };
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) }; const auto clusterGroups{ track->getClusterGroups(clusterTypes, 3) };
for (const auto& clusters : clusterGroups) for (const auto& clusters : clusterGroups)
+166 -171
View File
@@ -37,226 +37,221 @@
#include "common/DirectoryValidator.hpp" #include "common/DirectoryValidator.hpp"
#include "common/MandatoryValidator.hpp" #include "common/MandatoryValidator.hpp"
#include "common/UppercaseValidator.hpp"
#include "common/ValueStringModel.hpp" #include "common/ValueStringModel.hpp"
#include "ScannerController.hpp" #include "ScannerController.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
namespace UserInterface { namespace UserInterface
using namespace Database;
class DatabaseSettingsModel : public Wt::WFormModel
{ {
public: using namespace Database;
// 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 UpdatePeriodModel = ValueStringModel<ScanSettings::UpdatePeriod>; 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() using UpdatePeriodModel = ValueStringModel<ScanSettings::UpdatePeriod>;
{
initializeModels();
addField(MediaDirectoryField); static inline constexpr std::string_view extraTagsDelimiter{ ";" };
addField(UpdatePeriodField);
addField(UpdateStartTimeField);
addField(SimilarityEngineTypeField);
addField(ClustersField);
auto dirValidator {createDirectoryValidator()}; DatabaseSettingsModel()
dirValidator->setMandatory(true); {
setValidator(MediaDirectoryField, std::move(dirValidator)); initializeModels();
setValidator(UpdatePeriodField, createMandatoryValidator()); addField(MediaDirectoryField);
setValidator(UpdateStartTimeField, createMandatoryValidator()); addField(UpdatePeriodField);
setValidator(SimilarityEngineTypeField, createMandatoryValidator()); addField(UpdateStartTimeField);
addField(SimilarityEngineTypeField);
addField(ExtraTagsField);
// populate the model with initial data auto dirValidator{ createDirectoryValidator() };
loadData(); dirValidator->setMandatory(true);
} setValidator(MediaDirectoryField, std::move(dirValidator));
std::shared_ptr<UpdatePeriodModel> updatePeriodModel() { return _updatePeriodModel; } setValidator(UpdatePeriodField, createMandatoryValidator());
std::shared_ptr<Wt::WAbstractItemModel> updateStartTimeModel() { return _updateStartTimeModel; } setValidator(UpdateStartTimeField, createMandatoryValidator());
std::shared_ptr<Wt::WAbstractItemModel> similarityEngineTypeModel() { return _similarityEngineTypeModel; } setValidator(SimilarityEngineTypeField, createMandatoryValidator());
setValidator(ExtraTagsField, createUppercaseValidator(extraTagsDelimiter));
void loadData() // populate the model with initial data
{ loadData();
auto transaction {LmsApp->getDbSession().createReadTransaction()}; }
const ScanSettings::pointer scanSettings {ScanSettings::get(LmsApp->getDbSession())}; std::shared_ptr<UpdatePeriodModel> updatePeriodModel() { return _updatePeriodModel; }
std::shared_ptr<Wt::WAbstractItemModel> updateStartTimeModel() { return _updateStartTimeModel; }
std::shared_ptr<Wt::WAbstractItemModel> similarityEngineTypeModel() { return _similarityEngineTypeModel; }
setValue(MediaDirectoryField, scanSettings->getMediaDirectory().string()); void loadData()
{
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
auto periodRow {_updatePeriodModel->getRowFromValue(scanSettings->getUpdatePeriod())}; const ScanSettings::pointer scanSettings{ ScanSettings::get(LmsApp->getDbSession()) };
if (periodRow)
setValue(UpdatePeriodField, _updatePeriodModel->getString(*periodRow));
auto startTimeRow {_updateStartTimeModel->getRowFromValue(scanSettings->getUpdateStartTime())}; setValue(MediaDirectoryField, scanSettings->getMediaDirectory().string());
if (startTimeRow)
setValue(UpdateStartTimeField, _updateStartTimeModel->getString(*startTimeRow));
if (scanSettings->getUpdatePeriod() == ScanSettings::UpdatePeriod::Hourly auto periodRow{ _updatePeriodModel->getRowFromValue(scanSettings->getUpdatePeriod()) };
|| scanSettings->getUpdatePeriod() == ScanSettings::UpdatePeriod::Never) if (periodRow)
{ setValue(UpdatePeriodField, _updatePeriodModel->getString(*periodRow));
setReadOnly(DatabaseSettingsModel::UpdateStartTimeField, true);
}
auto similarityEngineTypeRow {_similarityEngineTypeModel->getRowFromValue(scanSettings->getSimilarityEngineType())}; auto startTimeRow{ _updateStartTimeModel->getRowFromValue(scanSettings->getUpdateStartTime()) };
if (similarityEngineTypeRow) if (startTimeRow)
setValue(SimilarityEngineTypeField, _similarityEngineTypeModel->getString(*similarityEngineTypeRow)); setValue(UpdateStartTimeField, _updateStartTimeModel->getString(*startTimeRow));
auto clusterTypes {scanSettings->getClusterTypes()}; if (scanSettings->getUpdatePeriod() == ScanSettings::UpdatePeriod::Hourly
if (!clusterTypes.empty()) || scanSettings->getUpdatePeriod() == ScanSettings::UpdatePeriod::Never)
{ {
std::vector<std::string_view> names; setReadOnly(DatabaseSettingsModel::UpdateStartTimeField, true);
std::transform(clusterTypes.begin(), clusterTypes.end(), std::back_inserter(names), [](const auto& clusterType) { return clusterType->getName(); }); }
setValue(ClustersField, StringUtils::joinStrings(names, " "));
}
}
void saveData() auto similarityEngineTypeRow{ _similarityEngineTypeModel->getRowFromValue(scanSettings->getSimilarityEngineType()) };
{ if (similarityEngineTypeRow)
auto transaction {LmsApp->getDbSession().createWriteTransaction()}; 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))}; ScanSettings::pointer scanSettings{ ScanSettings::get(LmsApp->getDbSession()) };
if (updatePeriodRow)
scanSettings.modify()->setUpdatePeriod(_updatePeriodModel->getValue(*updatePeriodRow));
auto startTimeRow {_updateStartTimeModel->getRowFromString(valueText(UpdateStartTimeField))}; scanSettings.modify()->setMediaDirectory(valueText(MediaDirectoryField).toUTF8());
if (startTimeRow)
scanSettings.modify()->setUpdateStartTime(_updateStartTimeModel->getValue(*startTimeRow));
auto similarityEngineTypeRow {_similarityEngineTypeModel->getRowFromString(valueText(SimilarityEngineTypeField))}; auto updatePeriodRow{ _updatePeriodModel->getRowFromString(valueText(UpdatePeriodField)) };
if (similarityEngineTypeRow) if (updatePeriodRow)
scanSettings.modify()->setSimilarityEngineType(_similarityEngineTypeModel->getValue(*similarityEngineTypeRow)); scanSettings.modify()->setUpdatePeriod(_updatePeriodModel->getValue(*updatePeriodRow));
const std::vector<std::string_view> clusterTypes {StringUtils::splitString(valueText(ClustersField).toUTF8(), " ")}; auto startTimeRow{ _updateStartTimeModel->getRowFromString(valueText(UpdateStartTimeField)) };
scanSettings.modify()->setClusterTypes(LmsApp->getDbSession(), std::set<std::string_view>(clusterTypes.begin(), clusterTypes.end())); if (startTimeRow)
} scanSettings.modify()->setUpdateStartTime(_updateStartTimeModel->getValue(*startTimeRow));
private: auto similarityEngineTypeRow{ _similarityEngineTypeModel->getRowFromString(valueText(SimilarityEngineTypeField)) };
void initializeModels() if (similarityEngineTypeRow)
{ scanSettings.modify()->setSimilarityEngineType(_similarityEngineTypeModel->getValue(*similarityEngineTypeRow));
_updatePeriodModel = std::make_shared<ValueStringModel<ScanSettings::UpdatePeriod>>();
_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);
_updateStartTimeModel = std::make_shared<ValueStringModel<Wt::WTime>>(); scanSettings.modify()->setExtraTagsToScan(StringUtils::splitString(valueText(ExtraTagsField).toUTF8(), extraTagsDelimiter));
for (std::size_t i = 0; i < 24; ++i) }
{
Wt::WTime time {static_cast<int>(i), 0};
_updateStartTimeModel->add(time.toString(), time);
}
_similarityEngineTypeModel = std::make_shared<ValueStringModel<ScanSettings::SimilarityEngineType>>(); private:
_similarityEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.similarity-engine-type.clusters"), ScanSettings::SimilarityEngineType::Clusters); void initializeModels()
_similarityEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.similarity-engine-type.none"), ScanSettings::SimilarityEngineType::None); {
} _updatePeriodModel = std::make_shared<ValueStringModel<ScanSettings::UpdatePeriod>>();
_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> _updatePeriodModel; _updateStartTimeModel = std::make_shared<ValueStringModel<Wt::WTime>>();
std::shared_ptr<ValueStringModel<Wt::WTime>> _updateStartTimeModel; for (std::size_t i = 0; i < 24; ++i)
std::shared_ptr<ValueStringModel<ScanSettings::SimilarityEngineType>> _similarityEngineTypeModel; {
}; Wt::WTime time{ static_cast<int>(i), 0 };
_updateStartTimeModel->add(time.toString(), time);
}
DatabaseSettingsView::DatabaseSettingsView() _similarityEngineTypeModel = std::make_shared<ValueStringModel<ScanSettings::SimilarityEngineType>>();
{ _similarityEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.similarity-engine-type.clusters"), ScanSettings::SimilarityEngineType::Clusters);
wApp->internalPathChanged().connect(this, [this] _similarityEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.similarity-engine-type.none"), ScanSettings::SimilarityEngineType::None);
{ }
refreshView();
});
refreshView(); std::shared_ptr<UpdatePeriodModel> _updatePeriodModel;
} std::shared_ptr<ValueStringModel<Wt::WTime>> _updateStartTimeModel;
std::shared_ptr<ValueStringModel<ScanSettings::SimilarityEngineType>> _similarityEngineTypeModel;
};
void DatabaseSettingsView::DatabaseSettingsView()
DatabaseSettingsView::refreshView() {
{ wApp->internalPathChanged().connect(this, [this]
if (!wApp->internalPathMatches("/admin/database")) {
return; refreshView();
});
clear(); refreshView();
}
auto t {addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.Admin.Database.template"))}; void DatabaseSettingsView::refreshView()
auto model {std::make_shared<DatabaseSettingsModel>()}; {
if (!wApp->internalPathMatches("/admin/database"))
return;
// Media Directory clear();
t->setFormWidget(DatabaseSettingsModel::MediaDirectoryField, std::make_unique<Wt::WLineEdit>());
// Update Period auto t{ addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.Admin.Database.template")) };
auto updatePeriod {std::make_unique<Wt::WComboBox>()}; auto model{ std::make_shared<DatabaseSettingsModel>() };
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));
// Update Start Time // Media Directory
auto updateStartTime {std::make_unique<Wt::WComboBox>()}; t->setFormWidget(DatabaseSettingsModel::MediaDirectoryField, std::make_unique<Wt::WLineEdit>());
updateStartTime->setModel(model->updateStartTimeModel());
t->setFormWidget(DatabaseSettingsModel::UpdateStartTimeField, std::move(updateStartTime));
// Similarity engine type // Update Period
auto similarityEngineType {std::make_unique<Wt::WComboBox>()}; auto updatePeriod{ std::make_unique<Wt::WComboBox>() };
similarityEngineType->setModel(model->similarityEngineTypeModel()); updatePeriod->setModel(model->updatePeriodModel());
t->setFormWidget(DatabaseSettingsModel::SimilarityEngineTypeField, std::move(similarityEngineType)); 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 // Update Start Time
t->setFormWidget(DatabaseSettingsModel::ClustersField, std::make_unique<Wt::WLineEdit>()); auto updateStartTime{ std::make_unique<Wt::WComboBox>() };
updateStartTime->setModel(model->updateStartTimeModel());
t->setFormWidget(DatabaseSettingsModel::UpdateStartTimeField, std::move(updateStartTime));
// Buttons // Similarity engine type
Wt::WPushButton *saveBtn = t->bindWidget("apply-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.apply"))); auto similarityEngineType{ std::make_unique<Wt::WComboBox>() };
Wt::WPushButton *discardBtn = t->bindWidget("discard-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.discard"))); similarityEngineType->setModel(model->similarityEngineTypeModel());
Wt::WPushButton *immScanBtn = t->bindWidget("immediate-scan-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.Admin.Database.immediate-scan"))); t->setFormWidget(DatabaseSettingsModel::SimilarityEngineTypeField, std::move(similarityEngineType));
t->bindNew<ScannerController>("scanner-controller"); // Clusters
t->setFormWidget(DatabaseSettingsModel::ExtraTagsField, std::make_unique<Wt::WLineEdit>());
saveBtn->clicked().connect([=] // Buttons
{ Wt::WPushButton* saveBtn = t->bindWidget("apply-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.apply")));
t->updateModel(model.get()); Wt::WPushButton* discardBtn = t->bindWidget("discard-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.discard")));
Wt::WPushButton* immScanBtn = t->bindWidget("immediate-scan-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.Admin.Database.immediate-scan")));
if (model->validate()) t->bindNew<ScannerController>("scanner-controller");
{
model->saveData();
Service<Recommendation::IRecommendationService>::get()->load(); saveBtn->clicked().connect([=]
Service<Scanner::IScannerService>::get()->requestImmediateScan(false); {
LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.Admin.Database.database"), Wt::WString::tr("Lms.Admin.Database.settings-saved")); t->updateModel(model.get());
}
// Udate the view: Delete any validation message in the view, etc. if (model->validate())
t->updateView(model.get()); {
}); model->saveData();
discardBtn->clicked().connect([=] Service<Recommendation::IRecommendationService>::get()->load();
{ Service<Scanner::IScannerService>::get()->requestImmediateScan(false);
model->loadData(); LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.Admin.Database.database"), Wt::WString::tr("Lms.Admin.Database.settings-saved"));
model->validate(); }
t->updateView(model.get());
});
immScanBtn->clicked().connect([=] // Udate the view: Delete any validation message in the view, etc.
{ t->updateView(model.get());
Service<Scanner::IScannerService>::get()->requestImmediateScan(false); });
});
t->updateView(model.get()); discardBtn->clicked().connect([=]
} {
model->loadData();
model->validate();
t->updateView(model.get());
});
immScanBtn->clicked().connect([=]
{
Service<Scanner::IScannerService>::get()->requestImmediateScan(false);
});
t->updateView(model.get());
}
} // namespace UserInterface } // namespace UserInterface
+57
View File
@@ -0,0 +1,57 @@
/*
* Copyright (C) 2023 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 "DirectoryValidator.hpp"
#include <string>
#include <string_view>
namespace UserInterface
{
class UppercaseValidator : public Wt::WValidator
{
public:
UppercaseValidator(std::string_view delimiters) : _delimiters{ delimiters } {}
private:
Wt::WValidator::Result validate(const Wt::WString& input) const override;
std::string javaScriptValidate() const override { return {}; }
std::string _delimiters;
};
Wt::WValidator::Result UppercaseValidator::validate(const Wt::WString& input) const
{
if (input.empty())
return Wt::WValidator::validate(input);
const std::string str{ input.toUTF8() };
const bool valid{ std::all_of(std::cbegin(str),std::cend(str), [&](char c) { return std::isupper(c) || _delimiters.find(c) != std::string::npos;}) };
if (!valid)
return Wt::WValidator::Result(Wt::ValidationState::Invalid, Wt::WString::tr("Lms.field-must-be-in-upper-case"));
return Wt::WValidator::Result(Wt::ValidationState::Valid);
}
std::unique_ptr<Wt::WValidator> createUppercaseValidator(std::string_view delimiters)
{
return std::make_unique<UppercaseValidator>(delimiters);
}
} // namespace UserInterface
+27
View File
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2023 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 <Wt/WValidator.h>
namespace UserInterface
{
std::unique_ptr<Wt::WValidator> createUppercaseValidator(std::string_view delimiters);
} // namespace UserInterface
+3 -3
View File
@@ -142,10 +142,10 @@ namespace UserInterface
Wt::WContainerWidget* clusterContainers{ bindNew<Wt::WContainerWidget>("clusters") }; Wt::WContainerWidget* clusterContainers{ bindNew<Wt::WContainerWidget>("clusters") };
{ {
auto clusterTypes = ScanSettings::get(LmsApp->getDbSession())->getClusterTypes(); auto clusterTypes{ ClusterType::findIds(LmsApp->getDbSession()).results };
auto clusterGroups = artist->getClusterGroups(clusterTypes, 3); auto clusterGroups{ artist->getClusterGroups(clusterTypes, 3) };
for (auto clusters : clusterGroups) for (const auto& clusters : clusterGroups)
{ {
for (const Database::Cluster::pointer& cluster : clusters) for (const Database::Cluster::pointer& cluster : clusters)
{ {
+2 -2
View File
@@ -258,8 +258,8 @@ namespace UserInterface
Wt::WContainerWidget* clusterContainers{ bindNew<Wt::WContainerWidget>("clusters") }; Wt::WContainerWidget* clusterContainers{ bindNew<Wt::WContainerWidget>("clusters") };
{ {
const auto clusterTypes{ ScanSettings::get(LmsApp->getDbSession())->getClusterTypes() }; const auto clusterTypeIds{ ClusterType::findIds(LmsApp->getDbSession()).results };
const auto clusterGroups{ release->getClusterGroups(clusterTypes, 3) }; const auto clusterGroups{ release->getClusterGroups(clusterTypeIds, 3) };
for (const auto& clusters : clusterGroups) for (const auto& clusters : clusterGroups)
{ {
+2 -2
View File
@@ -99,8 +99,8 @@ namespace UserInterface
Wt::WContainerWidget* clusterContainers{ bindNew<Wt::WContainerWidget>("clusters") }; Wt::WContainerWidget* clusterContainers{ bindNew<Wt::WContainerWidget>("clusters") };
{ {
const auto clusterTypes{ ScanSettings::get(LmsApp->getDbSession())->getClusterTypes() }; const auto clusterTypeIds{ ClusterType::findIds(LmsApp->getDbSession()).results };
const auto clusterGroups{ trackList->getClusterGroups(clusterTypes, 3) }; const auto clusterGroups{ trackList->getClusterGroups(clusterTypeIds, 3) };
for (const auto& clusters : clusterGroups) for (const auto& clusters : clusterGroups)
{ {
+1 -1
View File
@@ -142,7 +142,7 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
{ {
using namespace MetaData; using namespace MetaData;
parser.setClusterTypeNames({ "ALBUMMOOD", "MOOD", "ALBUMGROUPING", "ALBUMGENRE", "GENRE" }); parser.setExtraTags({ "MOOD", "ALBUMGROUPING", "GENRE", "LANGUAGE" });
const auto start{ std::chrono::steady_clock::now() }; const auto start{ std::chrono::steady_clock::now() };
std::optional<Track> track{ parser.parse(file, true) }; std::optional<Track> track{ parser.parse(file, true) };