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>
</div> </div>
${<if-has-releases>} ${<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"} ${releases class="mb-3"}
${</if-has-releases>} ${</if-has-releases>}
${<if-has-appears-on-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"} ${appears-on-releases class="mb-3"}
${</if-has-appears-on-releases>} ${</if-has-appears-on-releases>}
${<if-has-non-release-tracks>} ${<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"} ${tracks class="mb-3"}
${</if-has-non-release-tracks>} ${</if-has-non-release-tracks>}
${<if-has-similar-artists>} ${<if-has-similar-artists>}
+1 -1
View File
@@ -43,7 +43,7 @@
<message id="Lms.Explore.template.info.artists"> <message id="Lms.Explore.template.info.artists">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-4 col-sm-3"> <div class="col-4">
${type} ${type}
</div> </div>
<div class="col"> <div class="col">
+4
View File
@@ -175,6 +175,10 @@
<plural case="0">Mixer</plural> <plural case="0">Mixer</plural>
<plural case="1">Mixers</plural> <plural case="1">Mixers</plural>
</message> </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"> <message id="Lms.Explore.Artists.linktype-producer">
<plural case="0">Producer</plural> <plural case="0">Producer</plural>
<plural case="1">Producers</plural> <plural case="1">Producers</plural>
+4
View File
@@ -175,6 +175,10 @@
<plural case="0">Mixeur</plural> <plural case="0">Mixeur</plural>
<plural case="1">Mixeurs</plural> <plural case="1">Mixeurs</plural>
</message> </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"> <message id="Lms.Explore.Artists.linktype-producer">
<plural case="0">Producteur</plural> <plural case="0">Producteur</plural>
<plural case="1">Producteurs</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"> <message id="Lms.Explore.Release.template.release-info">
<div class="modal fade" tabindex="-1"> <div class="modal fade" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">${tr:Lms.Explore.release-info}</h5> <h5 class="modal-title">${tr:Lms.Explore.release-info}</h5>
@@ -98,7 +98,7 @@
${artist-table class="d-grid gap-2"} ${artist-table class="d-grid gap-2"}
${<if-has-codec>} ${<if-has-codec>}
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-4 col-sm-3"> <div class="col-4">
${tr:Lms.Explore.codec} ${tr:Lms.Explore.codec}
</div> </div>
<div class="col"> <div class="col">
@@ -108,7 +108,7 @@
${</if-has-codec>} ${</if-has-codec>}
${<if-has-bitrate>} ${<if-has-bitrate>}
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-4 col-sm-3"> <div class="col-4">
${tr:Lms.Explore.bitrate} ${tr:Lms.Explore.bitrate}
</div> </div>
<div class="col"> <div class="col">
+5 -5
View File
@@ -70,7 +70,7 @@
<message id="Lms.Explore.Tracks.template.track-info"> <message id="Lms.Explore.Tracks.template.track-info">
<div class="modal fade" tabindex="-1"> <div class="modal fade" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">${tr:Lms.Explore.track-info}</h5> <h5 class="modal-title">${tr:Lms.Explore.track-info}</h5>
@@ -80,7 +80,7 @@
<div class="container-fluid d-grid gap-2"> <div class="container-fluid d-grid gap-2">
${<if-has-clusters>} ${<if-has-clusters>}
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-4 col-sm-3"> <div class="col-4">
${tr:Lms.clusters} ${tr:Lms.clusters}
</div> </div>
<div class="col"> <div class="col">
@@ -90,7 +90,7 @@
${</if-has-clusters>} ${</if-has-clusters>}
${artist-table class="d-grid gap-2"} ${artist-table class="d-grid gap-2"}
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-4 col-sm-3"> <div class="col-4">
${tr:Lms.Explore.duration} ${tr:Lms.Explore.duration}
</div> </div>
<div class="col"> <div class="col">
@@ -99,7 +99,7 @@
</div> </div>
${<if-has-codec>} ${<if-has-codec>}
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-4 col-sm-3"> <div class="col-4">
${tr:Lms.Explore.codec} ${tr:Lms.Explore.codec}
</div> </div>
<div class="col"> <div class="col">
@@ -109,7 +109,7 @@
${</if-has-codec>} ${</if-has-codec>}
${<if-has-bitrate>} ${<if-has-bitrate>}
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-4 col-sm-3"> <div class="col-4">
${tr:Lms.Explore.bitrate} ${tr:Lms.Explore.bitrate}
</div> </div>
<div class="col"> <div class="col">
+52 -12
View File
@@ -132,6 +132,48 @@ getArtists(const TagLib::PropertyMap& properties,
return artists; 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 static
std::optional<Album> std::optional<Album>
getAlbum(const TagLib::PropertyMap& properties) getAlbum(const TagLib::PropertyMap& properties)
@@ -140,7 +182,7 @@ getAlbum(const TagLib::PropertyMap& properties)
if (albumName.empty()) if (albumName.empty())
return std::nullopt; 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()) if (albumMBID.empty())
return Album {std::move(albumName.front()), {}}; return Album {std::move(albumName.front()), {}};
@@ -187,12 +229,14 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
if (tag == "TITLE") if (tag == "TITLE")
track.title = value; track.title = value;
else if (tag == "MUSICBRAINZ_RELEASETRACKID" 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); track.trackMBID = UUID::fromString(value);
} }
else if (tag == "MUSICBRAINZ_TRACKID" else if (tag == "MUSICBRAINZ_TRACKID"
|| tag == "MUSICBRAINZ TRACK ID") || tag == "MUSICBRAINZ TRACK ID"
|| tag == "MUSICBRAINZ/TRACK ID")
track.recordingMBID = UUID::fromString(value); track.recordingMBID = UUID::fromString(value);
else if (tag == "ACOUSTID_ID") else if (tag == "ACOUSTID_ID")
track.acoustID = UUID::fromString(value); track.acoustID = UUID::fromString(value);
@@ -416,23 +460,19 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
track.hasCover = true; track.hasCover = true;
} }
for (const auto& property : properties) for (const auto& [tag, values] : properties)
{ processTag(track, tag.upper().to8Bit(true), values, debug);
const std::string tag {property.first.upper().to8Bit(true)};
const TagLib::StringList& values {property.second};
processTag(track, tag, values, debug);
}
track.album = getAlbum(properties); track.album = getAlbum(properties);
track.artists = getArtists(properties, {"ARTISTS", "ARTIST"}, {"ARTISTSORT"}, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ 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"}); 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.conductorArtists = getArtists(properties, {"CONDUCTORS", "CONDUCTOR"}, {"CONDUCTORSSORT", "CONDUCTORSORT"}, {});
track.composerArtists = getArtists(properties, {"COMPOSERS", "COMPOSER"}, {"COMPOSERSSORT", "COMPOSERSORT"}, {}); track.composerArtists = getArtists(properties, {"COMPOSERS", "COMPOSER"}, {"COMPOSERSSORT", "COMPOSERSORT"}, {});
track.lyricistArtists = getArtists(properties, {"LYRICISTS", "LYRICIST"}, {"LYRICISTSSORT", "LYRICISTSORT"}, {}); track.lyricistArtists = getArtists(properties, {"LYRICISTS", "LYRICIST"}, {"LYRICISTSSORT", "LYRICISTSORT"}, {});
track.mixerArtists = getArtists(properties, {"MIXERS", "MIXER"}, {"MIXERSSORT", "MIXERSORT"}, {}); track.mixerArtists = getArtists(properties, {"MIXERS", "MIXER"}, {"MIXERSSORT", "MIXERSORT"}, {});
track.producerArtists = getArtists(properties, {"PRODUCERS", "PRODUCER"}, {"PRODUCERSSORT", "PRODUCERSORT"}, {}); track.producerArtists = getArtists(properties, {"PRODUCERS", "PRODUCER"}, {"PRODUCERSSORT", "PRODUCERSORT"}, {});
track.remixerArtists = getArtists(properties, {"REMIXERS", "REMIXER", "ModifiedBy"}, {"REMIXERSSORT", "REMIXERSORT"}, {}); track.remixerArtists = getArtists(properties, {"REMIXERS", "REMIXER", "ModifiedBy"}, {"REMIXERSSORT", "REMIXERSORT"}, {});
track.performerArtists = getPerformerArtists(properties, {"PERFORMERS", "PERFORMER"});
return track; return track;
} }
+46
View File
@@ -71,5 +71,51 @@ namespace MetaData::Utils
throw LmsException {"Unknown read style"}; 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/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include <string_view>
#include <Wt/WDate.h> #include <Wt/WDate.h>
#include "metadata/IParser.hpp" #include "metadata/IParser.hpp"
@@ -26,5 +27,14 @@ namespace MetaData::Utils
{ {
Wt::WDate parseDate(const std::string& dateStr); Wt::WDate parseDate(const std::string& dateStr);
std::string_view readStyleToString(ParserReadStyle readStyle); 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 <map>
#include <optional> #include <optional>
#include <set> #include <set>
#include <string>
#include <string_view> #include <string_view>
#include <vector> #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} {} 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 struct Album
{ {
std::string name; std::string name;
@@ -83,6 +86,7 @@ namespace MetaData
std::vector<Artist> composerArtists; std::vector<Artist> composerArtists;
std::vector<Artist> lyricistArtists; std::vector<Artist> lyricistArtists;
std::vector<Artist> mixerArtists; std::vector<Artist> mixerArtists;
PerformerContainer performerArtists;
std::vector<Artist> producerArtists; std::vector<Artist> producerArtists;
std::vector<Artist> remixerArtists; std::vector<Artist> remixerArtists;
}; };
+44 -1
View File
@@ -62,7 +62,6 @@ TEST(MetaData, parseDate)
{ "1995-05/09", Wt::WDate {} }, // invalid mixup separators { "1995-05/09", Wt::WDate {} }, // invalid mixup separators
}; };
for (const TestCase& testCase : testCases) for (const TestCase& testCase : testCases)
{ {
const Wt::WDate parsed {parseDate(testCase.str)}; 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 << "'"; 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(); 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 void
doDbMigration(Session& session) doDbMigration(Session& session)
{ {
@@ -643,6 +654,7 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
{34, migrateFromV34}, {34, migrateFromV34},
{35, migrateFromV35}, {35, migrateFromV35},
{36, migrateFromV36}, {36, migrateFromV36},
{37, migrateFromV37},
}; };
while (1) while (1)
@@ -26,7 +26,7 @@ namespace Database
class Session; class Session;
using Version = std::size_t; using Version = std::size_t;
static constexpr Version LMS_DATABASE_VERSION {37}; static constexpr Version LMS_DATABASE_VERSION {38};
class VersionInfo class VersionInfo
{ {
public: public:
@@ -24,27 +24,70 @@
#include "services/database/Track.hpp" #include "services/database/Track.hpp"
#include "IdTypeTraits.hpp" #include "IdTypeTraits.hpp"
#include "Utils.hpp"
namespace Database { namespace Database
{
TrackArtistLink::TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type) static
: _type {type}, Wt::Dbo::Query<TrackArtistLinkId>
_track {getDboPtr(track)}, createQuery(Session& session, const TrackArtistLink::FindParameters& params)
_artist {getDboPtr(artist)} {
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::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(); 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(); session.getDboSession().flush();
return res; 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> EnumSet<TrackArtistLinkType>
TrackArtistLink::findUsedTypes(Session& session) TrackArtistLink::findUsedTypes(Session& session)
{ {
@@ -54,6 +97,5 @@ TrackArtistLink::findUsedTypes(Session& session)
return EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res)); 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 RangeResults<ArtistId> findAllOrphans(Session& session, Range range); // No track related
static bool exists(Session& session, ArtistId id); static bool exists(Session& session, ArtistId id);
// Accessors // Accessors
const std::string& getName() const { return _name; } const std::string& getName() const { return _name; }
const std::string& getSortName() const { return _sortName; } const std::string& getSortName() const { return _sortName; }
@@ -20,11 +20,14 @@
#pragma once #pragma once
#include <string> #include <string>
#include <string_view>
#include <Wt/Dbo/Dbo.h> #include <Wt/Dbo/Dbo.h>
#include "services/database/IdType.hpp" #include "services/database/IdType.hpp"
#include "services/database/Object.hpp" #include "services/database/Object.hpp"
#include "services/database/ReleaseId.hpp"
#include "services/database/TrackId.hpp"
#include "services/database/Types.hpp" #include "services/database/Types.hpp"
#include "utils/EnumSet.hpp" #include "utils/EnumSet.hpp"
@@ -40,20 +43,37 @@ namespace Database
class TrackArtistLink : public Object<TrackArtistLink, TrackArtistLinkId> class TrackArtistLink : public Object<TrackArtistLink, TrackArtistLinkId>
{ {
public: public:
TrackArtistLink() = default; struct FindParameters
TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type); {
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); static EnumSet<TrackArtistLinkType> findUsedTypes(Session& session);
ObjectPtr<Track> getTrack() const { return _track; } ObjectPtr<Track> getTrack() const { return _track; }
ObjectPtr<Artist> getArtist() const { return _artist; } ObjectPtr<Artist> getArtist() const { return _artist; }
TrackArtistLinkType getType() const { return _type; } TrackArtistLinkType getType() const { return _type; }
std::string_view getSubType() const { return _subType; }
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
{ {
Wt::Dbo::field(a, _type, "type"); 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, _track, "track", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
@@ -61,10 +81,10 @@ namespace Database
private: private:
TrackArtistLinkType _type; TrackArtistLinkType _type;
std::string _subType;
Wt::Dbo::ptr<Track> _track; Wt::Dbo::ptr<Track> _track;
Wt::Dbo::ptr<Artist> _artist; 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)) for (const Artist::pointer& mixer : getOrCreateArtists(_dbSession, trackInfo->mixerArtists, true))
track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, mixer, TrackArtistLinkType::Mixer)); 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)) for (const Artist::pointer& producer : getOrCreateArtists(_dbSession, trackInfo->producerArtists, true))
track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, producer, TrackArtistLinkType::Producer)); 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); return boost::algorithm::join(strings, delimiter);
} }
std::string std::string_view
stringTrim(std::string_view str, std::string_view whitespaces) stringTrim(std::string_view str, std::string_view whitespaces)
{ {
std::string_view res;
const auto strBegin = str.find_first_not_of(whitespaces); const auto strBegin = str.find_first_not_of(whitespaces);
if (strBegin == std::string_view::npos) if (strBegin != std::string_view::npos)
return ""; // no content {
const auto strEnd {str.find_last_not_of(whitespaces)};
const auto strRange {strEnd - strBegin + 1};
const auto strEnd = str.find_last_not_of(whitespaces); res = str.substr(strBegin, strRange);
const auto strRange = strEnd - strBegin + 1; }
return std::string {str.substr(strBegin, strRange)}; return res;
} }
std::string std::string
@@ -175,6 +179,21 @@ bufferToString(const std::vector<unsigned char>& data)
return oss.str(); 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 std::string
replaceInString(std::string_view str, const std::string& from, const std::string& to) 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); joinStrings(const std::vector<std::string>& strings, const std::string& delimiter);
[[nodiscard]] [[nodiscard]]
std::string std::string_view
stringTrim(std::string_view str, std::string_view whitespaces = " \t"); stringTrim(std::string_view str, std::string_view whitespaces = " \t");
[[nodiscard]] [[nodiscard]]
@@ -66,6 +66,9 @@ stringToUpper(const std::string& str);
std::string std::string
bufferToString(const std::vector<unsigned char>& data); bufferToString(const std::vector<unsigned char>& data);
void
capitalize(std::string& str);
template<typename T> template<typename T>
[[nodiscard]] [[nodiscard]]
std::optional<T> readAs(std::string_view str) 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>("foo"), std::nullopt);
EXPECT_EQ(StringUtils::readAs<bool>(""), 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()); LmsApp->setTitle(artist->getName());
_artistId = *artistId; _artistId = *artistId;
std::size_t sectionCount{}; bool setReleaseTitle {};
refreshReleases();
if (refreshReleases()) setReleaseTitle |= refreshAppearsOnReleases();
sectionCount++; setReleaseTitle |= refreshNonReleaseTracks();
if (refreshAppearsOnReleases())
sectionCount++;
if (refreshNonReleaseTracks())
sectionCount++;
refreshLinks(artist); refreshLinks(artist);
refreshSimilarArtists(similarArtistIds); refreshSimilarArtists(similarArtistIds);
if (sectionCount > 1) setCondition("if-release-title", setReleaseTitle);
setCondition("if-section-titles", true);
Wt::WContainerWidget* clusterContainers {bindNew<Wt::WContainerWidget>("clusters")}; Wt::WContainerWidget* clusterContainers {bindNew<Wt::WContainerWidget>("clusters")};
+2
View File
@@ -145,12 +145,14 @@ Artists::refreshArtistLinkTypes()
// add default one first (none) // add default one first (none)
linkTypeModel->add(Wt::WString::tr("Lms.Explore.Artists.linktype-all"), std::nullopt); 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::Artist, "Lms.Explore.Artists.linktype-artist");
addTypeIfUsed(TrackArtistLinkType::ReleaseArtist, "Lms.Explore.Artists.linktype-releaseartist"); addTypeIfUsed(TrackArtistLinkType::ReleaseArtist, "Lms.Explore.Artists.linktype-releaseartist");
addTypeIfUsed(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer"); addTypeIfUsed(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
addTypeIfUsed(TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor"); addTypeIfUsed(TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor");
addTypeIfUsed(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist"); addTypeIfUsed(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist");
addTypeIfUsed(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer"); 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::Producer, "Lms.Explore.Artists.linktype-producer");
addTypeIfUsed(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer"); addTypeIfUsed(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
} }
+36 -5
View File
@@ -19,6 +19,7 @@
#include "ReleaseView.hpp" #include "ReleaseView.hpp"
#include <map>
#include <Wt/WAnchor.h> #include <Wt/WAnchor.h>
#include <Wt/WImage.h> #include <Wt/WImage.h>
#include <Wt/WPushButton.h> #include <Wt/WPushButton.h>
@@ -30,6 +31,7 @@
#include "services/database/ScanSettings.hpp" #include "services/database/ScanSettings.hpp"
#include "services/database/Session.hpp" #include "services/database/Session.hpp"
#include "services/database/Track.hpp" #include "services/database/Track.hpp"
#include "services/database/TrackArtistLink.hpp"
#include "services/recommendation/IRecommendationService.hpp" #include "services/recommendation/IRecommendationService.hpp"
#include "services/scrobbling/IScrobblingService.hpp" #include "services/scrobbling/IScrobblingService.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
@@ -64,6 +66,7 @@ showReleaseInfoModal(Database::ReleaseId releaseId)
releaseInfo->addFunction("tr", &Wt::WTemplate::Functions::tr); releaseInfo->addFunction("tr", &Wt::WTemplate::Functions::tr);
Wt::WContainerWidget* artistTable {releaseInfo->bindNew<Wt::WContainerWidget>("artist-table")}; Wt::WContainerWidget* artistTable {releaseInfo->bindNew<Wt::WContainerWidget>("artist-table")};
std::map<Wt::WString, std::set<ArtistId>> artistMap;
auto addArtists = [&](TrackArtistLinkType linkType, const char* type) auto addArtists = [&](TrackArtistLinkType linkType, const char* type)
{ {
@@ -74,11 +77,28 @@ showReleaseInfoModal(Database::ReleaseId releaseId)
if (artistIds.results.empty()) if (artistIds.results.empty())
return; return;
std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistContainer(artistIds.results)}; Wt::WString typeStr {Wt::WString::trn(type, artistIds.results.size())};;
auto artistsEntry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists"))}; for (ArtistId artistId : artistIds.results)
artistsEntry->bindString("type", Wt::WString::trn(type, artistContainer->count())); artistMap[typeStr].insert(artistId);
artistsEntry->bindWidget("artist-container", std::move(artistContainer)); };
artistTable->addWidget(std::move(artistsEntry));
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"); 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::Mixer, "Lms.Explore.Artists.linktype-mixer");
addArtists(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer"); addArtists(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
addArtists(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer"); 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 // TODO: save in DB and mean all this
for (TrackId trackId : Track::find(LmsApp->getDbSession(), Track::FindParameters {}.setRelease(releaseId).setRange(Range {0, 1})).results) 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 "TrackListHelpers.hpp"
#include <map>
#include <Wt/WAnchor.h> #include <Wt/WAnchor.h>
#include <Wt/WImage.h> #include <Wt/WImage.h>
#include <Wt/WPushButton.h> #include <Wt/WPushButton.h>
@@ -26,9 +27,10 @@
#include "av/IAudioFile.hpp" #include "av/IAudioFile.hpp"
#include "services/database/Artist.hpp" #include "services/database/Artist.hpp"
#include "services/database/Release.hpp" #include "services/database/Release.hpp"
#include "services/scrobbling/IScrobblingService.hpp"
#include "services/database/Session.hpp" #include "services/database/Session.hpp"
#include "services/database/Track.hpp" #include "services/database/Track.hpp"
#include "services/database/TrackArtistLink.hpp"
#include "services/scrobbling/IScrobblingService.hpp"
#include "utils/Service.hpp" #include "utils/Service.hpp"
#include "common/Template.hpp" #include "common/Template.hpp"
@@ -58,6 +60,7 @@ namespace UserInterface::TrackListHelpers
trackInfo->addFunction("tr", &Wt::WTemplate::Functions::tr); trackInfo->addFunction("tr", &Wt::WTemplate::Functions::tr);
Wt::WContainerWidget* artistTable {trackInfo->bindNew<Wt::WContainerWidget>("artist-table")}; Wt::WContainerWidget* artistTable {trackInfo->bindNew<Wt::WContainerWidget>("artist-table")};
std::map<Wt::WString, std::set<ArtistId>> artistMap;
auto addArtists = [&](TrackArtistLinkType linkType, const char* type) auto addArtists = [&](TrackArtistLinkType linkType, const char* type)
{ {
@@ -68,11 +71,28 @@ namespace UserInterface::TrackListHelpers
if (artistIds.results.empty()) if (artistIds.results.empty())
return; return;
std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistContainer(artistIds.results)}; Wt::WString typeStr {Wt::WString::trn(type, artistIds.results.size())};;
auto artistsEntry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists"))}; for (ArtistId artistId : artistIds.results)
artistsEntry->bindString("type", Wt::WString::trn(type, artistContainer->count())); artistMap[typeStr].insert(artistId);
artistsEntry->bindWidget("artist-container", std::move(artistContainer)); };
artistTable->addWidget(std::move(artistsEntry));
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"); 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::Mixer, "Lms.Explore.Artists.linktype-mixer");
addArtists(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer"); addArtists(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
addArtists(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer"); 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())}) 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; return os;
} }
void parse(MetaData::IParser& parser, const std::filesystem::path& file) void parse(MetaData::IParser& parser, const std::filesystem::path& file)
{ {
using namespace MetaData; using namespace MetaData;
@@ -91,6 +89,16 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
for (const Artist& artist : track->mixerArtists) for (const Artist& artist : track->mixerArtists)
std::cout << "Mixer: " << artist << std::endl; 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) for (const Artist& artist : track->producerArtists)
std::cout << "Producer: " << artist << std::endl; std::cout << "Producer: " << artist << std::endl;