Allowed custom release types + handle more MusicBrainz secondary types in UI. fixes #373
This commit is contained in:
@@ -226,15 +226,15 @@ namespace MetaData
|
||||
{
|
||||
track.recordingMBID = UUID::fromString(value);
|
||||
}
|
||||
else if (std::find(std::cbegin(_extraTags), std::cend(_extraTags), tag) != std::cend(_extraTags))
|
||||
else if (std::find(std::cbegin(_userExtraTags), std::cend(_userExtraTags), tag) != std::cend(_userExtraTags))
|
||||
{
|
||||
const std::vector<std::string_view> tagValues{ StringUtils::splitString(value, "/,;") };
|
||||
|
||||
if (!tagValues.empty())
|
||||
{
|
||||
std::set<std::string> values;
|
||||
std::vector<std::string> values;
|
||||
std::transform(std::cbegin(tagValues), std::cend(tagValues), std::inserter(values, std::begin(values)), [](std::string_view v) { return std::string{ v }; });
|
||||
track.tags[tag] = std::move(values);
|
||||
track.userExtraTags[tag] = std::move(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,12 +214,7 @@ namespace MetaData
|
||||
}
|
||||
}
|
||||
|
||||
release->primaryType = getPropertyValueFirstMatchAs<MetaData::Release::PrimaryType>(tags, { "MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE" });
|
||||
if (release->primaryType)
|
||||
{
|
||||
const auto secondaryTypes{ getPropertyValuesFirstMatchAs<MetaData::Release::SecondaryType>(tags, {"MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE"}) };
|
||||
release->secondaryTypes.assign(std::cbegin(secondaryTypes), std::cend(secondaryTypes));
|
||||
}
|
||||
release->releaseTypes = getPropertyValuesFirstMatchAs<std::string>(tags, { "MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE" });
|
||||
|
||||
return release;
|
||||
}
|
||||
@@ -368,18 +363,18 @@ namespace MetaData
|
||||
track.replayGain = StringUtils::readAs<float>(value);
|
||||
else if (tag == "ARTIST")
|
||||
track.artistDisplayName = value;
|
||||
else if (std::find(std::cbegin(_extraTags), std::cend(_extraTags), tag) != std::cend(_extraTags))
|
||||
else if (std::find(std::cbegin(_userExtraTags), std::cend(_userExtraTags), tag) != std::cend(_userExtraTags))
|
||||
{
|
||||
std::set<std::string> tagValues;
|
||||
std::vector<std::string> tagValues;
|
||||
for (std::string_view valueList : values)
|
||||
{
|
||||
const std::vector<std::string_view> splittedValues{ splitAndTrimString(valueList, "/,;") }; // handle possibily bad split tags
|
||||
for (std::string_view value : splittedValues)
|
||||
tagValues.insert(std::string{ value });
|
||||
tagValues.push_back(std::string{ value });
|
||||
}
|
||||
|
||||
if (!tagValues.empty())
|
||||
track.tags[tag] = std::move(tagValues);
|
||||
track.userExtraTags[tag] = std::move(tagValues);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,142 +27,91 @@
|
||||
|
||||
namespace MetaData::Utils
|
||||
{
|
||||
Wt::WDate
|
||||
parseDate(std::string_view dateStr)
|
||||
{
|
||||
static constexpr const char* formats[]
|
||||
{
|
||||
"%Y-%m-%d",
|
||||
"%Y/%m/%d",
|
||||
};
|
||||
Wt::WDate parseDate(std::string_view dateStr)
|
||||
{
|
||||
static constexpr const char* formats[]
|
||||
{
|
||||
"%Y-%m-%d",
|
||||
"%Y/%m/%d",
|
||||
};
|
||||
|
||||
for (const char* format : formats)
|
||||
{
|
||||
std::tm tm = {};
|
||||
std::istringstream ss {std::string {dateStr}}; // TODO, remove extra copy here
|
||||
ss >> std::get_time(&tm, format);
|
||||
if (ss.fail())
|
||||
continue;
|
||||
for (const char* format : formats)
|
||||
{
|
||||
std::tm tm = {};
|
||||
std::istringstream ss{ std::string {dateStr} }; // TODO, remove extra copy here
|
||||
ss >> std::get_time(&tm, format);
|
||||
if (ss.fail())
|
||||
continue;
|
||||
|
||||
const Wt::WDate res
|
||||
{
|
||||
tm.tm_year + 1900, // years since 1900
|
||||
tm.tm_mon + 1, // months since January – [0, 11]
|
||||
tm.tm_mday ? tm.tm_mday : 1 // day of the month – [1, 31]
|
||||
};
|
||||
if (!res.isValid())
|
||||
continue;
|
||||
const Wt::WDate res
|
||||
{
|
||||
tm.tm_year + 1900, // years since 1900
|
||||
tm.tm_mon + 1, // months since January – [0, 11]
|
||||
tm.tm_mday ? tm.tm_mday : 1 // day of the month – [1, 31]
|
||||
};
|
||||
if (!res.isValid())
|
||||
continue;
|
||||
|
||||
return res;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string_view
|
||||
readStyleToString(ParserReadStyle readStyle)
|
||||
{
|
||||
switch (readStyle)
|
||||
{
|
||||
case ParserReadStyle::Fast: return "fast";
|
||||
case ParserReadStyle::Average: return "average";
|
||||
case ParserReadStyle::Accurate: return "accurate";
|
||||
}
|
||||
std::string_view readStyleToString(ParserReadStyle readStyle)
|
||||
{
|
||||
switch (readStyle)
|
||||
{
|
||||
case ParserReadStyle::Fast: return "fast";
|
||||
case ParserReadStyle::Average: return "average";
|
||||
case ParserReadStyle::Accurate: return "accurate";
|
||||
}
|
||||
|
||||
throw LmsException {"Unknown read style"};
|
||||
}
|
||||
throw LmsException{ "Unknown read style" };
|
||||
}
|
||||
|
||||
PerformerArtist
|
||||
extractPerformerAndRole(std::string_view entry)
|
||||
{
|
||||
std::string_view artistName;
|
||||
std::string_view role;
|
||||
PerformerArtist extractPerformerAndRole(std::string_view entry)
|
||||
{
|
||||
std::string_view artistName;
|
||||
std::string_view role;
|
||||
|
||||
std::size_t roleBegin {};
|
||||
std::size_t roleEnd {};
|
||||
std::size_t count {};
|
||||
std::size_t roleBegin{};
|
||||
std::size_t roleEnd{};
|
||||
std::size_t count{};
|
||||
|
||||
for (std::size_t i {}; i < entry.size(); ++i)
|
||||
{
|
||||
std::size_t currentIndex {entry.size() - i - 1};
|
||||
const char c {entry[currentIndex]};
|
||||
for (std::size_t i{}; i < entry.size(); ++i)
|
||||
{
|
||||
std::size_t currentIndex{ entry.size() - i - 1 };
|
||||
const char c{ entry[currentIndex] };
|
||||
|
||||
if (std::isspace(c))
|
||||
continue;
|
||||
if (std::isspace(c))
|
||||
continue;
|
||||
|
||||
if (c == ')')
|
||||
{
|
||||
if (count++ == 0)
|
||||
roleEnd = currentIndex;
|
||||
}
|
||||
else if (c == '(')
|
||||
{
|
||||
if (count == 0)
|
||||
break;
|
||||
if (c == ')')
|
||||
{
|
||||
if (count++ == 0)
|
||||
roleEnd = currentIndex;
|
||||
}
|
||||
else if (c == '(')
|
||||
{
|
||||
if (count == 0)
|
||||
break;
|
||||
|
||||
if (--count == 0)
|
||||
{
|
||||
roleBegin = currentIndex + 1;
|
||||
role = StringUtils::stringTrim(entry.substr(roleBegin, roleEnd - roleBegin));
|
||||
artistName = StringUtils::stringTrim(entry.substr(0, currentIndex));
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (count == 0)
|
||||
break;
|
||||
}
|
||||
if (--count == 0)
|
||||
{
|
||||
roleBegin = currentIndex + 1;
|
||||
role = StringUtils::stringTrim(entry.substr(roleBegin, roleEnd - roleBegin));
|
||||
artistName = StringUtils::stringTrim(entry.substr(0, currentIndex));
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (count == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!roleEnd || !roleBegin)
|
||||
artistName = StringUtils::stringTrim(entry);
|
||||
|
||||
return PerformerArtist {Artist {artistName}, std::string {role}};
|
||||
}
|
||||
}
|
||||
|
||||
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<MetaData::Release::PrimaryType> 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<MetaData::Release::SecondaryType> 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;
|
||||
else if (iequals(str, "demo"))
|
||||
return MetaData::Release::SecondaryType::Demo;
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
if (!roleEnd || !roleBegin)
|
||||
artistName = StringUtils::stringTrim(entry);
|
||||
|
||||
return PerformerArtist{ Artist {artistName}, std::string {role} };
|
||||
}
|
||||
}
|
||||
@@ -39,13 +39,3 @@ namespace MetaData::Utils
|
||||
// format is "artist name (role)"
|
||||
PerformerArtist extractPerformerAndRole(std::string_view entry);
|
||||
}
|
||||
|
||||
namespace StringUtils
|
||||
{
|
||||
template<>
|
||||
std::optional<MetaData::Release::PrimaryType> readAs(std::string_view str);
|
||||
|
||||
template<>
|
||||
std::optional<MetaData::Release::SecondaryType> readAs(std::string_view str);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,18 +23,16 @@
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WDate.h>
|
||||
#include "utils/EnumSet.hpp"
|
||||
#include "utils/UUID.hpp"
|
||||
|
||||
namespace MetaData
|
||||
{
|
||||
using Tags = std::map<std::string /* type */, std::set<std::string> /* values */>;
|
||||
using Tags = std::map<std::string /* type */, std::vector<std::string> /* values */>;
|
||||
|
||||
// Very simplified version of https://musicbrainz.org/doc/MusicBrainz_Database/Schema
|
||||
|
||||
@@ -52,43 +50,17 @@ 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<UUID> mbid;
|
||||
std::string name;
|
||||
std::string artistDisplayName;
|
||||
std::vector<Artist> artists;
|
||||
std::optional<std::size_t> mediumCount;
|
||||
std::optional<PrimaryType> primaryType;
|
||||
EnumSet<SecondaryType> secondaryTypes;
|
||||
std::vector<std::string> releaseTypes;
|
||||
};
|
||||
|
||||
struct Medium
|
||||
{
|
||||
std::string type;
|
||||
std::string type; // CD, etc.
|
||||
std::string name;
|
||||
std::optional<Release> release;
|
||||
std::optional<std::size_t> position; // in release
|
||||
@@ -103,7 +75,11 @@ namespace MetaData
|
||||
std::string title;
|
||||
std::optional<Medium> medium;
|
||||
std::optional<std::size_t> position; // in medium
|
||||
Tags tags;
|
||||
std::vector<std::string> grouping;
|
||||
std::vector<std::string> genres;
|
||||
std::vector<std::string> moods;
|
||||
std::vector<std::string> languages;
|
||||
Tags userExtraTags;
|
||||
std::chrono::milliseconds duration{};
|
||||
std::size_t bitrate{};
|
||||
Wt::WDate date;
|
||||
@@ -131,10 +107,10 @@ namespace MetaData
|
||||
|
||||
virtual std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) = 0;
|
||||
|
||||
void setExtraTags(const std::vector<std::string>& extraTags) { _extraTags = std::set(extraTags.cbegin(), extraTags.cend()); }
|
||||
void setUserExtraTags(const std::vector<std::string>& extraTags) { _userExtraTags = std::vector(extraTags.cbegin(), extraTags.cend()); }
|
||||
|
||||
protected:
|
||||
std::set<std::string> _extraTags;
|
||||
std::vector<std::string> _userExtraTags;
|
||||
};
|
||||
|
||||
enum class ParserType
|
||||
@@ -151,4 +127,3 @@ namespace MetaData
|
||||
};
|
||||
std::unique_ptr<IParser> createParser(ParserType parserType, ParserReadStyle parserReadStyle);
|
||||
} // namespace MetaData
|
||||
|
||||
|
||||
@@ -115,60 +115,3 @@ 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<Release::PrimaryType> 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<Release::PrimaryType> parsed {StringUtils::readAs<Release::PrimaryType>(testCase.str)};
|
||||
|
||||
EXPECT_EQ(parsed, testCase.result) << " str was '" << testCase.str << "'";
|
||||
}
|
||||
}
|
||||
|
||||
TEST(MetaData, secondaryReleaseTypes)
|
||||
{
|
||||
using namespace MetaData;
|
||||
|
||||
struct TestCase
|
||||
{
|
||||
std::string str;
|
||||
std::optional<Release::SecondaryType> 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<Release::SecondaryType> parsed {StringUtils::readAs<Release::SecondaryType>(testCase.str)};
|
||||
|
||||
EXPECT_EQ(parsed, testCase.result) << " str was '" << testCase.str << "'";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user