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 @@
-
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 +Extra tags to scan (use ; as separator) 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 +Tags supplémentaires à scanner (utiliser ; comme séparateur) 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()); + // Update Start Time + auto updateStartTime{ std::make_unique() }; + updateStartTime->setModel(model->updateStartTimeModel()); + t->setFormWidget(DatabaseSettingsModel::UpdateStartTimeField, std::move(updateStartTime)); - // Buttons - Wt::WPushButton *saveBtn = t->bindWidget("apply-btn", std::make_unique(Wt::WString::tr("Lms.apply"))); - Wt::WPushButton *discardBtn = t->bindWidget("discard-btn", std::make_unique(Wt::WString::tr("Lms.discard"))); - Wt::WPushButton *immScanBtn = t->bindWidget("immediate-scan-btn", std::make_unique(Wt::WString::tr("Lms.Admin.Database.immediate-scan"))); + // Similarity engine type + auto similarityEngineType{ std::make_unique() }; + similarityEngineType->setModel(model->similarityEngineTypeModel()); + t->setFormWidget(DatabaseSettingsModel::SimilarityEngineTypeField, std::move(similarityEngineType)); - t->bindNew("scanner-controller"); + // Clusters + t->setFormWidget(DatabaseSettingsModel::ExtraTagsField, std::make_unique()); - saveBtn->clicked().connect([=] - { - t->updateModel(model.get()); + // Buttons + Wt::WPushButton* saveBtn = t->bindWidget("apply-btn", std::make_unique(Wt::WString::tr("Lms.apply"))); + Wt::WPushButton* discardBtn = t->bindWidget("discard-btn", std::make_unique(Wt::WString::tr("Lms.discard"))); + Wt::WPushButton* immScanBtn = t->bindWidget("immediate-scan-btn", std::make_unique(Wt::WString::tr("Lms.Admin.Database.immediate-scan"))); - if (model->validate()) - { - model->saveData(); + t->bindNew("scanner-controller"); - Service::get()->load(); - Service::get()->requestImmediateScan(false); - LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.Admin.Database.database"), Wt::WString::tr("Lms.Admin.Database.settings-saved")); - } + saveBtn->clicked().connect([=] + { + t->updateModel(model.get()); - // Udate the view: Delete any validation message in the view, etc. - t->updateView(model.get()); - }); + if (model->validate()) + { + model->saveData(); - discardBtn->clicked().connect([=] - { - model->loadData(); - model->validate(); - t->updateView(model.get()); - }); + Service::get()->load(); + Service::get()->requestImmediateScan(false); + LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.Admin.Database.database"), Wt::WString::tr("Lms.Admin.Database.settings-saved")); + } - immScanBtn->clicked().connect([=] - { - Service::get()->requestImmediateScan(false); - }); + // Udate the view: Delete any validation message in the view, etc. + t->updateView(model.get()); + }); - t->updateView(model.get()); -} + discardBtn->clicked().connect([=] + { + model->loadData(); + model->validate(); + t->updateView(model.get()); + }); + + immScanBtn->clicked().connect([=] + { + Service::get()->requestImmediateScan(false); + }); + + t->updateView(model.get()); + } } // namespace UserInterface - - diff --git a/src/lms/ui/common/UppercaseValidator.cpp b/src/lms/ui/common/UppercaseValidator.cpp new file mode 100644 index 00000000..77ba47b1 --- /dev/null +++ b/src/lms/ui/common/UppercaseValidator.cpp @@ -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 . + */ + +#include "DirectoryValidator.hpp" + +#include +#include + +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 createUppercaseValidator(std::string_view delimiters) + { + return std::make_unique(delimiters); + } +} // namespace UserInterface diff --git a/src/lms/ui/common/UppercaseValidator.hpp b/src/lms/ui/common/UppercaseValidator.hpp new file mode 100644 index 00000000..679fbb1f --- /dev/null +++ b/src/lms/ui/common/UppercaseValidator.hpp @@ -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 . + */ + +#pragma once + +#include + +namespace UserInterface +{ + std::unique_ptr createUppercaseValidator(std::string_view delimiters); +} // namespace UserInterface diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 435dcba8..730d51ec 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -142,10 +142,10 @@ namespace UserInterface Wt::WContainerWidget* clusterContainers{ bindNew("clusters") }; { - auto clusterTypes = ScanSettings::get(LmsApp->getDbSession())->getClusterTypes(); - auto clusterGroups = artist->getClusterGroups(clusterTypes, 3); + auto clusterTypes{ ClusterType::findIds(LmsApp->getDbSession()).results }; + auto clusterGroups{ artist->getClusterGroups(clusterTypes, 3) }; - for (auto clusters : clusterGroups) + for (const auto& clusters : clusterGroups) { for (const Database::Cluster::pointer& cluster : clusters) { diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 508e7250..aeb25063 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -258,8 +258,8 @@ namespace UserInterface Wt::WContainerWidget* clusterContainers{ bindNew("clusters") }; { - const auto clusterTypes{ ScanSettings::get(LmsApp->getDbSession())->getClusterTypes() }; - const auto clusterGroups{ release->getClusterGroups(clusterTypes, 3) }; + const auto clusterTypeIds{ ClusterType::findIds(LmsApp->getDbSession()).results }; + const auto clusterGroups{ release->getClusterGroups(clusterTypeIds, 3) }; for (const auto& clusters : clusterGroups) { diff --git a/src/lms/ui/explore/TrackListView.cpp b/src/lms/ui/explore/TrackListView.cpp index 6289afc0..7e101882 100644 --- a/src/lms/ui/explore/TrackListView.cpp +++ b/src/lms/ui/explore/TrackListView.cpp @@ -99,8 +99,8 @@ namespace UserInterface Wt::WContainerWidget* clusterContainers{ bindNew("clusters") }; { - const auto clusterTypes{ ScanSettings::get(LmsApp->getDbSession())->getClusterTypes() }; - const auto clusterGroups{ trackList->getClusterGroups(clusterTypes, 3) }; + const auto clusterTypeIds{ ClusterType::findIds(LmsApp->getDbSession()).results }; + const auto clusterGroups{ trackList->getClusterGroups(clusterTypeIds, 3) }; for (const auto& clusters : clusterGroups) { diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp index 896f692e..80d2d8e1 100644 --- a/src/tools/metadata/LmsMetadata.cpp +++ b/src/tools/metadata/LmsMetadata.cpp @@ -142,7 +142,7 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file) { using namespace MetaData; - parser.setClusterTypeNames({ "ALBUMMOOD", "MOOD", "ALBUMGROUPING", "ALBUMGENRE", "GENRE" }); + parser.setExtraTags({ "MOOD", "ALBUMGROUPING", "GENRE", "LANGUAGE" }); const auto start{ std::chrono::steady_clock::now() }; std::optional track{ parser.parse(file, true) };