Added performers. fixes #278

This commit is contained in:
emeric
2022-12-14 20:07:54 +01:00
parent 495005357c
commit 998def846d
27 changed files with 451 additions and 96 deletions
+3 -3
View File
@@ -21,15 +21,15 @@
</div>
</div>
${<if-has-releases>}
${<if-section-titles>}<h3>${tr:Lms.Explore.releases}</h3>${</if-section-titles>}
${<if-release-title>}<h3>${tr:Lms.Explore.releases}</h3>${</if-release-title>}
${releases class="mb-3"}
${</if-has-releases>}
${<if-has-appears-on-releases>}
${<if-section-titles>}<h3>${tr:Lms.Explore.Artist.appears-on}</h3>${</if-section-titles>}
<h3>${tr:Lms.Explore.Artist.appears-on}</h3>
${appears-on-releases class="mb-3"}
${</if-has-appears-on-releases>}
${<if-has-non-release-tracks>}
${<if-section-titles>}<h3>${tr:Lms.Explore.tracks}</h3>${</if-section-titles>}
<h3>${tr:Lms.Explore.tracks}</h3>
${tracks class="mb-3"}
${</if-has-non-release-tracks>}
${<if-has-similar-artists>}
+1 -1
View File
@@ -43,7 +43,7 @@
<message id="Lms.Explore.template.info.artists">
<div class="row align-items-center">
<div class="col-4 col-sm-3">
<div class="col-4">
${type}
</div>
<div class="col">
+4
View File
@@ -175,6 +175,10 @@
<plural case="0">Mixer</plural>
<plural case="1">Mixers</plural>
</message>
<message id="Lms.Explore.Artists.linktype-performer">
<plural case="0">Performer</plural>
<plural case="1">Performers</plural>
</message>
<message id="Lms.Explore.Artists.linktype-producer">
<plural case="0">Producer</plural>
<plural case="1">Producers</plural>
+4
View File
@@ -175,6 +175,10 @@
<plural case="0">Mixeur</plural>
<plural case="1">Mixeurs</plural>
</message>
<message id="Lms.Explore.Artists.linktype-performer">
<plural case="0">Interprète</plural>
<plural case="1">Interprètes</plural>
</message>
<message id="Lms.Explore.Artists.linktype-producer">
<plural case="0">Producteur</plural>
<plural case="1">Producteurs</plural>
+4
View File
@@ -183,6 +183,10 @@
+4
View File
@@ -183,6 +183,10 @@
+3 -3
View File
@@ -87,7 +87,7 @@
<message id="Lms.Explore.Release.template.release-info">
<div class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${tr:Lms.Explore.release-info}</h5>
@@ -98,7 +98,7 @@
${artist-table class="d-grid gap-2"}
${<if-has-codec>}
<div class="row align-items-center">
<div class="col-4 col-sm-3">
<div class="col-4">
${tr:Lms.Explore.codec}
</div>
<div class="col">
@@ -108,7 +108,7 @@
${</if-has-codec>}
${<if-has-bitrate>}
<div class="row align-items-center">
<div class="col-4 col-sm-3">
<div class="col-4">
${tr:Lms.Explore.bitrate}
</div>
<div class="col">
+5 -5
View File
@@ -70,7 +70,7 @@
<message id="Lms.Explore.Tracks.template.track-info">
<div class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${tr:Lms.Explore.track-info}</h5>
@@ -80,7 +80,7 @@
<div class="container-fluid d-grid gap-2">
${<if-has-clusters>}
<div class="row align-items-center">
<div class="col-4 col-sm-3">
<div class="col-4">
${tr:Lms.clusters}
</div>
<div class="col">
@@ -90,7 +90,7 @@
${</if-has-clusters>}
${artist-table class="d-grid gap-2"}
<div class="row align-items-center">
<div class="col-4 col-sm-3">
<div class="col-4">
${tr:Lms.Explore.duration}
</div>
<div class="col">
@@ -99,7 +99,7 @@
</div>
${<if-has-codec>}
<div class="row align-items-center">
<div class="col-4 col-sm-3">
<div class="col-4">
${tr:Lms.Explore.codec}
</div>
<div class="col">
@@ -109,7 +109,7 @@
${</if-has-codec>}
${<if-has-bitrate>}
<div class="row align-items-center">
<div class="col-4 col-sm-3">
<div class="col-4">
${tr:Lms.Explore.bitrate}
</div>
<div class="col">
+52 -12
View File
@@ -132,6 +132,48 @@ getArtists(const TagLib::PropertyMap& properties,
return artists;
}
static
PerformerContainer
getPerformerArtists(const TagLib::PropertyMap& properties,
const std::vector<std::string_view>& artistTagNames)
{
PerformerContainer performers;
// picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer)
// PERFORMER: artist (role)
if (const std::vector<std::string> artistNames {getPropertyValuesFirstMatchAs<std::string>(properties, artistTagNames)}; !artistNames.empty())
{
for (std::string_view entry : artistNames)
{
Utils::PerformerArtist performer {Utils::extractPerformerAndRole(entry)};
StringUtils::capitalize(performer.role);
performers[performer.role].push_back(std::move(performer.artist));
}
}
// PERFORMER:role (MP3)
else
{
for (const auto& [key, values] : properties)
{
if (key.startsWith("PERFORMER"))
{
std::string performerStr {key.to8Bit(true)};
std::string role;
if (const std::size_t rolePos {performerStr.find(':')}; rolePos != std::string::npos)
{
role = StringUtils::stringToLower(performerStr.substr(rolePos + 1, performerStr.size() - rolePos + 1));
StringUtils::capitalize(role);
}
for (const auto& value : values)
performers[role].push_back(Artist {value.to8Bit(true)});
}
}
}
return performers;
}
static
std::optional<Album>
getAlbum(const TagLib::PropertyMap& properties)
@@ -140,7 +182,7 @@ getAlbum(const TagLib::PropertyMap& properties)
if (albumName.empty())
return std::nullopt;
const std::vector<UUID> albumMBID {getPropertyValuesFirstMatchAs<UUID>(properties, {"MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID"})};
const std::vector<UUID> albumMBID {getPropertyValuesFirstMatchAs<UUID>(properties, {"MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID"})};
if (albumMBID.empty())
return Album {std::move(albumName.front()), {}};
@@ -187,12 +229,14 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
if (tag == "TITLE")
track.title = value;
else if (tag == "MUSICBRAINZ_RELEASETRACKID"
|| tag == "MUSICBRAINZ RELEASE TRACK ID")
|| tag == "MUSICBRAINZ RELEASE TRACK ID"
|| tag == "MUSICBRAINZ/RELEASE TRACK ID")
{
track.trackMBID = UUID::fromString(value);
}
else if (tag == "MUSICBRAINZ_TRACKID"
|| tag == "MUSICBRAINZ TRACK ID")
|| tag == "MUSICBRAINZ TRACK ID"
|| tag == "MUSICBRAINZ/TRACK ID")
track.recordingMBID = UUID::fromString(value);
else if (tag == "ACOUSTID_ID")
track.acoustID = UUID::fromString(value);
@@ -416,23 +460,19 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
track.hasCover = true;
}
for (const auto& property : properties)
{
const std::string tag {property.first.upper().to8Bit(true)};
const TagLib::StringList& values {property.second};
processTag(track, tag, values, debug);
}
for (const auto& [tag, values] : properties)
processTag(track, tag.upper().to8Bit(true), values, debug);
track.album = getAlbum(properties);
track.artists = getArtists(properties, {"ARTISTS", "ARTIST"}, {"ARTISTSORT"}, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID"});
track.albumArtists = getArtists(properties, {"ALBUMARTISTS", "ALBUMARTIST"}, {"ALBUMARTISTSSORT", "ALBUMARTISTSORT"}, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID"});
track.artists = getArtists(properties, {"ARTISTS", "ARTIST"}, {"ARTISTSORT"}, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID"});
track.albumArtists = getArtists(properties, {"ALBUMARTISTS", "ALBUMARTIST"}, {"ALBUMARTISTSSORT", "ALBUMARTISTSORT"}, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"});
track.conductorArtists = getArtists(properties, {"CONDUCTORS", "CONDUCTOR"}, {"CONDUCTORSSORT", "CONDUCTORSORT"}, {});
track.composerArtists = getArtists(properties, {"COMPOSERS", "COMPOSER"}, {"COMPOSERSSORT", "COMPOSERSORT"}, {});
track.lyricistArtists = getArtists(properties, {"LYRICISTS", "LYRICIST"}, {"LYRICISTSSORT", "LYRICISTSORT"}, {});
track.mixerArtists = getArtists(properties, {"MIXERS", "MIXER"}, {"MIXERSSORT", "MIXERSORT"}, {});
track.producerArtists = getArtists(properties, {"PRODUCERS", "PRODUCER"}, {"PRODUCERSSORT", "PRODUCERSORT"}, {});
track.remixerArtists = getArtists(properties, {"REMIXERS", "REMIXER", "ModifiedBy"}, {"REMIXERSSORT", "REMIXERSORT"}, {});
track.performerArtists = getPerformerArtists(properties, {"PERFORMERS", "PERFORMER"});
return track;
}
+46
View File
@@ -71,5 +71,51 @@ namespace MetaData::Utils
throw LmsException {"Unknown read style"};
}
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 {};
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 (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 (!roleEnd || !roleBegin)
artistName = StringUtils::stringTrim(entry);
return PerformerArtist {Artist {artistName}, std::string {role}};
}
}
+10
View File
@@ -18,6 +18,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <string_view>
#include <Wt/WDate.h>
#include "metadata/IParser.hpp"
@@ -26,5 +27,14 @@ namespace MetaData::Utils
{
Wt::WDate parseDate(const std::string& dateStr);
std::string_view readStyleToString(ParserReadStyle readStyle);
struct PerformerArtist
{
Artist artist;
std::string role;
};
// format is "artist name (role)"
PerformerArtist extractPerformerAndRole(std::string_view entry);
}
@@ -24,6 +24,7 @@
#include <map>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <vector>
@@ -44,6 +45,8 @@ namespace MetaData
Artist(std::string_view _name, std::optional<std::string> _sortName, std::optional<UUID> _musicBrainzArtistID) : name {_name}, sortName {_sortName}, musicBrainzArtistID {_musicBrainzArtistID} {}
};
using PerformerContainer = std::map<std::string /*role*/, std::vector<Artist>>;
struct Album
{
std::string name;
@@ -83,6 +86,7 @@ namespace MetaData
std::vector<Artist> composerArtists;
std::vector<Artist> lyricistArtists;
std::vector<Artist> mixerArtists;
PerformerContainer performerArtists;
std::vector<Artist> producerArtists;
std::vector<Artist> remixerArtists;
};
+44 -1
View File
@@ -62,7 +62,6 @@ TEST(MetaData, parseDate)
{ "1995-05/09", Wt::WDate {} }, // invalid mixup separators
};
for (const TestCase& testCase : testCases)
{
const Wt::WDate parsed {parseDate(testCase.str)};
@@ -72,3 +71,47 @@ TEST(MetaData, parseDate)
EXPECT_EQ(parsed.day(), testCase.result.day()) << " str was '" << testCase.str << "'";
}
}
TEST(MetaData, extractPerformerAndRole)
{
using namespace MetaData::Utils;
struct TestCase
{
std::string str;
std::string expectedArtistName;
std::string expectedRole;
} testCases []
{
{ "", "", "" },
{ "(myrole)", "", "myrole" },
{ "(my role)", "", "my role" },
{ " ( my role ) ", "", "my role" },
{ " (()) ", "", "()" },
{ ")", ")", "" },
{ "(", "(", "" },
{ "artist name (my role)", "artist name", "my role" },
{ "artist name ()", "artist name", "" },
{ "artist name ( )", "artist name", "" },
{ "artist (subname) name", "artist (subname) name", "" },
{ " artist name ( my role )", "artist name", "my role" },
{ "artist name (artist subname) (my role)", "artist name (artist subname)", "my role" },
{ "artist name", "artist name", "" },
{ " artist name ", "artist name", "" },
{ "artist name (", "artist name (", "" },
{ "artist name )", "artist name )", "" },
{ "artist name (()", "artist name (", "" },
{ "artist name (())", "artist name", "()" },
{ "artist name ( () )", "artist name", "()" },
{ "artist name (drums (drum set))", "artist name", "drums (drum set)" },
{ "artist name ( drums (drum set) )", "artist name", "drums (drum set)" },
};
for (const TestCase& testCase : testCases)
{
PerformerArtist performer {extractPerformerAndRole(testCase.str)};
EXPECT_EQ(performer.artist.name, testCase.expectedArtistName) << " str was '" << testCase.str << "'";
EXPECT_EQ(performer.role, testCase.expectedRole) << " str was '" << testCase.str << "'";
}
}
@@ -600,6 +600,17 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
ScanSettings::get(session).modify()->incScanVersion();
}
static
void
migrateFromV37(Session& session)
{
// Support Performer tags (via subtypes)
session.getDboSession().execute("ALTER TABLE track_artist_link ADD subtype 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)
{
@@ -643,6 +654,7 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
{34, migrateFromV34},
{35, migrateFromV35},
{36, migrateFromV36},
{37, migrateFromV37},
};
while (1)
@@ -26,7 +26,7 @@ namespace Database
class Session;
using Version = std::size_t;
static constexpr Version LMS_DATABASE_VERSION {37};
static constexpr Version LMS_DATABASE_VERSION {38};
class VersionInfo
{
public:
@@ -24,27 +24,70 @@
#include "services/database/Track.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
namespace Database {
namespace Database
{
TrackArtistLink::TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type)
: _type {type},
_track {getDboPtr(track)},
_artist {getDboPtr(artist)}
static
Wt::Dbo::Query<TrackArtistLinkId>
createQuery(Session& session, const TrackArtistLink::FindParameters& params)
{
session.checkSharedLocked();
auto query {session.getDboSession().query<TrackArtistLinkId>("SELECT DISTINCT t_a_l.id FROM track_artist_link t_a_l")};
if (params.linkType)
query.where("t_a_l.type = ?").bind(*params.linkType);
if (params.track.isValid() || params.release.isValid())
query.join("track t ON t.id = t_a_l.track_id");
if (params.track.isValid())
query.where("t.id = ?").bind(params.track);
if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release);
return query;
}
TrackArtistLink::TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
: _type {type}
, _subType {subType}
, _track {getDboPtr(track)}
, _artist {getDboPtr(artist)}
{
}
TrackArtistLink::pointer
TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type)
TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
{
session.checkUniqueLocked();
TrackArtistLink::pointer res {session.getDboSession().add(std::make_unique<TrackArtistLink>(track, artist, type))};
TrackArtistLink::pointer res {session.getDboSession().add(std::make_unique<TrackArtistLink>(track, artist, type, subType))};
session.getDboSession().flush();
return res;
}
TrackArtistLink::pointer
TrackArtistLink::find(Session& session, TrackArtistLinkId id)
{
session.checkSharedLocked();
return session.getDboSession().find<TrackArtistLink>().where("id = ?").bind(id).resultValue();
}
RangeResults<TrackArtistLinkId>
TrackArtistLink::find(Session& session, const FindParameters& params)
{
session.checkSharedLocked();
auto query {createQuery(session, params)};
return Utils::execQuery(query, params.range);
}
EnumSet<TrackArtistLinkType>
TrackArtistLink::findUsedTypes(Session& session)
{
@@ -54,6 +97,5 @@ TrackArtistLink::findUsedTypes(Session& session)
return EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res));
}
}
@@ -87,7 +87,6 @@ class Artist : public Object<Artist, ArtistId>
static RangeResults<ArtistId> findAllOrphans(Session& session, Range range); // No track related
static bool exists(Session& session, ArtistId id);
// Accessors
const std::string& getName() const { return _name; }
const std::string& getSortName() const { return _sortName; }
@@ -20,11 +20,14 @@
#pragma once
#include <string>
#include <string_view>
#include <Wt/Dbo/Dbo.h>
#include "services/database/IdType.hpp"
#include "services/database/Object.hpp"
#include "services/database/ReleaseId.hpp"
#include "services/database/TrackId.hpp"
#include "services/database/Types.hpp"
#include "utils/EnumSet.hpp"
@@ -40,20 +43,37 @@ namespace Database
class TrackArtistLink : public Object<TrackArtistLink, TrackArtistLinkId>
{
public:
TrackArtistLink() = default;
TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type);
struct FindParameters
{
Range range;
std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
TrackId track; // artists involved in this track
ReleaseId release; // artists involved in this release
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type);
FindParameters& setRange(Range _range) {range = _range; return *this; }
FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
};
TrackArtistLink() = default;
TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType);
static RangeResults<TrackArtistLinkId> find(Session& session, const FindParameters& parameters);
static pointer find(Session& session, TrackArtistLinkId linkId);
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType = {});
static EnumSet<TrackArtistLinkType> findUsedTypes(Session& session);
ObjectPtr<Track> getTrack() const { return _track; }
ObjectPtr<Artist> getArtist() const { return _artist; }
TrackArtistLinkType getType() const { return _type; }
std::string_view getSubType() const { return _subType; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _type, "type");
Wt::Dbo::field(a, _subType, "subtype");
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
@@ -61,10 +81,10 @@ namespace Database
private:
TrackArtistLinkType _type;
std::string _subType;
Wt::Dbo::ptr<Track> _track;
Wt::Dbo::ptr<Artist> _artist;
};
}
@@ -837,6 +837,12 @@ ScannerService::scanAudioFile(const std::filesystem::path& file, bool forceScan,
for (const Artist::pointer& mixer : getOrCreateArtists(_dbSession, trackInfo->mixerArtists, true))
track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, mixer, TrackArtistLinkType::Mixer));
for (const auto& [role, performers] : trackInfo->performerArtists)
{
for (const Artist::pointer& performer : getOrCreateArtists(_dbSession, performers, true))
track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, performer, TrackArtistLinkType::Performer, role));
}
for (const Artist::pointer& producer : getOrCreateArtists(_dbSession, trackInfo->producerArtists, true))
track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, producer, TrackArtistLinkType::Producer));
+25 -6
View File
@@ -115,17 +115,21 @@ joinStrings(const std::vector<std::string>& strings, const std::string& delimite
return boost::algorithm::join(strings, delimiter);
}
std::string
std::string_view
stringTrim(std::string_view str, std::string_view whitespaces)
{
std::string_view res;
const auto strBegin = str.find_first_not_of(whitespaces);
if (strBegin == std::string_view::npos)
return ""; // no content
if (strBegin != std::string_view::npos)
{
const auto strEnd {str.find_last_not_of(whitespaces)};
const auto strRange {strEnd - strBegin + 1};
const auto strEnd = str.find_last_not_of(whitespaces);
const auto strRange = strEnd - strBegin + 1;
res = str.substr(strBegin, strRange);
}
return std::string {str.substr(strBegin, strRange)};
return res;
}
std::string
@@ -175,6 +179,21 @@ bufferToString(const std::vector<unsigned char>& data)
return oss.str();
}
void
capitalize(std::string& str)
{
for (auto it {std::begin(str)}; it != std::end(str); ++it)
{
if (std::isspace(*it))
continue;
if (std::isalpha(*it))
*it = std::toupper(*it);
break;
}
}
std::string
replaceInString(std::string_view str, const std::string& from, const std::string& to)
{
+4 -1
View File
@@ -44,7 +44,7 @@ std::string
joinStrings(const std::vector<std::string>& strings, const std::string& delimiter);
[[nodiscard]]
std::string
std::string_view
stringTrim(std::string_view str, std::string_view whitespaces = " \t");
[[nodiscard]]
@@ -66,6 +66,9 @@ stringToUpper(const std::string& str);
std::string
bufferToString(const std::vector<unsigned char>& data);
void
capitalize(std::string& str);
template<typename T>
[[nodiscard]]
std::optional<T> readAs(std::string_view str)
+29
View File
@@ -118,3 +118,32 @@ TEST(StringUtils, readAs)
EXPECT_EQ(StringUtils::readAs<bool>("foo"), std::nullopt);
EXPECT_EQ(StringUtils::readAs<bool>(""), std::nullopt);
}
TEST(StrinUtils, capitalize)
{
struct TestCase
{
std::string input;
std::string expectedOutput;
};
TestCase tests[]
{
{"", ""},
{"C", "C"},
{"c", "C"},
{" c", " C"},
{" cc", " Cc"},
{"(c", "(c"},
{"1c", "1c"},
{"&c", "&c"},
{"c c", "C c"}
};
for (const TestCase& test : tests)
{
std::string str {test.input};
StringUtils::capitalize(str);
EXPECT_EQ(str, test.expectedOutput) << " str was '" << test.input << "'";
}
}
+5 -10
View File
@@ -114,19 +114,14 @@ Artist::refreshView()
LmsApp->setTitle(artist->getName());
_artistId = *artistId;
std::size_t sectionCount{};
if (refreshReleases())
sectionCount++;
if (refreshAppearsOnReleases())
sectionCount++;
if (refreshNonReleaseTracks())
sectionCount++;
bool setReleaseTitle {};
refreshReleases();
setReleaseTitle |= refreshAppearsOnReleases();
setReleaseTitle |= refreshNonReleaseTracks();
refreshLinks(artist);
refreshSimilarArtists(similarArtistIds);
if (sectionCount > 1)
setCondition("if-section-titles", true);
setCondition("if-release-title", setReleaseTitle);
Wt::WContainerWidget* clusterContainers {bindNew<Wt::WContainerWidget>("clusters")};
+2
View File
@@ -145,12 +145,14 @@ Artists::refreshArtistLinkTypes()
// add default one first (none)
linkTypeModel->add(Wt::WString::tr("Lms.Explore.Artists.linktype-all"), std::nullopt);
// TODO: sort by translated strings
addTypeIfUsed(TrackArtistLinkType::Artist, "Lms.Explore.Artists.linktype-artist");
addTypeIfUsed(TrackArtistLinkType::ReleaseArtist, "Lms.Explore.Artists.linktype-releaseartist");
addTypeIfUsed(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
addTypeIfUsed(TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor");
addTypeIfUsed(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist");
addTypeIfUsed(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
addTypeIfUsed(TrackArtistLinkType::Performer, "Lms.Explore.Artists.linktype-performer");
addTypeIfUsed(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
addTypeIfUsed(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
}
+36 -5
View File
@@ -19,6 +19,7 @@
#include "ReleaseView.hpp"
#include <map>
#include <Wt/WAnchor.h>
#include <Wt/WImage.h>
#include <Wt/WPushButton.h>
@@ -30,6 +31,7 @@
#include "services/database/ScanSettings.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/TrackArtistLink.hpp"
#include "services/recommendation/IRecommendationService.hpp"
#include "services/scrobbling/IScrobblingService.hpp"
#include "utils/Logger.hpp"
@@ -64,6 +66,7 @@ showReleaseInfoModal(Database::ReleaseId releaseId)
releaseInfo->addFunction("tr", &Wt::WTemplate::Functions::tr);
Wt::WContainerWidget* artistTable {releaseInfo->bindNew<Wt::WContainerWidget>("artist-table")};
std::map<Wt::WString, std::set<ArtistId>> artistMap;
auto addArtists = [&](TrackArtistLinkType linkType, const char* type)
{
@@ -74,11 +77,28 @@ showReleaseInfoModal(Database::ReleaseId releaseId)
if (artistIds.results.empty())
return;
std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistContainer(artistIds.results)};
auto artistsEntry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists"))};
artistsEntry->bindString("type", Wt::WString::trn(type, artistContainer->count()));
artistsEntry->bindWidget("artist-container", std::move(artistContainer));
artistTable->addWidget(std::move(artistsEntry));
Wt::WString typeStr {Wt::WString::trn(type, artistIds.results.size())};;
for (ArtistId artistId : artistIds.results)
artistMap[typeStr].insert(artistId);
};
auto addPerformerArtists = [&]
{
TrackArtistLink::FindParameters params;
params.setRelease(releaseId);
params.setLinkType(TrackArtistLinkType::Performer);
const auto links {TrackArtistLink::find(LmsApp->getDbSession(), params)};
if (links.results.empty())
return;
for (const TrackArtistLinkId linkId : links.results)
{
const TrackArtistLink::pointer link {TrackArtistLink::find(LmsApp->getDbSession(), linkId)};
if (!link)
continue;
artistMap[std::string {link->getSubType()}].insert(link->getArtist()->getId());
}
};
addArtists(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
@@ -87,6 +107,17 @@ showReleaseInfoModal(Database::ReleaseId releaseId)
addArtists(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
addArtists(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
addArtists(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
addPerformerArtists();
for (const auto& [role, artistIds] : artistMap)
{
std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistContainer(std::vector (std::cbegin(artistIds), std::cend(artistIds)))};
auto artistsEntry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists"))};
artistsEntry->bindString("type", role);
artistsEntry->bindWidget("artist-container", std::move(artistContainer));
artistTable->addWidget(std::move(artistsEntry));
}
// TODO: save in DB and mean all this
for (TrackId trackId : Track::find(LmsApp->getDbSession(), Track::FindParameters {}.setRelease(releaseId).setRange(Range {0, 1})).results)
+36 -6
View File
@@ -19,6 +19,7 @@
#include "TrackListHelpers.hpp"
#include <map>
#include <Wt/WAnchor.h>
#include <Wt/WImage.h>
#include <Wt/WPushButton.h>
@@ -26,9 +27,10 @@
#include "av/IAudioFile.hpp"
#include "services/database/Artist.hpp"
#include "services/database/Release.hpp"
#include "services/scrobbling/IScrobblingService.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/TrackArtistLink.hpp"
#include "services/scrobbling/IScrobblingService.hpp"
#include "utils/Service.hpp"
#include "common/Template.hpp"
@@ -58,6 +60,7 @@ namespace UserInterface::TrackListHelpers
trackInfo->addFunction("tr", &Wt::WTemplate::Functions::tr);
Wt::WContainerWidget* artistTable {trackInfo->bindNew<Wt::WContainerWidget>("artist-table")};
std::map<Wt::WString, std::set<ArtistId>> artistMap;
auto addArtists = [&](TrackArtistLinkType linkType, const char* type)
{
@@ -68,11 +71,28 @@ namespace UserInterface::TrackListHelpers
if (artistIds.results.empty())
return;
std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistContainer(artistIds.results)};
auto artistsEntry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists"))};
artistsEntry->bindString("type", Wt::WString::trn(type, artistContainer->count()));
artistsEntry->bindWidget("artist-container", std::move(artistContainer));
artistTable->addWidget(std::move(artistsEntry));
Wt::WString typeStr {Wt::WString::trn(type, artistIds.results.size())};;
for (ArtistId artistId : artistIds.results)
artistMap[typeStr].insert(artistId);
};
auto addPerformerArtists = [&]
{
TrackArtistLink::FindParameters params;
params.setTrack(trackId);
params.setLinkType(TrackArtistLinkType::Performer);
const auto links {TrackArtistLink::find(LmsApp->getDbSession(), params)};
if (links.results.empty())
return;
for (const TrackArtistLinkId linkId : links.results)
{
const TrackArtistLink::pointer link {TrackArtistLink::find(LmsApp->getDbSession(), linkId)};
if (!link)
continue;
artistMap[std::string {link->getSubType()}].insert(link->getArtist()->getId());
}
};
addArtists(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
@@ -81,6 +101,16 @@ namespace UserInterface::TrackListHelpers
addArtists(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
addArtists(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
addArtists(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
addPerformerArtists();
for (const auto& [role, artistIds] : artistMap)
{
std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistContainer(std::vector (std::cbegin(artistIds), std::cend(artistIds)))};
auto artistsEntry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists"))};
artistsEntry->bindString("type", role);
artistsEntry->bindWidget("artist-container", std::move(artistContainer));
artistTable->addWidget(std::move(artistsEntry));
}
if (const auto audioFile {Av::parseAudioFile(track->getPath())})
{
+10 -2
View File
@@ -52,8 +52,6 @@ std::ostream& operator<<(std::ostream& os, const MetaData::Album& album)
return os;
}
void parse(MetaData::IParser& parser, const std::filesystem::path& file)
{
using namespace MetaData;
@@ -91,6 +89,16 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
for (const Artist& artist : track->mixerArtists)
std::cout << "Mixer: " << artist << std::endl;
for (const auto& [role, artists] : track->performerArtists)
{
std::cout << "Performer";
if (!role.empty())
std::cout << " (" << role << ")";
std::cout << ":" << std::endl;
for (const Artist& artist : artists)
std::cout << "\t" << artist << std::endl;
}
for (const Artist& artist : track->producerArtists)
std::cout << "Producer: " << artist << std::endl;