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
+17 -13
View File
@@ -109,7 +109,7 @@ namespace lms::api::subsonic
{
// API says: "Returns a random collection of songs from the given artist and similar artists"
const std::size_t similarArtistCount{ count / 5 };
std::vector<ArtistId> artistIds{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(artistId, { TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist }, similarArtistCount) };
std::vector<ArtistId> artistIds{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(artistId, { TrackArtistLinkType::Artist }, similarArtistCount) };
artistIds.push_back(artistId);
const std::size_t meanTrackCountPerArtist{ (count / artistIds.size()) + 1 };
@@ -392,16 +392,16 @@ namespace lms::api::subsonic
case SubsonicArtistListMode::AllArtists:
break;
case SubsonicArtistListMode::ReleaseArtists:
parameters.setLinkType(TrackArtistLinkType::ReleaseArtist);
parameters.setReleaseArtistsOnly(true);
break;
case SubsonicArtistListMode::TrackArtists:
parameters.setLinkType(TrackArtistLinkType::Artist);
parameters.setTrackArtistLinkType(TrackArtistLinkType::Artist);
break;
}
}
parameters.filters.setMediaLibrary(mediaLibrary);
// This endpoint does not scale: make sort lived transactions in order not to block the whole application
// This endpoint does not scale: make short lived transactions in order not to block the whole application
// first pass: dispatch the artists by first letter
LMS_LOG(API_SUBSONIC, DEBUG, "GetArtists: fetching all artists...");
@@ -419,12 +419,7 @@ namespace lms::api::subsonic
{
std::string_view sortName{ artist->getSortName() };
char sortChar;
if (sortName.empty() || !std::isalpha(sortName[0]))
sortChar = '#';
else
sortChar = std::toupper(sortName[0]);
const char sortChar{ (sortName.empty() || !std::isalpha(sortName[0])) ? '#' : static_cast<char>(std::toupper(sortName[0])) };
artistsSortedByFirstChar[sortChar].push_back(artist->getId());
}
@@ -465,9 +460,18 @@ namespace lms::api::subsonic
Response response{ Response::createOkResponse(context.getServerProtocolVersion()) };
Response::Node artistNode{ createArtistNode(context, artist) };
const auto releases{ Release::find(context.getDbSession(), Release::FindParameters{}.setArtist(artist->getId())) };
for (const Release::pointer& release : releases.results)
auto addRelease{ [&](const Release::pointer& release) {
artistNode.addArrayChild("album", createAlbumNode(context, release, true /* id3 */));
} };
Release::find(context.getDbSession(), Release::FindParameters{}.setArtist(artist->getId()), [&](const db::Release::pointer& release) {
addRelease(release);
});
Release::find(context.getDbSession(), Release::FindParameters{}.setTrackArtist(artist->getId()), [&](const db::Release::pointer& release) {
if (!release->hasArtist(id))
addRelease(release);
});
response.addNode("artist", std::move(artistNode));
@@ -561,7 +565,7 @@ namespace lms::api::subsonic
});
}
auto similarArtistsId{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(id, { TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist }, count) };
auto similarArtistsId{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(id, { TrackArtistLinkType::Artist }, count) };
{
auto transaction{ context.getDbSession().createReadTransaction() };
+31 -15
View File
@@ -29,6 +29,7 @@
#include "database/objects/Directory.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Release.hpp"
#include "database/objects/ReleaseArtistLink.hpp"
#include "database/objects/Track.hpp"
#include "database/objects/User.hpp"
#include "services/feedback/IFeedbackService.hpp"
@@ -99,23 +100,38 @@ namespace lms::api::subsonic
else if (const auto year{ release->getYear() })
albumNode.setAttribute("year", *year);
auto artists{ release->getReleaseArtists() };
if (artists.empty())
artists = release->getArtists();
if (!artists.empty())
struct Artist
{
if (!release->getArtistDisplayName().empty())
albumNode.setAttribute("artist", release->getArtistDisplayName());
else
albumNode.setAttribute("artist", utils::joinArtistNames(artists));
std::string name;
std::string id;
};
std::optional<Artist> artist;
if (artists.size() == 1)
const auto artistLinks{ release->getArtistLinks() };
if (!artistLinks.empty())
{
artist = Artist{ .name = std::string{ release->getArtistDisplayName() }, .id = {} };
if (artistLinks.size() == 1)
artist->id = idToString(artistLinks.front()->getArtistId());
}
else
{
if (const auto trackArtists{ release->getTrackArtists(TrackArtistLinkType::Artist) }; !trackArtists.empty())
{
albumNode.setAttribute("artistId", idToString(artists.front()->getId()));
if (trackArtists.size() > 1)
artist = Artist{ .name = "Various Artists", .id = {} };
else if (trackArtists.size() == 1)
artist = Artist{ .name = trackArtists.front()->getName(), .id = idToString(trackArtists.front()->getId()) };
}
}
if (artist)
{
albumNode.setAttribute("artist", artist->name);
if (!artist->id.empty())
albumNode.setAttribute("artistId", artist->id);
}
albumNode.setAttribute("playCount", core::Service<scrobbling::IScrobblingService>::get()->getCount(context.getUser()->getId(), release->getId()));
// Report the first GENRE for this track
@@ -182,16 +198,16 @@ namespace lms::api::subsonic
if (id3)
{
albumNode.createEmptyArrayChild("artists");
for (const Artist::pointer& artist : release->getReleaseArtists())
albumNode.addArrayChild("artists", createArtistNode(artist));
for (const db::ReleaseArtistLink::pointer& artistLink : artistLinks)
albumNode.addArrayChild("artists", createMinimalArtistNode(artistLink));
albumNode.setAttribute("displayArtist", release->getArtistDisplayName());
}
else
{
albumNode.createEmptyArrayChild("albumArtists");
for (const Artist::pointer& artist : release->getReleaseArtists())
albumNode.addArrayChild("albumArtists", createArtistNode(artist));
for (const db::ReleaseArtistLink::pointer& artistLink : artistLinks)
albumNode.addArrayChild("albumArtists", createMinimalArtistNode(artistLink));
albumNode.setAttribute("displayAlbumArtist", release->getArtistDisplayName());
+34 -6
View File
@@ -26,6 +26,7 @@
#include "database/objects/Artist.hpp"
#include "database/objects/Artwork.hpp"
#include "database/objects/Release.hpp"
#include "database/objects/ReleaseArtistLink.hpp"
#include "database/objects/TrackArtistLink.hpp"
#include "database/objects/User.hpp"
#include "services/feedback/IFeedbackService.hpp"
@@ -66,18 +67,25 @@ namespace lms::api::subsonic
{
LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateArtist");
Response::Node artistNode{ createArtistNode(artist) };
Response::Node artistNode{ createMinimalArtistNode(artist) };
artistNode.setAttribute("id", idToString(artist->getId()));
artistNode.setAttribute("name", artist->getName());
if (const auto artwork{ artist->getPreferredArtwork() })
{
CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() };
artistNode.setAttribute("coverArt", idToString(coverArtId));
}
const std::size_t count{ Release::getCount(context.getDbSession(), Release::FindParameters{}.setArtist(artist->getId())) };
artistNode.setAttribute("albumCount", count);
{
std::size_t count{ Release::getCount(context.getDbSession(), Release::FindParameters{}.setArtist(artist->getId())) };
// TODO: not very efficient
Release::find(context.getDbSession(), Release::FindParameters{}.setTrackArtist(artist->getId()), [&](const db::Release::pointer& release) {
if (!release->hasArtist(artist->getId()))
count++;
});
artistNode.setAttribute("albumCount", count);
}
if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(context.getUser()->getId(), artist->getId()) }; dateTime.isValid())
artistNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime));
@@ -107,7 +115,7 @@ namespace lms::api::subsonic
return artistNode;
}
Response::Node createArtistNode(const Artist::pointer& artist)
Response::Node createMinimalArtistNode(const Artist::pointer& artist)
{
Response::Node artistNode;
@@ -116,4 +124,24 @@ namespace lms::api::subsonic
return artistNode;
}
Response::Node createMinimalArtistNode(const db::ObjectPtr<db::ReleaseArtistLink>& artistLink)
{
Response::Node artistNode;
artistNode.setAttribute("id", idToString(artistLink->getArtistId()));
artistNode.setAttribute("name", artistLink->getArtistName());
return artistNode;
}
Response::Node createMinimalArtistNode(const db::ObjectPtr<db::TrackArtistLink>& artistLink)
{
Response::Node artistNode;
artistNode.setAttribute("id", idToString(artistLink->getArtistId()));
artistNode.setAttribute("name", artistLink->getArtistName());
return artistNode;
}
} // namespace lms::api::subsonic
+5 -1
View File
@@ -30,6 +30,8 @@
namespace lms::db
{
class Artist;
class ReleaseArtistLink;
class TrackArtistLink;
class User;
class Session;
} // namespace lms::db
@@ -45,5 +47,7 @@ namespace lms::api::subsonic
} // namespace utils
Response::Node createArtistNode(RequestContext& context, const db::ObjectPtr<db::Artist>& artist);
Response::Node createArtistNode(const db::ObjectPtr<db::Artist>& artist); // only minimal info
Response::Node createMinimalArtistNode(const db::ObjectPtr<db::Artist>& artist); // only minimal info
Response::Node createMinimalArtistNode(const db::ObjectPtr<db::ReleaseArtistLink>& artistLink); // only minimal info
Response::Node createMinimalArtistNode(const db::ObjectPtr<db::TrackArtistLink>& artistLink); // only minimal info
} // namespace lms::api::subsonic
@@ -27,14 +27,14 @@
namespace lms::api::subsonic
{
Response::Node createContributorNode(const db::ObjectPtr<db::TrackArtistLink>& trackArtistLink, const db::ObjectPtr<db::Artist>& artist)
Response::Node createContributorNode(const db::ObjectPtr<db::TrackArtistLink>& trackArtistLink)
{
Response::Node contributorNode;
contributorNode.setAttribute("role", utils::toString(trackArtistLink->getType()));
if (!trackArtistLink->getSubType().empty())
contributorNode.setAttribute("subRole", trackArtistLink->getSubType());
contributorNode.addChild("artist", createArtistNode(artist));
contributorNode.addChild("artist", createMinimalArtistNode(trackArtistLink));
return contributorNode;
}
@@ -31,5 +31,5 @@ namespace lms::db
namespace lms::api::subsonic
{
Response::Node createContributorNode(const db::ObjectPtr<db::TrackArtistLink>& trackArtistLink, const db::ObjectPtr<db::Artist>& artist);
Response::Node createContributorNode(const db::ObjectPtr<db::TrackArtistLink>& trackArtistLink);
}
+13 -7
View File
@@ -27,6 +27,7 @@
#include "core/MimeTypes.hpp"
#include "core/Service.hpp"
#include "core/String.hpp"
#include "database/Types.hpp"
#include "database/objects/Artist.hpp"
#include "database/objects/Artwork.hpp"
@@ -35,6 +36,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/User.hpp"
@@ -198,19 +200,23 @@ namespace lms::api::subsonic
trackResponse.createEmptyArrayChild("artists");
trackResponse.createEmptyArrayChild("contributors");
db::TrackArtistLink::find(context.getDbSession(), track->getId(), [&](const db::TrackArtistLink::pointer& link, const db::Artist::pointer& artist) {
switch (link->getType())
track->visitArtistLinks([&](const db::TrackArtistLink::pointer& artistLink) {
switch (artistLink->getType())
{
case db::TrackArtistLinkType::Artist:
trackResponse.addArrayChild("artists", createArtistNode(artist));
break;
case db::TrackArtistLinkType::ReleaseArtist:
trackResponse.addArrayChild("albumArtists", createArtistNode(artist));
trackResponse.addArrayChild("artists", createMinimalArtistNode(artistLink));
break;
default:
trackResponse.addArrayChild("contributors", createContributorNode(link, artist));
trackResponse.addArrayChild("contributors", createContributorNode(artistLink));
}
});
if (release)
{
release->visitArtistLinks([&](const db::ReleaseArtistLink::pointer& artistLink) {
trackResponse.addArrayChild("albumArtists", createMinimalArtistNode(artistLink));
});
}
}
trackResponse.setAttribute("displayArtist", track->getArtistDisplayName());