Fixed bad fallback when hitting only a secondary release type as used by picard, fixes #766

This commit is contained in:
emeric
2025-11-10 16:56:17 +01:00
parent c4924d820c
commit f22f4f0a71
4 changed files with 188 additions and 162 deletions
+1 -5
View File
@@ -253,11 +253,7 @@ namespace lms::ui
{ {
Wt::WTemplate* releaseContainer{ releaseContainers->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artist.template.release-container")) }; Wt::WTemplate* releaseContainer{ releaseContainers->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artist.template.release-container")) };
if (releaseType.primaryType || !releaseType.customTypes.empty()) releaseContainer->bindString("release-type", releaseHelpers::buildReleaseTypeString(releaseType));
releaseContainer->bindString("release-type", releaseHelpers::buildReleaseTypeString(releaseType));
else
releaseContainer->bindString("release-type", Wt::WString::tr("Lms.Explore.releases")); // fallback when not tagged with MB or custom type
releases.container = releaseContainer->bindNew<InfiniteScrollingContainer>("releases", Wt::WString::tr("Lms.Explore.Releases.template.container")); releases.container = releaseContainer->bindNew<InfiniteScrollingContainer>("releases", Wt::WString::tr("Lms.Explore.Releases.template.container"));
releases.container->onRequestElements.connect(this, [this, &releases = releases] { releases.container->onRequestElements.connect(this, [this, &releases = releases] {
addSomeReleases(releases); addSomeReleases(releases);
+99 -75
View File
@@ -24,10 +24,13 @@
#include <Wt/WText.h> #include <Wt/WText.h>
#include "core/EnumSet.hpp" #include "core/EnumSet.hpp"
#include "core/Utils.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "Utils.hpp" #include "Utils.hpp"
#include "explore/ReleaseTypes.hpp"
namespace lms::ui::releaseListHelpers namespace lms::ui::releaseListHelpers
{ {
@@ -110,85 +113,106 @@ namespace lms::ui::releaseListHelpers
namespace lms::ui::releaseHelpers namespace lms::ui::releaseHelpers
{ {
namespace
{
static const Wt::WString releaseTypeSeparator{ " · " };
Wt::WString buildPicardReleaseTypeString(const PicardReleaseType& releaseType)
{
Wt::WString res;
switch (releaseType.primaryType)
{
case PicardReleaseType::PrimaryType::Album:
res = Wt::WString::tr("Lms.Explore.Release.type-primary-album");
break;
case PicardReleaseType::PrimaryType::Broadcast:
res = Wt::WString::tr("Lms.Explore.Release.type-primary-broadcast");
break;
case PicardReleaseType::PrimaryType::EP:
res = Wt::WString::tr("Lms.Explore.Release.type-primary-ep");
break;
case PicardReleaseType::PrimaryType::Single:
res = Wt::WString::tr("Lms.Explore.Release.type-primary-single");
break;
case PicardReleaseType::PrimaryType::Other:
res = Wt::WString::tr("Lms.Explore.Release.type-primary-other");
break;
}
for (PicardReleaseType::SecondaryType secondaryType : releaseType.secondaryTypes)
{
if (!res.empty())
res += releaseTypeSeparator;
switch (secondaryType)
{
case PicardReleaseType::SecondaryType::Compilation:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-compilation");
break;
case PicardReleaseType::SecondaryType::Spokenword:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-spokenword");
break;
case PicardReleaseType::SecondaryType::Soundtrack:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-soundtrack");
break;
case PicardReleaseType::SecondaryType::Interview:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-interview");
break;
case PicardReleaseType::SecondaryType::Audiobook:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-audiobook");
break;
case PicardReleaseType::SecondaryType::AudioDrama:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-audiodrama");
break;
case PicardReleaseType::SecondaryType::Live:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-live");
break;
case PicardReleaseType::SecondaryType::Remix:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-remix");
break;
case PicardReleaseType::SecondaryType::DJMix:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-djmix");
break;
case PicardReleaseType::SecondaryType::Mixtape_Street:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-mixtape-street");
break;
case PicardReleaseType::SecondaryType::Demo:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-demo");
break;
case PicardReleaseType::SecondaryType::FieldRecording:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-field-recording");
break;
}
}
return res;
}
Wt::WString buildCustomReleaseTypeString(const CustomReleaseType& releaseType)
{
Wt::WString res;
for (const std::string& customType : releaseType.types)
{
if (!res.empty())
res += releaseTypeSeparator;
res += customType;
}
return res;
}
} // namespace
Wt::WString buildReleaseTypeString(const ReleaseType& releaseType) Wt::WString buildReleaseTypeString(const ReleaseType& releaseType)
{ {
Wt::WString res; Wt::WString res;
if (releaseType.primaryType) std::visit(core::utils::overloads{
{ [&](const PicardReleaseType& picardReleaseType) { res = buildPicardReleaseTypeString(picardReleaseType); },
switch (*releaseType.primaryType) [&](const CustomReleaseType& customReleaseType) { res = buildCustomReleaseTypeString(customReleaseType); } },
{ releaseType);
case PrimaryReleaseType::Album:
res = Wt::WString::tr("Lms.Explore.Release.type-primary-album");
break;
case PrimaryReleaseType::Broadcast:
res = Wt::WString::tr("Lms.Explore.Release.type-primary-broadcast");
break;
case PrimaryReleaseType::EP:
res = Wt::WString::tr("Lms.Explore.Release.type-primary-ep");
break;
case PrimaryReleaseType::Single:
res = Wt::WString::tr("Lms.Explore.Release.type-primary-single");
break;
case PrimaryReleaseType::Other:
res = Wt::WString::tr("Lms.Explore.Release.type-primary-other");
break;
}
}
for (SecondaryReleaseType secondaryType : releaseType.secondaryTypes)
{
if (!res.empty())
res += Wt::WString{ " · " };
switch (secondaryType)
{
case SecondaryReleaseType::Compilation:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-compilation");
break;
case SecondaryReleaseType::Spokenword:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-spokenword");
break;
case SecondaryReleaseType::Soundtrack:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-soundtrack");
break;
case SecondaryReleaseType::Interview:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-interview");
break;
case SecondaryReleaseType::Audiobook:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-audiobook");
break;
case SecondaryReleaseType::AudioDrama:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-audiodrama");
break;
case SecondaryReleaseType::Live:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-live");
break;
case SecondaryReleaseType::Remix:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-remix");
break;
case SecondaryReleaseType::DJMix:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-djmix");
break;
case SecondaryReleaseType::Mixtape_Street:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-mixtape-street");
break;
case SecondaryReleaseType::Demo:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-demo");
break;
case SecondaryReleaseType::FieldRecording:
res += Wt::WString::tr("Lms.Explore.Release.type-secondary-field-recording");
break;
}
}
for (const std::string& customType : releaseType.customTypes)
{
if (!res.empty())
res += Wt::WString{ " · " };
res += customType;
}
return res; return res;
} }
+51 -51
View File
@@ -17,24 +17,24 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "ReleaseTypes.hpp"
#include <tuple> #include <tuple>
#include <unordered_map> #include <unordered_map>
#include "core/String.hpp" #include "core/String.hpp"
#include "ReleaseTypes.hpp"
namespace lms::core::stringUtils namespace lms::core::stringUtils
{ {
template<> template<>
std::optional<ui::PrimaryReleaseType> readAs(std::string_view str) std::optional<ui::PicardReleaseType::PrimaryType> readAs(std::string_view str)
{ {
static const std::unordered_map<std::string, ui::PrimaryReleaseType> entries{ static const std::unordered_map<std::string, ui::PicardReleaseType::PrimaryType> entries{
{ "album", ui::PrimaryReleaseType::Album }, { "album", ui::PicardReleaseType::PrimaryType::Album },
{ "single", ui::PrimaryReleaseType::Single }, { "single", ui::PicardReleaseType::PrimaryType::Single },
{ "ep", ui::PrimaryReleaseType::EP }, { "ep", ui::PicardReleaseType::PrimaryType::EP },
{ "broadcast", ui::PrimaryReleaseType::Broadcast }, { "broadcast", ui::PicardReleaseType::PrimaryType::Broadcast },
{ "other", ui::PrimaryReleaseType::Other }, { "other", ui::PicardReleaseType::PrimaryType::Other },
}; };
const auto it{ entries.find(stringToLower(stringTrim(str))) }; const auto it{ entries.find(stringToLower(stringTrim(str))) };
@@ -45,21 +45,21 @@ namespace lms::core::stringUtils
} }
template<> template<>
std::optional<ui::SecondaryReleaseType> readAs(std::string_view str) std::optional<ui::PicardReleaseType::SecondaryType> readAs(std::string_view str)
{ {
static const std::unordered_map<std::string, ui::SecondaryReleaseType> entries{ static const std::unordered_map<std::string, ui::PicardReleaseType::SecondaryType> entries{
{ "compilation", ui::SecondaryReleaseType::Compilation }, { "compilation", ui::PicardReleaseType::SecondaryType::Compilation },
{ "soundtrack", ui::SecondaryReleaseType::Soundtrack }, { "soundtrack", ui::PicardReleaseType::SecondaryType::Soundtrack },
{ "spokenword", ui::SecondaryReleaseType::Spokenword }, { "spokenword", ui::PicardReleaseType::SecondaryType::Spokenword },
{ "interview", ui::SecondaryReleaseType::Interview }, { "interview", ui::PicardReleaseType::SecondaryType::Interview },
{ "audiobook", ui::SecondaryReleaseType::Audiobook }, { "audiobook", ui::PicardReleaseType::SecondaryType::Audiobook },
{ "audio drama", ui::SecondaryReleaseType::AudioDrama }, { "audio drama", ui::PicardReleaseType::SecondaryType::AudioDrama },
{ "live", ui::SecondaryReleaseType::Live }, { "live", ui::PicardReleaseType::SecondaryType::Live },
{ "remix", ui::SecondaryReleaseType::Remix }, { "remix", ui::PicardReleaseType::SecondaryType::Remix },
{ "dj-mix", ui::SecondaryReleaseType::DJMix }, { "dj-mix", ui::PicardReleaseType::SecondaryType::DJMix },
{ "mixtape/street", ui::SecondaryReleaseType::Mixtape_Street }, { "mixtape/street", ui::PicardReleaseType::SecondaryType::Mixtape_Street },
{ "demo", ui::SecondaryReleaseType::Demo }, { "demo", ui::PicardReleaseType::SecondaryType::Demo },
{ "field recording", ui::SecondaryReleaseType::FieldRecording }, { "field recording", ui::PicardReleaseType::SecondaryType::FieldRecording },
}; };
const auto it{ entries.find(stringToLower(stringTrim(str))) }; const auto it{ entries.find(stringToLower(stringTrim(str))) };
@@ -72,48 +72,48 @@ namespace lms::core::stringUtils
namespace lms::ui namespace lms::ui
{ {
ReleaseType parseReleaseType(const std::vector<std::string>& releaseTypeNames) std::optional<PicardReleaseType> parsePicardReleaseType(const std::vector<std::string>& releaseTypeNames)
{ {
ReleaseType res; if (releaseTypeNames.empty())
return std::nullopt;
for (std::string_view releaseTypeName : releaseTypeNames) const auto primaryType{ core::stringUtils::readAs<PicardReleaseType::PrimaryType>(releaseTypeNames[0]) };
if (!primaryType)
return std::nullopt;
PicardReleaseType res{ .primaryType = *primaryType, .secondaryTypes = {} };
for (std::size_t i{ 1 }; i < releaseTypeNames.size(); ++i)
{ {
if (auto primaryType{ core::stringUtils::readAs<PrimaryReleaseType>(releaseTypeName) }) const auto secondaryType{ core::stringUtils::readAs<PicardReleaseType::SecondaryType>(releaseTypeNames[i]) };
{ if (!secondaryType)
if (!res.primaryType) return std::nullopt;
res.primaryType = primaryType;
else res.secondaryTypes.insert(*secondaryType);
res.customTypes.push_back(std::string{ releaseTypeName });
}
else if (auto secondaryType{ core::stringUtils::readAs<SecondaryReleaseType>(releaseTypeName) })
{
res.secondaryTypes.insert(*secondaryType);
}
else
res.customTypes.push_back(std::string{ releaseTypeName });
} }
return res; return res;
} }
bool operator<(std::optional<PrimaryReleaseType> typeA, std::optional<PrimaryReleaseType> typeB) ReleaseType parseReleaseType(const std::vector<std::string>& releaseTypeNames)
{ {
if (!typeA && typeB) if (const auto picardReleaseType{ parsePicardReleaseType(releaseTypeNames) })
return false; return *picardReleaseType;
else if (typeA && !typeB)
return true; return CustomReleaseType{ .types = releaseTypeNames };
else
return static_cast<int>(*typeA) < static_cast<int>(*typeB);
} }
bool operator<(core::EnumSet<SecondaryReleaseType> typesA, core::EnumSet<SecondaryReleaseType> typesB) bool operator<(core::EnumSet<PicardReleaseType::SecondaryType> lhs, core::EnumSet<PicardReleaseType::SecondaryType> rhs)
{ {
return typesA.getBitfield() < typesB.getBitfield(); return lhs.getBitfield() < rhs.getBitfield();
} }
bool ReleaseType::operator<(const ReleaseType& other) const bool PicardReleaseType::operator<(const PicardReleaseType& other) const
{ {
// TODO : order custom types and compare for each element (size is not to be compared first) return std::tie(primaryType, secondaryTypes) < std::tie(other.primaryType, other.secondaryTypes);
return std::tie(primaryType, secondaryTypes, customTypes) < std::tie(other.primaryType, other.secondaryTypes, other.customTypes); }
bool CustomReleaseType::operator<(const CustomReleaseType& other) const
{
return types < other.types;
} }
} // namespace lms::ui } // namespace lms::ui
+37 -31
View File
@@ -19,49 +19,55 @@
#pragma once #pragma once
#include <optional>
#include <string> #include <string>
#include <variant>
#include <vector> #include <vector>
#include "core/EnumSet.hpp" #include "core/EnumSet.hpp"
namespace lms::ui namespace lms::ui
{ {
// see https://musicbrainz.org/doc/Release_Group/Type struct PicardReleaseType
enum class PrimaryReleaseType
{ {
Album, // see https://musicbrainz.org/doc/Release_Group/Type
Single, enum class PrimaryType : std::uint8_t
EP, {
Broadcast, Album,
Other Single,
EP,
Broadcast,
Other
};
enum class SecondaryType : std::uint8_t
{
Compilation,
Soundtrack,
Spokenword,
Interview,
Audiobook,
AudioDrama,
Live,
Remix,
DJMix,
Mixtape_Street,
Demo,
FieldRecording,
};
PrimaryType primaryType;
core::EnumSet<SecondaryType> secondaryTypes;
bool operator<(const PicardReleaseType&) const;
}; };
enum class SecondaryReleaseType struct CustomReleaseType
{ {
Compilation, std::vector<std::string> types;
Soundtrack,
Spokenword, bool operator<(const CustomReleaseType&) const;
Interview,
Audiobook,
AudioDrama,
Live,
Remix,
DJMix,
Mixtape_Street,
Demo,
FieldRecording,
};
struct ReleaseType
{
std::optional<PrimaryReleaseType> primaryType;
core::EnumSet<SecondaryReleaseType> secondaryTypes;
std::vector<std::string> customTypes;
bool operator<(const ReleaseType& other) const;
}; };
using ReleaseType = std::variant<PicardReleaseType, CustomReleaseType>;
ReleaseType parseReleaseType(const std::vector<std::string>& releaseTypeNames); ReleaseType parseReleaseType(const std::vector<std::string>& releaseTypeNames);
} // namespace lms::ui } // namespace lms::ui