Added performers. fixes #278
This commit is contained in:
+3
-3
@@ -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
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -183,6 +183,10 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -183,6 +183,10 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -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
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 << "'";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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")};
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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())})
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user