Extracted browsing entry points
This commit is contained in:
@@ -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&)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "Utils.hpp"
|
||||
|
||||
#include "utils/Service.hpp"
|
||||
#include "utils/String.hpp"
|
||||
#include "services/auth/IPasswordService.hpp"
|
||||
#include "SubsonicResponse.hpp"
|
||||
|
||||
@@ -31,4 +32,10 @@ namespace API::Subsonic::Utils
|
||||
if (!passwordService || !passwordService->canSetPasswords())
|
||||
throw NotImplementedGenericError{};
|
||||
}
|
||||
|
||||
std::string makeNameFilesystemCompatible(const std::string& name)
|
||||
{
|
||||
return StringUtils::replaceInString(name, "/", "_");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,7 +19,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace API::Subsonic::Utils
|
||||
{
|
||||
void checkSetPasswordImplemented();
|
||||
std::string makeNameFilesystemCompatible(const std::string& name);
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Browsing.hpp"
|
||||
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "services/database/Cluster.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "services/recommendation/IRecommendationService.hpp"
|
||||
#include "utils/Random.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include "responses/Album.hpp"
|
||||
#include "responses/Artist.hpp"
|
||||
#include "responses/Genre.hpp"
|
||||
#include "responses/Song.hpp"
|
||||
#include "ParameterParsing.hpp"
|
||||
#include "SubsonicId.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
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
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
std::vector<TrackId> findSimilarSongs(RequestContext&, TrackId trackId, std::size_t count)
|
||||
{
|
||||
return Service<Recommendation::IRecommendationService>::get()->findSimilarTracks({ trackId }, count);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Response handleGetIndexesRequest(RequestContext& context)
|
||||
{
|
||||
return handleGetArtistsRequestCommon(context, false /* no id3 */);
|
||||
}
|
||||
|
||||
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", Utils::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", Utils::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;
|
||||
}
|
||||
|
||||
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, "GENRE") };
|
||||
if (clusterType)
|
||||
{
|
||||
const auto clusters{ clusterType->getClusters() };
|
||||
|
||||
for (const Cluster::pointer& cluster : clusters)
|
||||
genresNode.addArrayChild("genre", createGenreNode(cluster));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response handleGetArtistsRequest(RequestContext& context)
|
||||
{
|
||||
return handleGetArtistsRequestCommon(context, true /* id3 */);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Response handleGetArtistInfoRequest(RequestContext& context)
|
||||
{
|
||||
return handleGetArtistInfoRequestCommon(context, false /* no id3 */);
|
||||
}
|
||||
|
||||
Response handleGetArtistInfo2Request(RequestContext& context)
|
||||
{
|
||||
return handleGetArtistInfoRequestCommon(context, true /* id3 */);
|
||||
}
|
||||
|
||||
Response handleGetSimilarSongsRequest(RequestContext& context)
|
||||
{
|
||||
return handleGetSimilarSongsRequestCommon(context, false /* no id3 */);
|
||||
}
|
||||
|
||||
Response handleGetSimilarSongs2Request(RequestContext& context)
|
||||
{
|
||||
return handleGetSimilarSongsRequestCommon(context, true /* id3 */);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RequestContext.hpp"
|
||||
#include "SubsonicResponse.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
Response handleGetMusicFoldersRequest(RequestContext& context);
|
||||
Response handleGetIndexesRequest(RequestContext& context);
|
||||
Response handleGetMusicDirectoryRequest(RequestContext& context);
|
||||
Response handleGetGenresRequest(RequestContext& context);
|
||||
Response handleGetArtistsRequest(RequestContext& context);
|
||||
Response handleGetArtistRequest(RequestContext& context);
|
||||
Response handleGetAlbumRequest(RequestContext& context);
|
||||
Response handleGetSongRequest(RequestContext& context);
|
||||
Response handleGetArtistInfoRequest(RequestContext& context);
|
||||
Response handleGetArtistInfo2Request(RequestContext& context);
|
||||
Response handleGetSimilarSongsRequest(RequestContext& context);
|
||||
Response handleGetSimilarSongs2Request(RequestContext& context);
|
||||
}
|
||||
@@ -21,7 +21,6 @@
|
||||
|
||||
#include "services/database/TrackBookmark.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "SubsonicResponse.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "responses/Genre.hpp"
|
||||
|
||||
#include "services/database/Cluster.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
Response::Node createGenreNode(const Database::Cluster::pointer& cluster)
|
||||
{
|
||||
Response::Node clusterNode;
|
||||
|
||||
clusterNode.setValue(cluster->getName());
|
||||
clusterNode.setAttribute("songCount", cluster->getTracksCount());
|
||||
clusterNode.setAttribute("albumCount", cluster->getReleasesCount());
|
||||
|
||||
return clusterNode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "services/database/Cluster.hpp"
|
||||
|
||||
#include "services/database/Object.hpp"
|
||||
#include "SubsonicResponse.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Cluster;
|
||||
}
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
Response::Node createGenreNode(const Database::ObjectPtr<Database::Cluster>& cluster);
|
||||
}
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "utils/String.hpp"
|
||||
#include "responses/Artist.hpp"
|
||||
#include "SubsonicId.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
@@ -54,11 +55,6 @@ namespace API::Subsonic
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string makeNameFilesystemCompatible(std::string_view name)
|
||||
{
|
||||
return StringUtils::replaceInString(name, "/", "_");
|
||||
}
|
||||
|
||||
std::string getTrackPath(const Track::pointer& track)
|
||||
{
|
||||
std::string path;
|
||||
@@ -75,9 +71,9 @@ namespace API::Subsonic
|
||||
if (artists.size() > 1)
|
||||
path = "Various Artists/";
|
||||
else if (artists.size() == 1)
|
||||
path = makeNameFilesystemCompatible(artists.front()->getName()) + "/";
|
||||
path = Utils::makeNameFilesystemCompatible(artists.front()->getName()) + "/";
|
||||
|
||||
path += makeNameFilesystemCompatible(track->getRelease()->getName()) + "/";
|
||||
path += Utils::makeNameFilesystemCompatible(track->getRelease()->getName()) + "/";
|
||||
}
|
||||
|
||||
if (track->getDiscNumber())
|
||||
@@ -85,7 +81,7 @@ namespace API::Subsonic
|
||||
if (track->getTrackNumber())
|
||||
path += std::to_string(*track->getTrackNumber()) + "-";
|
||||
|
||||
path += makeNameFilesystemCompatible(track->getName());
|
||||
path += Utils::makeNameFilesystemCompatible(track->getName());
|
||||
|
||||
if (track->getPath().has_extension())
|
||||
path += track->getPath().extension();
|
||||
|
||||
Reference in New Issue
Block a user