From 4ba7aff628c684f1cc17c8b67191f5cfaeb3e3ad Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 4 Apr 2023 23:22:27 +0200 Subject: [PATCH 1/9] Added support for release type in metadata parser --- src/libs/metadata/impl/TagLibParser.cpp | 11 +++- src/libs/metadata/impl/Utils.cpp | 45 +++++++++++++++ src/libs/metadata/impl/Utils.hpp | 10 +++- .../metadata/include/metadata/IParser.hpp | 28 +++++++++ src/libs/metadata/test/Utils.cpp | 57 +++++++++++++++++++ src/libs/utils/include/utils/EnumSet.hpp | 12 ++++ src/tools/metadata/LmsMetadata.cpp | 47 +++++++++++++++ 7 files changed, 207 insertions(+), 3 deletions(-) diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp index 4e0553ad..3001e05f 100644 --- a/src/libs/metadata/impl/TagLibParser.cpp +++ b/src/libs/metadata/impl/TagLibParser.cpp @@ -225,6 +225,13 @@ getRelease(const TagMap& tags) } } + release->primaryType = getPropertyValueFirstMatchAs(tags, {"MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE"}); + if (release->primaryType) + { + const auto secondaryTypes {getPropertyValuesFirstMatchAs(tags, {"MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE"})}; + release->secondaryTypes.assign(std::cbegin(secondaryTypes), std::cend(secondaryTypes)); + } + return release; } @@ -445,7 +452,7 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug) for (const auto& [name, attributeList] : tag->attributeListMap()) { - std::string strName {name.to8Bit(true)}; + std::string strName {StringUtils::stringToUpper(name.to8Bit(true))}; if (strName.find("WM/") == 0 || tags.find(strName) != std::cend(tags)) continue; @@ -461,7 +468,7 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug) if (debug) std::cout << "ASF property: '" << name << "'" << std::endl; - tags[strName] = std::move(attributes); + tags.emplace(strName, std::move(attributes)); } } } diff --git a/src/libs/metadata/impl/Utils.cpp b/src/libs/metadata/impl/Utils.cpp index b8ae6810..774de175 100644 --- a/src/libs/metadata/impl/Utils.cpp +++ b/src/libs/metadata/impl/Utils.cpp @@ -119,3 +119,48 @@ namespace MetaData::Utils } } +namespace StringUtils +{ + static bool iequals(std::string_view a, std::string_view b) + { + return std::equal(std::cbegin(a), std::cend(a), + std::cbegin(b), std::cend(b), + [](char a, char b) { return tolower(a) == tolower(b);} + ); + } + + template<> + std::optional readAs(std::string_view str) + { + str = stringTrim(str); + + if (iequals(str, "album")) + return MetaData::Release::PrimaryType::Album; + else if (iequals(str, "single")) + return MetaData::Release::PrimaryType::Single; + else if (iequals(str, "EP")) + return MetaData::Release::PrimaryType::EP; + else if (iequals(str, "broadcast")) + return MetaData::Release::PrimaryType::Broadcast; + else if (iequals(str, "other")) + return MetaData::Release::PrimaryType::Other; + + return std::nullopt; + } + + template<> + std::optional readAs(std::string_view str) + { + str = stringTrim(str); + + if (iequals(str, "compilation")) + return MetaData::Release::SecondaryType::Compilation; + else if (iequals(str, "soundtrack")) + return MetaData::Release::SecondaryType::Soundtrack; + else if (iequals(str, "live")) + return MetaData::Release::SecondaryType::Live; + + return std::nullopt; + } +} + diff --git a/src/libs/metadata/impl/Utils.hpp b/src/libs/metadata/impl/Utils.hpp index 5d89e2d9..8c80cc14 100644 --- a/src/libs/metadata/impl/Utils.hpp +++ b/src/libs/metadata/impl/Utils.hpp @@ -38,6 +38,14 @@ namespace MetaData::Utils // format is "artist name (role)" PerformerArtist extractPerformerAndRole(std::string_view entry); - +} + +namespace StringUtils +{ + template<> + std::optional readAs(std::string_view str); + + template<> + std::optional readAs(std::string_view str); } diff --git a/src/libs/metadata/include/metadata/IParser.hpp b/src/libs/metadata/include/metadata/IParser.hpp index 765d8ac6..15b72282 100644 --- a/src/libs/metadata/include/metadata/IParser.hpp +++ b/src/libs/metadata/include/metadata/IParser.hpp @@ -29,6 +29,7 @@ #include #include +#include "utils/EnumSet.hpp" #include "utils/UUID.hpp" namespace MetaData @@ -51,10 +52,37 @@ namespace MetaData struct Release { + // see https://musicbrainz.org/doc/Release_Group/Type + enum class PrimaryType + { + Album, + Single, + EP, + Broadcast, + Other + }; + + enum class SecondaryType + { + Compilation, + Soundtrack, + Spokenword, + Interview, + Audiobook, + AudioDrama, + Live, + Remix, + DJMix, + Mixtape_Street, + Demo, + }; + std::optional mbid; std::string name; std::vector artists; std::optional mediumCount; + std::optional primaryType; + EnumSet secondaryTypes; }; struct Medium diff --git a/src/libs/metadata/test/Utils.cpp b/src/libs/metadata/test/Utils.cpp index 41b9ee9e..1cc0e69c 100644 --- a/src/libs/metadata/test/Utils.cpp +++ b/src/libs/metadata/test/Utils.cpp @@ -115,3 +115,60 @@ TEST(MetaData, extractPerformerAndRole) EXPECT_EQ(performer.role, testCase.expectedRole) << " str was '" << testCase.str << "'"; } } + +TEST(MetaData, primaryReleaseTypes) +{ + using namespace MetaData; + + struct TestCase + { + std::string str; + std::optional result; + } testCases [] + { + { "", std::nullopt }, + { "album", Release::PrimaryType::Album }, + { "Album", Release::PrimaryType::Album }, + { " Album", Release::PrimaryType::Album }, + { "Album ", Release::PrimaryType::Album }, + { "ep", Release::PrimaryType::EP }, + { " ep ", Release::PrimaryType::EP }, + { "broadcast", Release::PrimaryType::Broadcast }, + { "single", Release::PrimaryType::Single }, + { "other", Release::PrimaryType::Other }, + }; + + for (const TestCase& testCase : testCases) + { + std::optional parsed {StringUtils::readAs(testCase.str)}; + + EXPECT_EQ(parsed, testCase.result) << " str was '" << testCase.str << "'"; + } +} + +TEST(MetaData, secondaryReleaseTypes) +{ + using namespace MetaData; + + struct TestCase + { + std::string str; + std::optional result; + } testCases [] + { + { "", std::nullopt }, + { "compilation", Release::SecondaryType::Compilation }, + { " compilation ", Release::SecondaryType::Compilation }, + { "soundtrack", Release::SecondaryType::Soundtrack }, + { "live", Release::SecondaryType::Live }, + { "demo", Release::SecondaryType::Demo }, + }; + + for (const TestCase& testCase : testCases) + { + std::optional parsed {StringUtils::readAs(testCase.str)}; + + EXPECT_EQ(parsed, testCase.result) << " str was '" << testCase.str << "'"; + } +} + diff --git a/src/libs/utils/include/utils/EnumSet.hpp b/src/libs/utils/include/utils/EnumSet.hpp index 2d4361c6..f09ae1e2 100644 --- a/src/libs/utils/include/utils/EnumSet.hpp +++ b/src/libs/utils/include/utils/EnumSet.hpp @@ -44,6 +44,13 @@ class EnumSet template constexpr EnumSet(It begin, It end) { + assign(begin, end); + } + + template + constexpr void assign(It begin, It end) + { + clear(); for (It it {begin}; it != end; ++it) insert(*it); } @@ -71,6 +78,11 @@ class EnumSet return _bitfield & (underlying_type{ 1 } << static_cast(value)); } + constexpr void clear() + { + _bitfield = 0; + } + class iterator { public: diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp index 81e77b18..9ac1a459 100644 --- a/src/tools/metadata/LmsMetadata.cpp +++ b/src/tools/metadata/LmsMetadata.cpp @@ -44,6 +44,46 @@ operator<<(std::ostream& os, const MetaData::Artist& artist) return os; } +static +std::ostream& +operator<<(std::ostream& os, MetaData::Release::PrimaryType type) +{ + switch (type) + { + case MetaData::Release::PrimaryType::Album: os << "Album"; break; + case MetaData::Release::PrimaryType::Single: os << "Single"; break; + case MetaData::Release::PrimaryType::EP: os << "EP"; break; + case MetaData::Release::PrimaryType::Broadcast: os << "Broadcast"; break; + case MetaData::Release::PrimaryType::Other: os << "Other"; break; + default: + os << "??"; + } + return os; +} + +static +std::ostream& +operator<<(std::ostream& os, MetaData::Release::SecondaryType type) +{ + switch (type) + { + case MetaData::Release::SecondaryType::Compilation: os << "Compilation"; break; + case MetaData::Release::SecondaryType::Soundtrack: os << "Soundtrack"; break; + case MetaData::Release::SecondaryType::Spokenword: os << "Spokenword"; break; + case MetaData::Release::SecondaryType::Interview: os << "Interview"; break; + case MetaData::Release::SecondaryType::Audiobook: os << "Audiobook"; break; + case MetaData::Release::SecondaryType::AudioDrama: os << "Audio drama"; break; + case MetaData::Release::SecondaryType::Live: os << "Live"; break; + case MetaData::Release::SecondaryType::Remix: os << "Remix"; break; + case MetaData::Release::SecondaryType::DJMix: os << "DJ-mix"; break; + case MetaData::Release::SecondaryType::Mixtape_Street: os << "Mixtape/Street"; break; + case MetaData::Release::SecondaryType::Demo: os << "Mixtape/Demo"; break; + default: + os << "??"; + } + return os; +} + static std::ostream& operator<<(std::ostream& os, const MetaData::Release& release) @@ -59,6 +99,13 @@ operator<<(std::ostream& os, const MetaData::Release& release) for (const MetaData::Artist& artist : release.artists) std::cout << "\tRelease artist: " << artist << std::endl; + if (release.primaryType) + { + std::cout << "\tPrimary type: " << *release.primaryType << std::endl; + for (MetaData::Release::SecondaryType type : release.secondaryTypes) + std::cout << "\tSecondary type:" << type << std::endl; + } + return os; } From f465c804b78e6aafc6fcc8ddf470ed4435590ceb Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 4 Apr 2023 23:47:29 +0200 Subject: [PATCH 2/9] Minor optims --- src/libs/metadata/impl/TagLibParser.cpp | 28 ++++++++++++------------- src/libs/utils/impl/String.cpp | 7 +++++++ src/libs/utils/include/utils/String.hpp | 5 +++++ 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp index 3001e05f..dad243ed 100644 --- a/src/libs/metadata/impl/TagLibParser.cpp +++ b/src/libs/metadata/impl/TagLibParser.cpp @@ -50,7 +50,7 @@ using TagMap = std::map>; template std::vector -getPropertyValuesFirstMatchAs(const TagMap& tags, const std::vector& keys) +getPropertyValuesFirstMatchAs(const TagMap& tags, std::initializer_list keys) { std::vector res; @@ -83,7 +83,7 @@ getPropertyValuesFirstMatchAs(const TagMap& tags, const std::vector std::optional -getPropertyValueFirstMatchAs(const TagMap& tags, const std::vector& keys) +getPropertyValueFirstMatchAs(const TagMap& tags, std::initializer_list keys) { std::optional res; std::vector values {getPropertyValuesFirstMatchAs(tags, keys)}; @@ -95,14 +95,14 @@ getPropertyValueFirstMatchAs(const TagMap& tags, const std::vector std::vector -getPropertyValuesAs(const TagMap& tags, const std::string& key) +getPropertyValuesAs(const TagMap& tags, std::string_view key) { return getPropertyValuesFirstMatchAs(tags, {key}); } template std::optional -getPropertyValueAs(const TagMap& tags, const std::string& key) +getPropertyValueAs(const TagMap& tags, std::string_view key) { return getPropertyValueFirstMatchAs(tags, {key}); } @@ -121,22 +121,22 @@ splitAndTrimString(std::string_view str, std::string_view delimiters) static std::vector getArtists(const TagMap& tags, - const std::vector& artistTagNames, - const std::vector& artistSortTagNames, - const std::vector& artistMBIDTagNames + std::initializer_list artistTagNames, + std::initializer_list artistSortTagNames, + std::initializer_list artistMBIDTagNames ) { - const std::vector artistNames {getPropertyValuesFirstMatchAs(tags, artistTagNames)}; + const std::vector artistNames {getPropertyValuesFirstMatchAs(tags, artistTagNames)}; if (artistNames.empty()) return {}; std::vector artists; artists.reserve(artistNames.size()); std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(artists), - [&](const std::string& name) { return Artist {name}; }); + [&](std::string_view name) { return Artist {name}; }); { - const std::vector artistSortNames {getPropertyValuesFirstMatchAs(tags, artistSortTagNames)}; + const std::vector artistSortNames {getPropertyValuesFirstMatchAs(tags, artistSortTagNames)}; if (artistSortNames.size() == artists.size()) { for (std::size_t i {}; i < artistSortNames.size(); ++i) @@ -161,14 +161,14 @@ getArtists(const TagMap& tags, static PerformerContainer getPerformerArtists(const TagMap& tags, - const std::vector& artistTagNames) + std::initializer_list artistTagNames) { PerformerContainer performers; // picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer) // We may hit both styles for the same track // PERFORMER: artist (role) - if (const std::vector artistNames {getPropertyValuesFirstMatchAs(tags, artistTagNames)}; !artistNames.empty()) + if (const std::vector artistNames {getPropertyValuesFirstMatchAs(tags, artistTagNames)}; !artistNames.empty()) { for (std::string_view entry : artistNames) { @@ -216,7 +216,7 @@ getRelease(const TagMap& tags) if (!release->mediumCount) { // mediumCount may be encoded as "position/count" - if (const auto value {getPropertyValueAs(tags, "DISCNUMBER")}) + if (const auto value {getPropertyValueAs(tags, "DISCNUMBER")}) { // Expecting 'Number/Total' const std::vector strings {StringUtils::splitString(*value, "/") }; @@ -248,7 +248,7 @@ getMedium(const TagMap& tags) if (!medium->trackCount) { // totalTracks may be encoded as "position/count" - if (const auto value {getPropertyValueAs(tags, "TRACKNUMBER")}) + if (const auto value {getPropertyValueAs(tags, "TRACKNUMBER")}) { // Expecting 'Number/Total' const std::vector strings {StringUtils::splitString(*value, "/") }; diff --git a/src/libs/utils/impl/String.cpp b/src/libs/utils/impl/String.cpp index 344e3eeb..89c58edf 100644 --- a/src/libs/utils/impl/String.cpp +++ b/src/libs/utils/impl/String.cpp @@ -63,6 +63,13 @@ readAs(std::string_view str) return std::string {str}; } +template<> +std::optional +readAs(std::string_view str) +{ + return str; +} + template<> std::optional readAs(std::string_view str) diff --git a/src/libs/utils/include/utils/String.hpp b/src/libs/utils/include/utils/String.hpp index 95e97c60..dff67ca0 100644 --- a/src/libs/utils/include/utils/String.hpp +++ b/src/libs/utils/include/utils/String.hpp @@ -88,6 +88,11 @@ template<> std::optional readAs(std::string_view str); +template<> +[[nodiscard]] +std::optional +readAs(std::string_view str); + template<> [[nodiscard]] std::optional From 52406e7e4b334849fdbc34d7974383fb8a009c34 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 4 Apr 2023 23:51:21 +0200 Subject: [PATCH 3/9] Fixed missing secondary type --- src/libs/metadata/impl/Utils.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/metadata/impl/Utils.cpp b/src/libs/metadata/impl/Utils.cpp index 774de175..c821959e 100644 --- a/src/libs/metadata/impl/Utils.cpp +++ b/src/libs/metadata/impl/Utils.cpp @@ -159,6 +159,8 @@ namespace StringUtils return MetaData::Release::SecondaryType::Soundtrack; else if (iequals(str, "live")) return MetaData::Release::SecondaryType::Live; + else if (iequals(str, "demo")) + return MetaData::Release::SecondaryType::Demo; return std::nullopt; } From 3363479b64c129d9131e57fc288b2bdba1d0b5a4 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 14 Apr 2023 21:49:38 +0200 Subject: [PATCH 4/9] Subsonic API: report code as int and not string --- src/libs/subsonic/impl/SubsonicResponse.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/subsonic/impl/SubsonicResponse.cpp b/src/libs/subsonic/impl/SubsonicResponse.cpp index 8d9c640a..2958291c 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.cpp +++ b/src/libs/subsonic/impl/SubsonicResponse.cpp @@ -133,7 +133,7 @@ Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& err responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks Node& errorNode {responseNode.createChild("error")}; - errorNode.setAttribute("code", std::to_string(static_cast(error.getCode()))); + errorNode.setAttribute("code", static_cast(error.getCode())); errorNode.setAttribute("message", error.getMessage()); return response; From 1d41a26ead459dcffe5796a0027978abbde6e0fe Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 16 Apr 2023 15:00:04 +0200 Subject: [PATCH 5/9] Added release type in scanner + UI (detailed view only) --- approot/messages.xml | 17 +++++ approot/messages_fr.xml | 17 +++++ approot/messages_it.xml | 17 +++++ approot/messages_zh.xml | 21 ++++-- approot/release.xml | 12 ++++ approot/tracks.xml | 2 + src/libs/services/database/impl/Artist.cpp | 1 + .../services/database/impl/EnumSetTraits.hpp | 54 ++++++++++++++ src/libs/services/database/impl/Migration.cpp | 13 ++++ src/libs/services/database/impl/Migration.hpp | 2 +- src/libs/services/database/impl/Release.cpp | 1 + src/libs/services/database/impl/Session.cpp | 1 + .../include/services/database/Release.hpp | 14 +++- .../include/services/database/Types.hpp | 25 +++++++ src/libs/services/database/test/Release.cpp | 23 ++++++ .../scanner/impl/ScanStepScanFiles.cpp | 70 +++++++++++++++++++ src/libs/utils/include/utils/EnumSet.hpp | 44 ++++++++---- src/libs/utils/test/CMakeLists.txt | 3 +- src/libs/utils/test/EnumSet.cpp | 60 ++++++++++++++++ src/lms/ui/explore/ReleaseView.cpp | 62 ++++++++++++++-- src/lms/ui/explore/TrackListHelpers.cpp | 19 +++-- 21 files changed, 443 insertions(+), 35 deletions(-) create mode 100644 src/libs/services/database/impl/EnumSetTraits.hpp create mode 100644 src/libs/utils/test/EnumSet.cpp diff --git a/approot/messages.xml b/approot/messages.xml index 3b338585..7eb377d8 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -197,6 +197,23 @@ Similar albums Copyright Disc {1} +Type +Album +Broadcast +EP +Other +Single +Audiobook +Audio drama +Compilation +Demo +DJ-mix +Interview +Live +Mixtape/Street +Remix +Soundtrack +Spokenword Delete playlist? diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 850d261e..1817137f 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -197,6 +197,23 @@ Albums similaires Copyright Disque {1} +Type +Album +Broadcast +EP +Other +Single +Audiobook +Audio drama +Compilation +Demo +DJ-mix +Interview +Live +Mixtape/Street +Remix +Soundtrack +Spokenword Supprimer la liste de lecture ? diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 77712086..a322cbdc 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -198,6 +198,23 @@ Copyright Disco {1} + + + + + + + + + + + + + + + + + diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 99542a29..41402fe0 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -198,6 +198,23 @@ 版权所有 唱片 {1} + + + + + + + + + + + + + + + + + 删除播放列表? @@ -210,10 +227,6 @@ 清除 创建新播放列表 - - - - 播放队列 广播模式 循环播放 diff --git a/approot/release.xml b/approot/release.xml index 91a38f6f..2d9d2c94 100644 --- a/approot/release.xml +++ b/approot/release.xml @@ -103,7 +103,19 @@