diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index 92faa800..5d043fe8 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(lmssubsonic SHARED + impl/entrypoints/AlbumSongLists.cpp impl/entrypoints/Bookmarks.cpp impl/entrypoints/MediaLibraryScanning.cpp impl/entrypoints/MediaRetrieval.cpp diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 14da8c5c..2594acd9 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -46,6 +46,7 @@ #include "utils/String.hpp" #include "utils/Utils.hpp" +#include "entrypoints/AlbumSongLists.hpp" #include "entrypoints/Bookmarks.hpp" #include "entrypoints/MediaLibraryScanning.hpp" #include "entrypoints/MediaRetrieval.hpp" @@ -263,156 +264,6 @@ handleGetLicenseRequest(RequestContext& context) return response; } -static -Response -handleGetRandomSongsRequest(RequestContext& context) -{ - // Optional params - std::size_t size {getParameterAs(context.parameters, "size").value_or(50)}; - size = std::min(size, std::size_t {500}); - - auto transaction {context.dbSession.createSharedTransaction()}; - - User::pointer user {User::find(context.dbSession, context.userId)}; - if (!user) - throw UserNotAuthorizedError {}; - - const auto trackIds {Track::find(context.dbSession, Track::FindParameters {}.setSortMethod(TrackSortMethod::Random).setRange({0, size}))}; - - Response response {Response::createOkResponse(context.serverProtocolVersion)}; - - Response::Node& randomSongsNode {response.createNode("randomSongs")}; - for (const TrackId trackId : trackIds.results) - { - const Track::pointer track {Track::find(context.dbSession, trackId)}; - randomSongsNode.addArrayChild("song", createSongNode(track, context.dbSession, user)); - } - - return response; -} - -static -Response -handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) -{ - // Mandatory params - const std::string type {getMandatoryParameterAs(context.parameters, "type")}; - - // Optional params - const std::size_t size {getParameterAs(context.parameters, "size").value_or(10)}; - const std::size_t offset {getParameterAs(context.parameters, "offset").value_or(0)}; - - const Range range {offset, size}; - - RangeResults releases; - Scrobbling::IScrobblingService& scrobbling {*Service::get()}; - - auto transaction {context.dbSession.createSharedTransaction()}; - - User::pointer user {User::find(context.dbSession, context.userId)}; - if (!user) - throw UserNotAuthorizedError {}; - - if (type == "alphabeticalByName") - { - Release::FindParameters params; - params.setSortMethod(ReleaseSortMethod::Name); - params.setRange(range); - - releases = Release::find(context.dbSession, params); - } - else if (type == "alphabeticalByArtist") - { - releases = Release::findOrderedByArtist(context.dbSession, range); - } - else if (type == "byGenre") - { - // Mandatory param - const std::string genre {getMandatoryParameterAs(context.parameters, "genre")}; - - if (const ClusterType::pointer clusterType {ClusterType::find(context.dbSession, genreClusterName)}) - { - if (const Cluster::pointer cluster {clusterType->getCluster(genre)}) - { - Release::FindParameters params; - params.setClusters({cluster->getId()}); - params.setSortMethod(ReleaseSortMethod::Name); - params.setRange(range); - - releases = Release::find(context.dbSession, params); - } - } - } - else if (type == "byYear") - { - const int fromYear {getMandatoryParameterAs(context.parameters, "fromYear")}; - const int toYear {getMandatoryParameterAs(context.parameters, "toYear")}; - - Release::FindParameters params; - params.setSortMethod(ReleaseSortMethod::Date); - params.setRange(range); - params.setDateRange(DateRange::fromYearRange(fromYear, toYear)); - - releases = Release::find(context.dbSession, params); - } - else if (type == "frequent") - { - releases = scrobbling.getTopReleases(context.userId, {}, range); - } - else if (type == "newest") - { - Release::FindParameters params; - params.setSortMethod(ReleaseSortMethod::LastWritten); - params.setRange(range); - - releases = Release::find(context.dbSession, params); - } - else if (type == "random") - { - // Random results are paginated, but there is no acceptable way to handle the pagination params without repeating some albums - // (no seed provided by subsonic, ot it would require to store some kind of context for each user/client when iterating over the random albums) - Release::FindParameters params; - params.setSortMethod(ReleaseSortMethod::Random); - params.setRange({0, size}); - - releases = Release::find(context.dbSession, params); - } - else if (type == "recent") - { - releases = scrobbling.getRecentReleases(context.userId, {}, range); - } - else if (type == "starred") - { - releases = scrobbling.getStarredReleases(context.userId, {}, range); - } - else - throw NotImplementedGenericError {}; - - Response response {Response::createOkResponse(context.serverProtocolVersion)}; - Response::Node& albumListNode {response.createNode(id3 ? "albumList2" : "albumList")}; - - for (const ReleaseId releaseId : releases.results) - { - const Release::pointer release {Release::find(context.dbSession, releaseId)}; - albumListNode.addArrayChild("album", createAlbumNode(release, context.dbSession, user, id3)); - } - - return response; -} - -static -Response -handleGetAlbumListRequest(RequestContext& context) -{ - return handleGetAlbumListRequestCommon(context, false /* no id3 */); -} - -static -Response -handleGetAlbumList2Request(RequestContext& context) -{ - return handleGetAlbumListRequestCommon(context, true /* id3 */); -} static Response @@ -865,57 +716,6 @@ handleGetSimilarSongs2Request(RequestContext& context) return handleGetSimilarSongsRequestCommon(context, true /* id3 */); } -static -Response -handleGetStarredRequestCommon(RequestContext& context, bool id3) -{ - 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& starredNode {response.createNode(id3 ? "starred2" : "starred")}; - - Scrobbling::IScrobblingService& scrobbling {*Service::get()}; - - for (const ArtistId artistId : scrobbling.getStarredArtists(context.userId, {} /* clusters */, std::nullopt /* linkType */, ArtistSortMethod::BySortName, Range {}).results) - { - if (auto artist {Artist::find(context.dbSession, artistId)}) - starredNode.addArrayChild("artist", createArtistNode(artist, context.dbSession, user, id3)); - } - - for (const ReleaseId releaseId : scrobbling.getStarredReleases(context.userId, {} /* clusters */, Range {}).results) - { - if (auto release {Release::find(context.dbSession, releaseId)}) - starredNode.addArrayChild("album", createAlbumNode(release, context.dbSession, user, id3)); - } - - for (const TrackId trackId : scrobbling.getStarredTracks(context.userId, {} /* clusters */, Range {}).results) - { - if (auto track {Track::find(context.dbSession, trackId)}) - starredNode.addArrayChild("song", createSongNode(track, context.dbSession, user)); - } - - return response; - -} - -static -Response -handleGetStarredRequest(RequestContext& context) -{ - return handleGetStarredRequestCommon(context, false /* no id3 */); -} - -static -Response -handleGetStarred2Request(RequestContext& context) -{ - return handleGetStarredRequestCommon(context, true /* id3 */); -} - static Response::Node tracklistToResponseNode(const TrackList::pointer& tracklist, Session&) @@ -985,50 +785,6 @@ handleGetPlaylistsRequest(RequestContext& context) return response; } -static -Response -handleGetSongsByGenreRequest(RequestContext& context) -{ - // Mandatory params - std::string genre {getMandatoryParameterAs(context.parameters, "genre")}; - - // Optional params - std::size_t size {getParameterAs(context.parameters, "count").value_or(10)}; - size = std::min(size, std::size_t {500}); - - std::size_t offset {getParameterAs(context.parameters, "offset").value_or(0)}; - - auto transaction {context.dbSession.createSharedTransaction()}; - - auto clusterType {ClusterType::find(context.dbSession, genreClusterName)}; - if (!clusterType) - throw RequestedDataNotFoundError {}; - - auto cluster {clusterType->getCluster(genre)}; - if (!cluster) - throw RequestedDataNotFoundError {}; - - User::pointer user {User::find(context.dbSession, context.userId)}; - if (!user) - throw UserNotAuthorizedError {}; - - Response response {Response::createOkResponse(context.serverProtocolVersion)}; - Response::Node& songsByGenreNode {response.createNode("songsByGenre")}; - - Track::FindParameters params; - params.setClusters({cluster->getId()}); - params.setRange({offset, size}); - - auto trackIds {Track::find(context.dbSession, params)}; - for (const TrackId trackId : trackIds.results) - { - const Track::pointer track {Track::find(context.dbSession, trackId)}; - songsByGenreNode.addArrayChild("song", createSongNode(track, context.dbSession, user)); - } - - return response; -} - struct StarParameters { std::vector artistIds; diff --git a/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp b/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp new file mode 100644 index 00000000..362bf311 --- /dev/null +++ b/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp @@ -0,0 +1,271 @@ +/* + * 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 . + */ + +#include "AlbumSongLists.hpp" + +#include "services/database/Artist.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/User.hpp" +#include "services/scrobbling/IScrobblingService.hpp" +#include "responses/Album.hpp" +#include "responses/Artist.hpp" +#include "responses/Song.hpp" +#include "utils/Service.hpp" +#include "ParameterParsing.hpp" + +namespace API::Subsonic +{ + using namespace Database; + + namespace { + Response handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) + { + // Mandatory params + const std::string type{ getMandatoryParameterAs(context.parameters, "type") }; + + // Optional params + const std::size_t size{ getParameterAs(context.parameters, "size").value_or(10) }; + const std::size_t offset{ getParameterAs(context.parameters, "offset").value_or(0) }; + + const Range range{ offset, size }; + + RangeResults releases; + Scrobbling::IScrobblingService& scrobbling{ *Service::get() }; + + auto transaction{ context.dbSession.createSharedTransaction() }; + + User::pointer user{ User::find(context.dbSession, context.userId) }; + if (!user) + throw UserNotAuthorizedError{}; + + if (type == "alphabeticalByName") + { + Release::FindParameters params; + params.setSortMethod(ReleaseSortMethod::Name); + params.setRange(range); + + releases = Release::find(context.dbSession, params); + } + else if (type == "alphabeticalByArtist") + { + releases = Release::findOrderedByArtist(context.dbSession, range); + } + else if (type == "byGenre") + { + // Mandatory param + const std::string genre{ getMandatoryParameterAs(context.parameters, "genre") }; + + if (const ClusterType::pointer clusterType{ ClusterType::find(context.dbSession, "GENRE") }) + { + if (const Cluster::pointer cluster{ clusterType->getCluster(genre) }) + { + Release::FindParameters params; + params.setClusters({ cluster->getId() }); + params.setSortMethod(ReleaseSortMethod::Name); + params.setRange(range); + + releases = Release::find(context.dbSession, params); + } + } + } + else if (type == "byYear") + { + const int fromYear{ getMandatoryParameterAs(context.parameters, "fromYear") }; + const int toYear{ getMandatoryParameterAs(context.parameters, "toYear") }; + + Release::FindParameters params; + params.setSortMethod(ReleaseSortMethod::Date); + params.setRange(range); + params.setDateRange(DateRange::fromYearRange(fromYear, toYear)); + + releases = Release::find(context.dbSession, params); + } + else if (type == "frequent") + { + releases = scrobbling.getTopReleases(context.userId, {}, range); + } + else if (type == "newest") + { + Release::FindParameters params; + params.setSortMethod(ReleaseSortMethod::LastWritten); + params.setRange(range); + + releases = Release::find(context.dbSession, params); + } + else if (type == "random") + { + // Random results are paginated, but there is no acceptable way to handle the pagination params without repeating some albums + // (no seed provided by subsonic, ot it would require to store some kind of context for each user/client when iterating over the random albums) + Release::FindParameters params; + params.setSortMethod(ReleaseSortMethod::Random); + params.setRange({ 0, size }); + + releases = Release::find(context.dbSession, params); + } + else if (type == "recent") + { + releases = scrobbling.getRecentReleases(context.userId, {}, range); + } + else if (type == "starred") + { + releases = scrobbling.getStarredReleases(context.userId, {}, range); + } + else + throw NotImplementedGenericError{}; + + Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response::Node& albumListNode{ response.createNode(id3 ? "albumList2" : "albumList") }; + + for (const ReleaseId releaseId : releases.results) + { + const Release::pointer release{ Release::find(context.dbSession, releaseId) }; + albumListNode.addArrayChild("album", createAlbumNode(release, context.dbSession, user, id3)); + } + + return response; + } + + Response handleGetStarredRequestCommon(RequestContext& context, bool id3) + { + 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& starredNode{ response.createNode(id3 ? "starred2" : "starred") }; + + Scrobbling::IScrobblingService& scrobbling{ *Service::get() }; + + for (const ArtistId artistId : scrobbling.getStarredArtists(context.userId, {} /* clusters */, std::nullopt /* linkType */, ArtistSortMethod::BySortName, Range{}).results) + { + if (auto artist{ Artist::find(context.dbSession, artistId) }) + starredNode.addArrayChild("artist", createArtistNode(artist, context.dbSession, user, id3)); + } + + for (const ReleaseId releaseId : scrobbling.getStarredReleases(context.userId, {} /* clusters */, Range{}).results) + { + if (auto release{ Release::find(context.dbSession, releaseId) }) + starredNode.addArrayChild("album", createAlbumNode(release, context.dbSession, user, id3)); + } + + for (const TrackId trackId : scrobbling.getStarredTracks(context.userId, {} /* clusters */, Range{}).results) + { + if (auto track{ Track::find(context.dbSession, trackId) }) + starredNode.addArrayChild("song", createSongNode(track, context.dbSession, user)); + } + + return response; + + } + } // namespace + + Response handleGetAlbumListRequest(RequestContext& context) + { + return handleGetAlbumListRequestCommon(context, false /* no id3 */); + } + + Response handleGetAlbumList2Request(RequestContext& context) + { + return handleGetAlbumListRequestCommon(context, true /* id3 */); + } + + Response handleGetRandomSongsRequest(RequestContext& context) + { + // Optional params + std::size_t size{ getParameterAs(context.parameters, "size").value_or(50) }; + size = std::min(size, std::size_t{ 500 }); + + auto transaction{ context.dbSession.createSharedTransaction() }; + + User::pointer user{ User::find(context.dbSession, context.userId) }; + if (!user) + throw UserNotAuthorizedError{}; + + const auto trackIds{ Track::find(context.dbSession, Track::FindParameters {}.setSortMethod(TrackSortMethod::Random).setRange({0, size})) }; + + Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + + Response::Node& randomSongsNode{ response.createNode("randomSongs") }; + for (const TrackId trackId : trackIds.results) + { + const Track::pointer track{ Track::find(context.dbSession, trackId) }; + randomSongsNode.addArrayChild("song", createSongNode(track, context.dbSession, user)); + } + + return response; + } + + Response handleGetSongsByGenreRequest(RequestContext& context) + { + // Mandatory params + std::string genre{ getMandatoryParameterAs(context.parameters, "genre") }; + + // Optional params + std::size_t size{ getParameterAs(context.parameters, "count").value_or(10) }; + size = std::min(size, std::size_t{ 500 }); + + std::size_t offset{ getParameterAs(context.parameters, "offset").value_or(0) }; + + auto transaction{ context.dbSession.createSharedTransaction() }; + + auto clusterType{ ClusterType::find(context.dbSession, "GENRE") }; + if (!clusterType) + throw RequestedDataNotFoundError{}; + + auto cluster{ clusterType->getCluster(genre) }; + if (!cluster) + throw RequestedDataNotFoundError{}; + + User::pointer user{ User::find(context.dbSession, context.userId) }; + if (!user) + throw UserNotAuthorizedError{}; + + Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response::Node& songsByGenreNode{ response.createNode("songsByGenre") }; + + Track::FindParameters params; + params.setClusters({ cluster->getId() }); + params.setRange({ offset, size }); + + auto trackIds{ Track::find(context.dbSession, params) }; + for (const TrackId trackId : trackIds.results) + { + const Track::pointer track{ Track::find(context.dbSession, trackId) }; + songsByGenreNode.addArrayChild("song", createSongNode(track, context.dbSession, user)); + } + + return response; + } + + Response handleGetStarredRequest(RequestContext& context) + { + return handleGetStarredRequestCommon(context, false /* no id3 */); + } + + Response handleGetStarred2Request(RequestContext& context) + { + return handleGetStarredRequestCommon(context, true /* id3 */); + } + +} \ No newline at end of file diff --git a/src/libs/subsonic/impl/entrypoints/AlbumSongLists.hpp b/src/libs/subsonic/impl/entrypoints/AlbumSongLists.hpp new file mode 100644 index 00000000..3968a78f --- /dev/null +++ b/src/libs/subsonic/impl/entrypoints/AlbumSongLists.hpp @@ -0,0 +1,33 @@ +/* + * 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 . + */ + +#pragma once + +#include "RequestContext.hpp" +#include "SubsonicResponse.hpp" + +namespace API::Subsonic +{ + Response handleGetAlbumListRequest(RequestContext& context); + Response handleGetAlbumList2Request(RequestContext& context); + Response handleGetRandomSongsRequest(RequestContext& context); + Response handleGetSongsByGenreRequest(RequestContext& context); + Response handleGetStarredRequest(RequestContext& context); + Response handleGetStarred2Request(RequestContext& context); +}