Extracted browsing entry points

This commit is contained in:
emeric
2023-10-02 00:04:58 +02:00
parent 247975d304
commit 4fbacd691d
10 changed files with 593 additions and 506 deletions
+2 -497
View File
@@ -20,33 +20,22 @@
#include "SubsonicResource.hpp"
#include <atomic>
#include <ctime>
#include <map>
#include <unordered_map>
#include <Wt/WDateTime.h>
#include "services/auth/IPasswordService.hpp"
#include "services/auth/IEnvService.hpp"
#include "services/database/Artist.hpp"
#include "services/database/Cluster.hpp"
#include "services/database/Db.hpp"
#include "services/database/Release.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/TrackBookmark.hpp"
#include "services/database/TrackList.hpp"
#include "services/database/User.hpp"
#include "services/recommendation/IRecommendationService.hpp"
#include "services/scrobbling/IScrobblingService.hpp"
#include "utils/EnumSet.hpp"
#include "utils/IConfig.hpp"
#include "utils/Logger.hpp"
#include "utils/Random.hpp"
#include "utils/Service.hpp"
#include "utils/String.hpp"
#include "utils/Utils.hpp"
#include "entrypoints/AlbumSongLists.hpp"
#include "entrypoints/Browsing.hpp"
#include "entrypoints/Bookmarks.hpp"
#include "entrypoints/MediaAnnotation.hpp"
#include "entrypoints/MediaLibraryScanning.hpp"
@@ -54,9 +43,6 @@
#include "entrypoints/Playlists.hpp"
#include "entrypoints/Searching.hpp"
#include "entrypoints/UserManagement.hpp"
#include "responses/Artist.hpp"
#include "responses/Album.hpp"
#include "responses/Song.hpp"
#include "ParameterParsing.hpp"
#include "ProtocolVersion.hpp"
#include "RequestContext.hpp"
@@ -64,14 +50,8 @@
#include "SubsonicResponse.hpp"
#include "Utils.hpp"
using namespace Database;
static const std::string_view genreClusterName {"GENRE"};
static const std::string_view reportedStarredDate {"2000-01-01T00:00:00"};
static const std::string_view reportedDummyDate {"2000-01-01T00:00:00"};
static const unsigned long long reportedDummyDateULong {946684800000ULL}; // 2000-01-01T00:00:00 UTC
namespace API::Subsonic
{
@@ -81,14 +61,6 @@ createSubsonicResource(Database::Db& db)
return std::make_unique<SubsonicResource>(db);
}
static
std::string
makeNameFilesystemCompatible(const std::string& name)
{
return StringUtils::replaceInString(name, "/", "_");
}
static
std::unordered_map<std::string, ProtocolVersion>
readConfigProtocolVersions()
@@ -157,19 +129,6 @@ checkUserTypeIsAllowed(RequestContext& context, EnumSet<Database::UserType> allo
throw UserNotAuthorizedError {};
}
static
Response::Node
clusterToResponseNode(const Cluster::pointer& cluster)
{
Response::Node clusterNode;
clusterNode.setValue(cluster->getName());
clusterNode.setAttribute("songCount", cluster->getTracksCount());
clusterNode.setAttribute("albumCount", cluster->getReleasesCount());
return clusterNode;
}
static
Response
handlePingRequest(RequestContext& context)
@@ -177,8 +136,6 @@ handlePingRequest(RequestContext& context)
return Response::createOkResponse(context.serverProtocolVersion);
}
static
Response
handleGetLicenseRequest(RequestContext& context)
@@ -193,458 +150,6 @@ handleGetLicenseRequest(RequestContext& context)
return response;
}
static
Response
handleGetAlbumRequest(RequestContext& context)
{
// Mandatory params
ReleaseId id {getMandatoryParameterAs<ReleaseId>(context.parameters, "id")};
auto transaction {context.dbSession.createSharedTransaction()};
Release::pointer release {Release::find(context.dbSession, id)};
if (!release)
throw RequestedDataNotFoundError {};
User::pointer user {User::find(context.dbSession, context.userId)};
if (!user)
throw UserNotAuthorizedError {};
Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node albumNode {createAlbumNode(release, context.dbSession, user, true /* id3 */)};
const auto tracks {Track::find(context.dbSession, Track::FindParameters {}.setRelease(id).setSortMethod(TrackSortMethod::Release))};
for (const TrackId trackId : tracks.results)
{
const Track::pointer track {Track::find(context.dbSession, trackId)};
albumNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
}
response.addNode("album", std::move(albumNode));
return response;
}
static
Response
handleGetSongRequest(RequestContext& context)
{
// Mandatory params
TrackId id {getMandatoryParameterAs<TrackId>(context.parameters, "id")};
auto transaction {context.dbSession.createSharedTransaction()};
const Track::pointer track {Track::find(context.dbSession, id)};
if (!track)
throw RequestedDataNotFoundError {};
User::pointer user {User::find(context.dbSession, context.userId)};
if (!user)
throw UserNotAuthorizedError {};
Response response {Response::createOkResponse(context.serverProtocolVersion)};
response.addNode("song", createSongNode(track, context.dbSession, user));
return response;
}
static
Response
handleGetArtistRequest(RequestContext& context)
{
// Mandatory params
ArtistId id {getMandatoryParameterAs<ArtistId>(context.parameters, "id")};
auto transaction {context.dbSession.createSharedTransaction()};
const Artist::pointer artist {Artist::find(context.dbSession, id)};
if (!artist)
throw RequestedDataNotFoundError {};
User::pointer user {User::find(context.dbSession, context.userId)};
if (!user)
throw UserNotAuthorizedError {};
Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node artistNode {createArtistNode(artist, context.dbSession, user, true /* id3 */)};
const auto releases {Release::find(context.dbSession, Release::FindParameters {}.setArtist(artist->getId()))};
for (const ReleaseId releaseId : releases.results)
{
const Release::pointer release {Release::find(context.dbSession, releaseId)};
artistNode.addArrayChild("album", createAlbumNode(release, context.dbSession, user, true /* id3 */));
}
response.addNode("artist", std::move(artistNode));
return response;
}
static
Response
handleGetArtistInfoRequestCommon(RequestContext& context, bool id3)
{
// Mandatory params
ArtistId id {getMandatoryParameterAs<ArtistId>(context.parameters, "id")};
// Optional params
std::size_t count {getParameterAs<std::size_t>(context.parameters, "count").value_or(20)};
Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& artistInfoNode {response.createNode(id3 ? "artistInfo2" : "artistInfo")};
{
auto transaction {context.dbSession.createSharedTransaction()};
const Artist::pointer artist {Artist::find(context.dbSession, id)};
if (!artist)
throw RequestedDataNotFoundError {};
std::optional<UUID> artistMBID {artist->getMBID()};
if (artistMBID)
artistInfoNode.createChild("musicBrainzId").setValue(artistMBID->getAsString());
}
auto similarArtistsId {Service<Recommendation::IRecommendationService>::get()->getSimilarArtists(id, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, count)};
{
auto transaction {context.dbSession.createSharedTransaction()};
User::pointer user {User::find(context.dbSession, context.userId)};
if (!user)
throw UserNotAuthorizedError {};
for (const ArtistId similarArtistId : similarArtistsId)
{
const Artist::pointer similarArtist {Artist::find(context.dbSession, similarArtistId)};
if (similarArtist)
artistInfoNode.addArrayChild("similarArtist", createArtistNode(similarArtist, context.dbSession, user, id3));
}
}
return response;
}
static
Response
handleGetArtistInfoRequest(RequestContext& context)
{
return handleGetArtistInfoRequestCommon(context, false /* no id3 */);
}
static
Response
handleGetArtistInfo2Request(RequestContext& context)
{
return handleGetArtistInfoRequestCommon(context, true /* id3 */);
}
static
Response
handleGetMusicDirectoryRequest(RequestContext& context)
{
// Mandatory params
const auto artistId {getParameterAs<ArtistId>(context.parameters, "id")};
const auto releaseId {getParameterAs<ReleaseId>(context.parameters, "id")};
const auto root {getParameterAs<RootId>(context.parameters, "id")};
if (!root && !artistId && !releaseId)
throw BadParameterGenericError {"id"};
Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& directoryNode {response.createNode("directory")};
auto transaction {context.dbSession.createSharedTransaction()};
User::pointer user {User::find(context.dbSession, context.userId)};
if (!user)
throw UserNotAuthorizedError {};
if (root)
{
directoryNode.setAttribute("id", idToString(RootId {}));
directoryNode.setAttribute("name", "Music");
auto rootArtistIds {Artist::find(context.dbSession, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName))};
for (const ArtistId rootArtistId : rootArtistIds.results)
{
const Artist::pointer artist {Artist::find(context.dbSession, rootArtistId)};
directoryNode.addArrayChild("child", createArtistNode(artist, context.dbSession, user, false /* no id3 */));
}
}
else if (artistId)
{
directoryNode.setAttribute("id", idToString(*artistId));
auto artist {Artist::find(context.dbSession, *artistId)};
if (!artist)
throw RequestedDataNotFoundError {};
directoryNode.setAttribute("name", makeNameFilesystemCompatible(artist->getName()));
const auto artistReleases {Release::find(context.dbSession, Release::FindParameters {}.setArtist(*artistId))};
for (const ReleaseId artistReleaseId : artistReleases.results)
{
const Release::pointer release {Release::find(context.dbSession, artistReleaseId)};
directoryNode.addArrayChild("child", createAlbumNode(release, context.dbSession, user, false /* no id3 */));
}
}
else if (releaseId)
{
directoryNode.setAttribute("id", idToString(*releaseId));
auto release {Release::find(context.dbSession, *releaseId)};
if (!release)
throw RequestedDataNotFoundError {};
directoryNode.setAttribute("name", makeNameFilesystemCompatible(release->getName()));
const auto tracks {Track::find(context.dbSession, Track::FindParameters {}.setRelease(*releaseId).setSortMethod(TrackSortMethod::Release))};
for (const TrackId trackId : tracks.results)
{
const Track::pointer track {Track::find(context.dbSession, trackId)};
directoryNode.addArrayChild("child", createSongNode(track, context.dbSession, user));
}
}
else
throw BadParameterGenericError {"id"};
return response;
}
static
Response
handleGetMusicFoldersRequest(RequestContext& context)
{
Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& musicFoldersNode {response.createNode("musicFolders")};
Response::Node& musicFolderNode {musicFoldersNode.createArrayChild("musicFolder")};
musicFolderNode.setAttribute("id", "0");
musicFolderNode.setAttribute("name", "Music");
return response;
}
static
Response
handleGetArtistsRequestCommon(RequestContext& context, bool id3)
{
Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& artistsNode {response.createNode(id3 ? "artists" : "indexes")};
artistsNode.setAttribute("ignoredArticles", "");
artistsNode.setAttribute("lastModified", reportedDummyDateULong);
auto transaction {context.dbSession.createSharedTransaction()};
User::pointer user {User::find(context.dbSession, context.userId)};
if (!user)
throw UserNotAuthorizedError {};
Artist::FindParameters parameters;
parameters.setSortMethod(ArtistSortMethod::BySortName);
switch (user->getSubsonicArtistListMode())
{
case SubsonicArtistListMode::AllArtists:
break;
case SubsonicArtistListMode::ReleaseArtists:
parameters.setLinkType(TrackArtistLinkType::ReleaseArtist);
break;
case SubsonicArtistListMode::TrackArtists:
parameters.setLinkType(TrackArtistLinkType::Artist);
break;
}
std::map<char, std::vector<Artist::pointer>> artistsSortedByFirstChar;
const RangeResults<ArtistId> artists {Artist::find(context.dbSession, parameters)};
for (const ArtistId artistId : artists.results)
{
const Artist::pointer artist {Artist::find(context.dbSession, artistId)};
const std::string& sortName {artist->getSortName()};
char sortChar;
if (sortName.empty() || !std::isalpha(sortName[0]))
sortChar = '?';
else
sortChar = std::toupper(sortName[0]);
artistsSortedByFirstChar[sortChar].push_back(artist);
}
for (const auto& [sortChar, artists] : artistsSortedByFirstChar)
{
Response::Node& indexNode {artistsNode.createArrayChild("index")};
indexNode.setAttribute("name", std::string {sortChar});
for (const Artist::pointer& artist :artists)
indexNode.addArrayChild("artist", createArtistNode(artist, context.dbSession, user, id3));
}
return response;
}
static
Response
handleGetIndexesRequest(RequestContext& context)
{
return handleGetArtistsRequestCommon(context, false /* no id3 */);
}
static
Response
handleGetGenresRequest(RequestContext& context)
{
Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& genresNode {response.createNode("genres")};
auto transaction {context.dbSession.createSharedTransaction()};
const ClusterType::pointer clusterType {ClusterType::find(context.dbSession, genreClusterName)};
if (clusterType)
{
const auto clusters {clusterType->getClusters()};
for (const Cluster::pointer& cluster : clusters)
genresNode.addArrayChild("genre", clusterToResponseNode(cluster));
}
return response;
}
static
Response
handleGetArtistsRequest(RequestContext& context)
{
return handleGetArtistsRequestCommon(context, true /* id3 */);
}
static
std::vector<TrackId>
findSimilarSongs(RequestContext& context, ArtistId artistId, std::size_t count)
{
// 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 {Service<Recommendation::IRecommendationService>::get()->getSimilarArtists(artistId, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, similarArtistCount)};
artistIds.push_back(artistId);
const std::size_t meanTrackCountPerArtist {(count / artistIds.size()) + 1};
auto transaction {context.dbSession.createSharedTransaction()};
std::vector<TrackId> tracks;
tracks.reserve(count);
for (const ArtistId id : artistIds)
{
Track::FindParameters params;
params.setArtist(id);
params.setRange({0, meanTrackCountPerArtist});
params.setSortMethod(TrackSortMethod::Random);
const auto artistTracks {Track::find(context.dbSession, params)};
tracks.insert(std::end(tracks),
std::begin(artistTracks.results),
std::end(artistTracks.results));
}
return tracks;
}
static
std::vector<TrackId>
findSimilarSongs(RequestContext& context, ReleaseId releaseId, std::size_t count)
{
// API says: "Returns a random collection of songs from the given artist and similar artists"
// so let's extend this for release
const std::size_t similarReleaseCount {count / 5};
std::vector<ReleaseId> releaseIds {Service<Recommendation::IRecommendationService>::get()->getSimilarReleases(releaseId, similarReleaseCount)};
releaseIds.push_back(releaseId);
const std::size_t meanTrackCountPerRelease {(count / releaseIds.size()) + 1};
auto transaction {context.dbSession.createSharedTransaction()};
std::vector<TrackId> tracks;
tracks.reserve(count);
for (const ReleaseId id : releaseIds)
{
Track::FindParameters params;
params.setRelease(id);
params.setRange({0, meanTrackCountPerRelease});
params.setSortMethod(TrackSortMethod::Random);
const auto releaseTracks {Track::find(context.dbSession, params)};
tracks.insert(std::end(tracks),
std::begin(releaseTracks.results),
std::end(releaseTracks.results));
}
return tracks;
}
static
std::vector<TrackId>
findSimilarSongs(RequestContext&, TrackId trackId, std::size_t count)
{
return Service<Recommendation::IRecommendationService>::get()->findSimilarTracks({trackId}, count);
}
static
Response
handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
{
// Optional params
std::size_t count {getParameterAs<std::size_t>(context.parameters, "count").value_or(50)};
std::vector<TrackId> tracks;
if (const auto artistId {getParameterAs<ArtistId>(context.parameters, "id")})
tracks = findSimilarSongs(context, *artistId, count);
else if (const auto releaseId {getParameterAs<ReleaseId>(context.parameters, "id")})
tracks = findSimilarSongs(context, *releaseId, count);
else if (const auto trackId {getParameterAs<TrackId>(context.parameters, "id")})
tracks = findSimilarSongs(context, *trackId, count);
else
throw BadParameterGenericError {"id"};
Random::shuffleContainer(tracks);
auto transaction {context.dbSession.createSharedTransaction()};
User::pointer user {User::find(context.dbSession, context.userId)};
if (!user)
throw UserNotAuthorizedError {};
Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& similarSongsNode {response.createNode(id3 ? "similarSongs2" : "similarSongs")};
for (const TrackId trackId : tracks)
{
const Track::pointer track {Track::find(context.dbSession, trackId)};
similarSongsNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
}
return response;
}
static
Response
handleGetSimilarSongsRequest(RequestContext& context)
{
return handleGetSimilarSongsRequestCommon(context, false /* no id3 */);
}
static
Response
handleGetSimilarSongs2Request(RequestContext& context)
{
return handleGetSimilarSongsRequestCommon(context, true /* id3 */);
}
static
Response
handleNotImplemented(RequestContext&)