Added a dedicated table for release artists + handle display names in UI/Subsonic API, ref #731

This commit is contained in:
emeric
2025-12-24 09:00:59 +01:00
parent 68cc7116a7
commit f70692b680
78 changed files with 1916 additions and 935 deletions
@@ -100,7 +100,7 @@ namespace lms::feedback
searchParams.setFilters(params.filters);
searchParams.setStarringUser(params.user, *backend);
searchParams.setKeywords(params.keywords);
searchParams.setLinkType(params.linkType);
searchParams.setTrackArtistLinkType(params.trackArtistLinkType);
searchParams.setSortMethod(params.sortMethod);
searchParams.setRange(params.range);
@@ -81,12 +81,18 @@ namespace lms::feedback
// Artists
struct ArtistFindParameters : public FindParameters
{
std::optional<db::TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
std::optional<db::TrackArtistLinkType> trackArtistLinkType; // if set, only artists that have produced at least one track with this link type
db::ArtistSortMethod sortMethod{ db::ArtistSortMethod::None };
std::optional<bool> releaseArtistsOnly;
ArtistFindParameters& setLinkType(std::optional<db::TrackArtistLinkType> _linkType)
ArtistFindParameters& setReleaseArtistsOnly(std::optional<bool> _releaseArtistsOnly)
{
linkType = _linkType;
releaseArtistsOnly = _releaseArtistsOnly;
return *this;
}
ArtistFindParameters& setTrackArtistLinkType(std::optional<db::TrackArtistLinkType> _trackArtistLinkType)
{
trackArtistLinkType = _trackArtistLinkType;
return *this;
}
ArtistFindParameters& setSortMethod(db::ArtistSortMethod _sortMethod)
@@ -38,6 +38,7 @@
#include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Release.hpp"
#include "database/objects/ReleaseArtistLink.hpp"
#include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp"
#include "database/objects/TrackEmbeddedImage.hpp"
@@ -80,6 +81,20 @@ namespace lms::scanner
createTrackArtistLinks(session, track, linkType, noRole, artists, allowArtistMBIDFallback);
}
void createReleaseArtistLinks(db::Session& session, const db::Release::pointer& dbRelease, std::span<const Artist> artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback)
{
for (const Artist& artist : artists)
{
db::Artist::pointer dbArtist{ helpers::getOrCreateArtist(session, artist, allowArtistMBIDFallback) };
const bool matchedUsingMbid{ artist.mbid.has_value() && dbArtist->getMBID() == artist.mbid };
db::ReleaseArtistLink::pointer link{ session.create<db::ReleaseArtistLink>(dbRelease, dbArtist, matchedUsingMbid) };
link.modify()->setArtistName(artist.name);
if (artist.sortName)
link.modify()->setArtistSortName(*artist.sortName);
}
}
db::ReleaseType::pointer getOrCreateReleaseType(db::Session& session, std::string_view name)
{
db::ReleaseType::pointer releaseType{ db::ReleaseType::find(session, name) };
@@ -107,7 +122,46 @@ namespace lms::scanner
return label;
}
void updateReleaseIfNeeded(db::Session& session, db::Release::pointer dbRelease, const Release& release)
bool needUpdateReleaseArtists(const db::Release::pointer& dbRelease, const Release& release)
{
const std::vector<db::ReleaseArtistLink::pointer> dbArtistLinks{ dbRelease->getArtistLinks() };
if (dbArtistLinks.size() != release.artists.size())
return true;
// Must be in same order
for (std::size_t i{}; i < dbArtistLinks.size(); ++i)
{
const db::ReleaseArtistLink::pointer& dbArtistLink{ dbArtistLinks[i] };
const Artist& artist{ release.artists[i] };
if (dbArtistLink->getArtistName() != artist.name)
return true;
if (dbArtistLink->getArtistSortName() != artist.sortName)
return true;
if (!dbArtistLink->isArtistMBIDMatched() && artist.mbid)
return true;
if (dbArtistLink->isArtistMBIDMatched() && !artist.mbid)
return true;
if (artist.mbid)
{
const db::Artist::pointer dbArtist{ dbArtistLink->getArtist() };
if (!dbArtist)
return true;
if (dbArtistLink->getArtist()->getMBID() != artist.mbid)
return true;
}
}
return false;
}
void updateReleaseIfNeeded(db::Session& session, db::Release::pointer dbRelease, const Release& release, helpers::AllowFallbackOnMBIDEntry allowFallback)
{
if (dbRelease->getName() != release.name)
dbRelease.modify()->setName(release.name);
@@ -143,6 +197,11 @@ namespace lms::scanner
for (std::string_view label : release.labels)
dbRelease.modify()->addLabel(getOrCreateLabel(session, label));
}
if (needUpdateReleaseArtists(dbRelease, release))
{
dbRelease.modify()->clearArtistLinks();
createReleaseArtistLinks(session, dbRelease, release.artists, allowFallback);
}
}
// Compare release level info
@@ -157,7 +216,7 @@ namespace lms::scanner
&& dbCandidateRelease->getBarcode() == release.barcode;
}
db::Release::pointer getOrCreateRelease(db::Session& session, const Release& release, const db::Directory::pointer& currentDirectory)
db::Release::pointer getOrCreateRelease(db::Session& session, const Release& release, const db::Directory::pointer& currentDirectory, helpers::AllowFallbackOnMBIDEntry allowFallback)
{
db::Release::pointer dbRelease;
@@ -224,7 +283,7 @@ namespace lms::scanner
if (!dbRelease)
dbRelease = session.create<db::Release>(release.name);
updateReleaseIfNeeded(session, dbRelease, release);
updateReleaseIfNeeded(session, dbRelease, release, allowFallback);
return dbRelease;
}
@@ -671,13 +730,24 @@ namespace lms::scanner
db::Directory::pointer directory{ utils::getOrCreateDirectory(dbSession, getFilePath().parent_path(), mediaLibrary) };
track.modify()->setDirectory(directory);
track.modify()->clearArtistLinks();
const helpers::AllowFallbackOnMBIDEntry allowFallback{ getScannerSettings().allowArtistMBIDFallback };
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _file->track.artists, allowFallback);
if (_file->track.medium && _file->track.medium->release)
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::ReleaseArtist, _file->track.medium->release->artists, allowFallback);
// For now, alway tie a medium to a release, and a release mst have at least one medium, even if no disc number is set
if (_file->track.medium && _file->track.medium->release)
{
db::Release::pointer release{ getOrCreateRelease(dbSession, *_file->track.medium->release, directory, allowFallback) };
assert(release);
track.modify()->setRelease(release);
track.modify()->setMedium(getOrCreateMedium(dbSession, *_file->track.medium, release));
}
else
{
track.modify()->setRelease({});
track.modify()->setMedium({});
}
track.modify()->clearArtistLinks();
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _file->track.artists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Conductor, _file->track.conductorArtists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Composer, _file->track.composerArtists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Lyricist, _file->track.lyricistArtists, allowFallback);
@@ -688,19 +758,6 @@ namespace lms::scanner
for (const auto& [role, performers] : _file->track.performerArtists)
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Performer, role, performers, allowFallback);
// For now, alway tie a medium to a release, and a release mst have at least one medium, even if no disc number is set
if (_file->track.medium && _file->track.medium->release)
{
db::Release::pointer release{ getOrCreateRelease(dbSession, *_file->track.medium->release, directory) };
assert(release);
track.modify()->setRelease(release);
track.modify()->setMedium(getOrCreateMedium(dbSession, *_file->track.medium, release));
}
else
{
track.modify()->setRelease({});
track.modify()->setMedium({});
}
track.modify()->setClusters(getOrCreateClusters(dbSession, _file->track));
track.modify()->setName(title);
track.modify()->setTrackNumber(_file->track.position);
@@ -28,6 +28,7 @@
#include "database/Session.hpp"
#include "database/objects/Artist.hpp"
#include "database/objects/ArtistInfo.hpp"
#include "database/objects/ReleaseArtistLink.hpp"
#include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp"
#include "database/objects/TrackList.hpp"
@@ -75,23 +76,46 @@ namespace lms::scanner
artistInfo.modify()->setArtist(newArtist);
}
db::TrackArtistLink::pointer getMostRecentMBIDArtistLink(db::Session& session, db::ArtistId artistId, std::optional<db::TrackArtistLinkType> linkType = std::nullopt)
struct ArtistReference
{
std::string name;
std::string sortName;
};
std::optional<ArtistReference> getMostRecentReleaseArtistReference(db::Session& session, db::ArtistId artistId)
{
std::optional<ArtistReference> ref;
db::ReleaseArtistLink::FindParameters params;
params.setArtist(artistId);
params.setSortMethod(db::ReleaseArtistLinkSortMethod::OriginalDateDesc);
params.setMBIDMatched(true);
params.setRange(db::Range{ .offset = 0, .size = 1 });
db::ReleaseArtistLink::pointer foundLink;
db::ReleaseArtistLink::find(session, params, [&](const db::ReleaseArtistLink::pointer& link) {
ref = ArtistReference{ .name = std::string{ link->getArtistName() }, .sortName = std::string{ link->getArtistSortName() } };
});
return ref;
}
std::optional<ArtistReference> getMostRecentTrackArtistReference(db::Session& session, db::ArtistId artistId)
{
std::optional<ArtistReference> ref;
db::TrackArtistLink::FindParameters params;
params.setArtist(artistId);
params.setLinkType(linkType);
params.setSortMethod(db::TrackArtistLinkSortMethod::OriginalDateDesc);
params.setMBIDMatched(true);
params.setRange(db::Range{ .offset = 0, .size = 1 });
db::TrackArtistLink::pointer foundLink;
db::TrackArtistLink::find(session, params, [&](const db::TrackArtistLink::pointer& link) {
foundLink = link;
ref = ArtistReference{ .name = std::string{ link->getArtistName() }, .sortName = std::string{ link->getArtistSortName() } };
});
return foundLink;
return ref;
}
} // namespace
bool ScanStepArtistReconciliation::needProcess([[maybe_unused]] const ScanContext& context) const
@@ -173,24 +197,24 @@ namespace lms::scanner
if (hasArtistInfo)
continue;
db::TrackArtistLink::pointer artistMostRecentLink{ getMostRecentMBIDArtistLink(session, artist->getId(), db::TrackArtistLinkType::ReleaseArtist) };
if (!artistMostRecentLink)
artistMostRecentLink = getMostRecentMBIDArtistLink(session, artist->getId());
std::optional<ArtistReference> mostRecentArtistRef{ getMostRecentReleaseArtistReference(session, artist->getId()) };
if (!mostRecentArtistRef)
mostRecentArtistRef = getMostRecentTrackArtistReference(session, artist->getId());
if (!artistMostRecentLink)
if (!mostRecentArtistRef)
{
LMS_LOG(DBUPDATER, DEBUG, "Unable to fix name discrepancy for artist " << artist << ": no link found!");
continue;
}
if (artistMostRecentLink->getArtistName() != artist->getName())
if (mostRecentArtistRef->name != artist->getName())
{
ArtistToUpdate& artistToUpdate{ artistsToUpdate.emplace_back() };
artistToUpdate.artist = artist;
artistToUpdate.newName = artistMostRecentLink->getArtistName();
artistToUpdate.newSortName = artistMostRecentLink->getArtistSortName();
artistToUpdate.newName = mostRecentArtistRef->name;
artistToUpdate.newSortName = mostRecentArtistRef->sortName;
LMS_LOG(DBUPDATER, DEBUG, "Updating artist " << artist << " name to '" << artistToUpdate.newName << "' using most recent '" << db::trackArtistLinkTypeToString(artistMostRecentLink->getType()) << "' link reference");
LMS_LOG(DBUPDATER, DEBUG, "Updating artist " << artist << " name to '" << artistToUpdate.newName << "' using most recent artist link reference");
}
}
@@ -126,7 +126,7 @@ namespace lms::scanner
std::set<std::filesystem::path> releasePaths;
db::Directory::FindParameters params;
params.setArtist(artistId, { db::TrackArtistLinkType::ReleaseArtist });
params.setReleaseArtist(artistId);
db::Directory::find(session, params, [&](const db::Directory::pointer& directory) {
releasePaths.insert(directory->getAbsolutePath());
@@ -176,7 +176,7 @@ namespace lms::scanner
db::Artwork::pointer artwork;
db::Release::FindParameters params;
params.setArtist(artist->getId(), { db::TrackArtistLinkType::ReleaseArtist });
params.setArtist(artist->getId());
params.setSortMethod(db::ReleaseSortMethod::OriginalDate);
db::Release::find(session, params, [&](const db::Release::pointer& release) {
@@ -51,7 +51,8 @@ namespace lms::scrobbling
db::Listen::ArtistStatsFindParameters convertToListenFindParameters(const ScrobblingService::ArtistFindParameters& params)
{
return db::Listen::ArtistStatsFindParameters{ convertToListenFindParameters(static_cast<const ScrobblingService::FindParameters&>(params)), params.linkType };
db::Listen::ArtistStatsFindParameters listenFindParams{ convertToListenFindParameters(static_cast<const ScrobblingService::FindParameters&>(params)), params.linkType, params.releaseArtistsOnly };
return listenFindParams;
}
} // namespace
@@ -36,6 +36,7 @@
#include "database/objects/Listen.hpp"
#include "database/objects/Release.hpp"
#include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp"
#include "database/objects/User.hpp"
#include "services/scrobbling/Exception.hpp"
@@ -46,6 +47,27 @@ namespace lms::scrobbling::listenBrainz
{
namespace
{
struct Artist
{
std::string name;
std::optional<core::UUID> mbid;
};
std::vector<Artist> getTrackArtists(const db::Track::pointer& track)
{
std::vector<Artist> artists;
for (const db::TrackArtistLink::pointer& trackArtistLink : track->getArtistLinks(db::TrackArtistLinkType::Artist))
{
const auto trackArtist{ trackArtistLink->getArtist() };
if (!trackArtist)
continue;
artists.emplace_back(Artist{ std::string{ trackArtistLink->getArtistName() }, trackArtist->getMBID() });
}
return artists;
}
std::optional<Wt::Json::Object> listenToJsonPayload(db::Session& session, const scrobbling::Listen& listen, const Wt::WDateTime& timePoint)
{
auto transaction{ session.createReadTransaction() };
@@ -54,10 +76,7 @@ namespace lms::scrobbling::listenBrainz
if (!track)
return std::nullopt;
auto artists{ track->getArtists({ db::TrackArtistLinkType::Artist }) };
if (artists.empty())
artists = track->getArtists({ db::TrackArtistLinkType::ReleaseArtist });
const std::vector<Artist> artists{ getTrackArtists(track) };
if (artists.empty())
{
LOG(DEBUG, "Track cannot be scrobbled since it does not have any artist");
@@ -77,10 +96,10 @@ namespace lms::scrobbling::listenBrainz
{
Wt::Json::Array artistMBIDs;
for (const db::Artist::pointer& artist : artists)
for (const Artist& artist : artists)
{
if (auto MBID{ artist->getMBID() })
artistMBIDs.push_back(Wt::Json::Value{ std::string{ MBID->getAsString() } });
if (artist.mbid)
artistMBIDs.push_back(Wt::Json::Value{ std::string{ artist.mbid->getAsString() } });
}
if (!artistMBIDs.empty())
@@ -97,8 +97,14 @@ namespace lms::scrobbling
{
std::optional<db::TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
db::ArtistSortMethod sortMethod{ db::ArtistSortMethod::None };
bool releaseArtistsOnly;
ArtistFindParameters& setLinkType(std::optional<db::TrackArtistLinkType> _linkType)
ArtistFindParameters& setReleaseArtistsOnly(bool _releaseArtistsOnly)
{
releaseArtistsOnly = _releaseArtistsOnly;
return *this;
}
ArtistFindParameters& setTrackArtistLinkType(std::optional<db::TrackArtistLinkType> _linkType)
{
linkType = _linkType;
return *this;