diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index 79fa40a3..072b50ec 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -5,11 +5,13 @@ add_library(lmssubsonic SHARED impl/entrypoints/MediaAnnotation.cpp impl/entrypoints/MediaLibraryScanning.cpp impl/entrypoints/MediaRetrieval.cpp + impl/entrypoints/Playlists.cpp impl/entrypoints/Searching.cpp impl/entrypoints/UserManagement.cpp impl/responses/Album.cpp impl/responses/Artist.cpp impl/responses/Bookmark.cpp + impl/responses/Playlist.cpp impl/responses/Song.cpp impl/responses/User.cpp impl/ProtocolVersion.cpp diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 1088498d..1d437a16 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -51,6 +51,7 @@ #include "entrypoints/MediaAnnotation.hpp" #include "entrypoints/MediaLibraryScanning.hpp" #include "entrypoints/MediaRetrieval.hpp" +#include "entrypoints/Playlists.hpp" #include "entrypoints/Searching.hpp" #include "entrypoints/UserManagement.hpp" #include "responses/Artist.hpp" @@ -176,80 +177,7 @@ handlePingRequest(RequestContext& context) return Response::createOkResponse(context.serverProtocolVersion); } -static -Response -handleCreatePlaylistRequest(RequestContext& context) -{ - // Optional params - const auto id {getParameterAs(context.parameters, "playlistId")}; - auto name {getParameterAs(context.parameters, "name")}; - std::vector trackIds {getMultiParametersAs(context.parameters, "songId")}; - - if (!name && !id) - throw RequiredParameterMissingError {"name or id"}; - - auto transaction {context.dbSession.createUniqueTransaction()}; - - User::pointer user {User::find(context.dbSession, context.userId)}; - if (!user) - throw UserNotAuthorizedError {}; - - TrackList::pointer tracklist; - if (id) - { - tracklist = TrackList::find(context.dbSession, *id); - if (!tracklist - || tracklist->getUser() != user - || tracklist->getType() != TrackListType::Playlist) - { - throw RequestedDataNotFoundError {}; - } - - if (name) - tracklist.modify()->setName(*name); - } - else - { - tracklist = context.dbSession.create(*name, TrackListType::Playlist, false, user); - } - - for (const TrackId trackId : trackIds) - { - Track::pointer track {Track::find(context.dbSession, trackId)}; - if (!track) - continue; - - context.dbSession.create(track, tracklist); - } - - return Response::createOkResponse(context.serverProtocolVersion); -} - -static -Response -handleDeletePlaylistRequest(RequestContext& context) -{ - TrackListId id {getMandatoryParameterAs(context.parameters, "id")}; - - auto transaction {context.dbSession.createUniqueTransaction()}; - - User::pointer user {User::find(context.dbSession, context.userId)}; - if (!user) - throw UserNotAuthorizedError {}; - - TrackList::pointer tracklist {TrackList::find(context.dbSession, id)}; - if (!tracklist - || tracklist->getUser() != user - || tracklist->getType() != TrackListType::Playlist) - { - throw RequestedDataNotFoundError {}; - } - - tracklist.remove(); - - return Response::createOkResponse(context.serverProtocolVersion); -} static Response @@ -717,134 +645,6 @@ handleGetSimilarSongs2Request(RequestContext& context) return handleGetSimilarSongsRequestCommon(context, true /* id3 */); } -static -Response::Node -tracklistToResponseNode(const TrackList::pointer& tracklist, Session&) -{ - Response::Node playlistNode; - - playlistNode.setAttribute("id", idToString(tracklist->getId())); - playlistNode.setAttribute("name", tracklist->getName()); - playlistNode.setAttribute("songCount", tracklist->getCount()); - playlistNode.setAttribute("duration", std::chrono::duration_cast(tracklist->getDuration()).count()); - playlistNode.setAttribute("public", tracklist->isPublic()); - playlistNode.setAttribute("created", reportedDummyDate); - playlistNode.setAttribute("owner", tracklist->getUser()->getLoginName()); - - return playlistNode; -} - -static -Response -handleGetPlaylistRequest(RequestContext& context) -{ - // Mandatory params - TrackListId trackListId {getMandatoryParameterAs(context.parameters, "id")}; - - auto transaction {context.dbSession.createSharedTransaction()}; - - User::pointer user {User::find(context.dbSession, context.userId)}; - if (!user) - throw UserNotAuthorizedError {}; - - TrackList::pointer tracklist {TrackList::find(context.dbSession, trackListId)}; - if (!tracklist) - throw RequestedDataNotFoundError {}; - - Response response {Response::createOkResponse(context.serverProtocolVersion)}; - Response::Node playlistNode {tracklistToResponseNode(tracklist, context.dbSession)}; - - auto entries {tracklist->getEntries()}; - for (const TrackListEntry::pointer& entry : entries) - playlistNode.addArrayChild("entry", createSongNode(entry->getTrack(), context.dbSession, user)); - - response.addNode("playlist", playlistNode ); - - return response; -} - -static -Response -handleGetPlaylistsRequest(RequestContext& context) -{ - auto transaction {context.dbSession.createSharedTransaction()}; - - Response response {Response::createOkResponse(context.serverProtocolVersion)}; - Response::Node& playlistsNode {response.createNode("playlists")}; - - TrackList::FindParameters params; - params.setUser(context.userId); - params.setType(TrackListType::Playlist); - - auto tracklistIds {TrackList::find(context.dbSession, params)}; - for (const TrackListId trackListId : tracklistIds.results) - { - const TrackList::pointer trackList {TrackList::find(context.dbSession, trackListId)}; - playlistsNode.addArrayChild("playlist", tracklistToResponseNode(trackList, context.dbSession)); - } - - return response; -} - -static -Response -handleUpdatePlaylistRequest(RequestContext& context) -{ - // Mandatory params - TrackListId id {getMandatoryParameterAs(context.parameters, "playlistId")}; - - // Optional parameters - auto name {getParameterAs(context.parameters, "name")}; - auto isPublic {getParameterAs(context.parameters, "public")}; - - std::vector trackIdsToAdd {getMultiParametersAs(context.parameters, "songIdToAdd")}; - std::vector trackPositionsToRemove {getMultiParametersAs(context.parameters, "songIndexToRemove")}; - - auto transaction {context.dbSession.createUniqueTransaction()}; - - User::pointer user {User::find(context.dbSession, context.userId)}; - if (!user) - throw UserNotAuthorizedError {}; - - TrackList::pointer tracklist {TrackList::find(context.dbSession, id)}; - if (!tracklist - || tracklist->getUser() != user - || tracklist->getType() != TrackListType::Playlist) - { - throw RequestedDataNotFoundError {}; - } - - if (name) - tracklist.modify()->setName(*name); - - if (isPublic) - tracklist.modify()->setIsPublic(*isPublic); - - { - // Remove from end to make indexes stable - std::sort(std::begin(trackPositionsToRemove), std::end(trackPositionsToRemove), std::greater()); - - for (std::size_t trackPositionToRemove : trackPositionsToRemove) - { - auto entry {tracklist->getEntry(trackPositionToRemove)}; - if (entry) - entry.remove(); - } - } - - // Add tracks - for (const TrackId trackIdToAdd : trackIdsToAdd) - { - Track::pointer track {Track::find(context.dbSession, trackIdToAdd)}; - if (!track) - continue; - - context.dbSession.create(track, tracklist); - } - - return Response::createOkResponse(context.serverProtocolVersion); -} - static Response handleNotImplemented(RequestContext&) diff --git a/src/libs/subsonic/impl/entrypoints/Playlists.cpp b/src/libs/subsonic/impl/entrypoints/Playlists.cpp new file mode 100644 index 00000000..049a8dd7 --- /dev/null +++ b/src/libs/subsonic/impl/entrypoints/Playlists.cpp @@ -0,0 +1,210 @@ +/* + * 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 "Playlists.hpp" + +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/TrackList.hpp" +#include "services/database/User.hpp" +#include "responses/Playlist.hpp" +#include "responses/Song.hpp" +#include "ParameterParsing.hpp" +#include "SubsonicId.hpp" + +namespace API::Subsonic +{ + using namespace Database; + + Response handleGetPlaylistsRequest(RequestContext& context) + { + auto transaction{ context.dbSession.createSharedTransaction() }; + + Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response::Node& playlistsNode{ response.createNode("playlists") }; + + TrackList::FindParameters params; + params.setUser(context.userId); + params.setType(TrackListType::Playlist); + + auto tracklistIds{ TrackList::find(context.dbSession, params) }; + for (const TrackListId trackListId : tracklistIds.results) + { + const TrackList::pointer trackList{ TrackList::find(context.dbSession, trackListId) }; + playlistsNode.addArrayChild("playlist", createPlaylistNode(trackList, context.dbSession)); + } + + return response; + } + + Response handleGetPlaylistRequest(RequestContext& context) + { + // Mandatory params + TrackListId trackListId{ getMandatoryParameterAs(context.parameters, "id") }; + + auto transaction{ context.dbSession.createSharedTransaction() }; + + User::pointer user{ User::find(context.dbSession, context.userId) }; + if (!user) + throw UserNotAuthorizedError{}; + + TrackList::pointer tracklist{ TrackList::find(context.dbSession, trackListId) }; + if (!tracklist) + throw RequestedDataNotFoundError{}; + + Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response::Node playlistNode{ createPlaylistNode(tracklist, context.dbSession) }; + + auto entries{ tracklist->getEntries() }; + for (const TrackListEntry::pointer& entry : entries) + playlistNode.addArrayChild("entry", createSongNode(entry->getTrack(), context.dbSession, user)); + + response.addNode("playlist", playlistNode); + + return response; + } + + Response handleCreatePlaylistRequest(RequestContext& context) + { + // Optional params + const auto id{ getParameterAs(context.parameters, "playlistId") }; + auto name{ getParameterAs(context.parameters, "name") }; + + std::vector trackIds{ getMultiParametersAs(context.parameters, "songId") }; + + if (!name && !id) + throw RequiredParameterMissingError{ "name or id" }; + + auto transaction{ context.dbSession.createUniqueTransaction() }; + + User::pointer user{ User::find(context.dbSession, context.userId) }; + if (!user) + throw UserNotAuthorizedError{}; + + TrackList::pointer tracklist; + if (id) + { + tracklist = TrackList::find(context.dbSession, *id); + if (!tracklist + || tracklist->getUser() != user + || tracklist->getType() != TrackListType::Playlist) + { + throw RequestedDataNotFoundError{}; + } + + if (name) + tracklist.modify()->setName(*name); + } + else + { + tracklist = context.dbSession.create(*name, TrackListType::Playlist, false, user); + } + + for (const TrackId trackId : trackIds) + { + Track::pointer track{ Track::find(context.dbSession, trackId) }; + if (!track) + continue; + + context.dbSession.create(track, tracklist); + } + + return Response::createOkResponse(context.serverProtocolVersion); + } + + Response handleUpdatePlaylistRequest(RequestContext& context) + { + // Mandatory params + TrackListId id{ getMandatoryParameterAs(context.parameters, "playlistId") }; + + // Optional parameters + auto name{ getParameterAs(context.parameters, "name") }; + auto isPublic{ getParameterAs(context.parameters, "public") }; + + std::vector trackIdsToAdd{ getMultiParametersAs(context.parameters, "songIdToAdd") }; + std::vector trackPositionsToRemove{ getMultiParametersAs(context.parameters, "songIndexToRemove") }; + + auto transaction{ context.dbSession.createUniqueTransaction() }; + + User::pointer user{ User::find(context.dbSession, context.userId) }; + if (!user) + throw UserNotAuthorizedError{}; + + TrackList::pointer tracklist{ TrackList::find(context.dbSession, id) }; + if (!tracklist + || tracklist->getUser() != user + || tracklist->getType() != TrackListType::Playlist) + { + throw RequestedDataNotFoundError{}; + } + + if (name) + tracklist.modify()->setName(*name); + + if (isPublic) + tracklist.modify()->setIsPublic(*isPublic); + + { + // Remove from end to make indexes stable + std::sort(std::begin(trackPositionsToRemove), std::end(trackPositionsToRemove), std::greater()); + + for (std::size_t trackPositionToRemove : trackPositionsToRemove) + { + auto entry{ tracklist->getEntry(trackPositionToRemove) }; + if (entry) + entry.remove(); + } + } + + // Add tracks + for (const TrackId trackIdToAdd : trackIdsToAdd) + { + Track::pointer track{ Track::find(context.dbSession, trackIdToAdd) }; + if (!track) + continue; + + context.dbSession.create(track, tracklist); + } + + return Response::createOkResponse(context.serverProtocolVersion); + } + + Response handleDeletePlaylistRequest(RequestContext& context) + { + TrackListId id{ getMandatoryParameterAs(context.parameters, "id") }; + + auto transaction{ context.dbSession.createUniqueTransaction() }; + + User::pointer user{ User::find(context.dbSession, context.userId) }; + if (!user) + throw UserNotAuthorizedError{}; + + TrackList::pointer tracklist{ TrackList::find(context.dbSession, id) }; + if (!tracklist + || tracklist->getUser() != user + || tracklist->getType() != TrackListType::Playlist) + { + throw RequestedDataNotFoundError{}; + } + + tracklist.remove(); + + return Response::createOkResponse(context.serverProtocolVersion); + } +} \ No newline at end of file diff --git a/src/libs/subsonic/impl/entrypoints/Playlists.hpp b/src/libs/subsonic/impl/entrypoints/Playlists.hpp new file mode 100644 index 00000000..a9d44093 --- /dev/null +++ b/src/libs/subsonic/impl/entrypoints/Playlists.hpp @@ -0,0 +1,32 @@ +/* + * 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 handleGetPlaylistsRequest(RequestContext& context); + Response handleGetPlaylistRequest(RequestContext& context); + Response handleCreatePlaylistRequest(RequestContext& context); + Response handleUpdatePlaylistRequest(RequestContext& context); + Response handleDeletePlaylistRequest(RequestContext& context); +} \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/Playlist.cpp b/src/libs/subsonic/impl/responses/Playlist.cpp new file mode 100644 index 00000000..9c11d81b --- /dev/null +++ b/src/libs/subsonic/impl/responses/Playlist.cpp @@ -0,0 +1,46 @@ +/* + * 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 "Playlist.hpp" + +#include "services/database/TrackList.hpp" +#include "services/database/User.hpp" +#include "SubsonicId.hpp" + +namespace API::Subsonic +{ + using namespace Database; + + static const std::string_view reportedDummyDate{ "2000-01-01T00:00:00" }; + + Response::Node createPlaylistNode(const TrackList::pointer& tracklist, Session&) + { + Response::Node playlistNode; + + playlistNode.setAttribute("id", idToString(tracklist->getId())); + playlistNode.setAttribute("name", tracklist->getName()); + playlistNode.setAttribute("songCount", tracklist->getCount()); + playlistNode.setAttribute("duration", std::chrono::duration_cast(tracklist->getDuration()).count()); + playlistNode.setAttribute("public", tracklist->isPublic()); + playlistNode.setAttribute("created", reportedDummyDate); + playlistNode.setAttribute("owner", tracklist->getUser()->getLoginName()); + + return playlistNode; + } +} \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/Playlist.hpp b/src/libs/subsonic/impl/responses/Playlist.hpp new file mode 100644 index 00000000..2085d69c --- /dev/null +++ b/src/libs/subsonic/impl/responses/Playlist.hpp @@ -0,0 +1,34 @@ +/* + * 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 "services/database/Object.hpp" +#include "SubsonicResponse.hpp" + +namespace Database +{ + class TrackList; + class Session; +} + +namespace API::Subsonic +{ + Response::Node createPlaylistNode(const Database::ObjectPtr& tracklist, Database::Session& session); +} \ No newline at end of file