Extracted playlists entry points

This commit is contained in:
emeric
2023-10-01 23:37:48 +02:00
parent 70d0c26f2d
commit 247975d304
6 changed files with 325 additions and 201 deletions
+1 -201
View File
@@ -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<TrackListId>(context.parameters, "playlistId")};
auto name {getParameterAs<std::string>(context.parameters, "name")};
std::vector<TrackId> trackIds {getMultiParametersAs<TrackId>(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<TrackList>(*name, TrackListType::Playlist, false, user);
}
for (const TrackId trackId : trackIds)
{
Track::pointer track {Track::find(context.dbSession, trackId)};
if (!track)
continue;
context.dbSession.create<TrackListEntry>(track, tracklist);
}
return Response::createOkResponse(context.serverProtocolVersion);
}
static
Response
handleDeletePlaylistRequest(RequestContext& context)
{
TrackListId id {getMandatoryParameterAs<TrackListId>(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<std::chrono::seconds>(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<TrackListId>(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<TrackListId>(context.parameters, "playlistId")};
// Optional parameters
auto name {getParameterAs<std::string>(context.parameters, "name")};
auto isPublic {getParameterAs<bool>(context.parameters, "public")};
std::vector<TrackId> trackIdsToAdd {getMultiParametersAs<TrackId>(context.parameters, "songIdToAdd")};
std::vector<std::size_t> trackPositionsToRemove {getMultiParametersAs<std::size_t>(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<std::size_t>());
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<TrackListEntry>(track, tracklist);
}
return Response::createOkResponse(context.serverProtocolVersion);
}
static
Response
handleNotImplemented(RequestContext&)
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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<TrackListId>(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<TrackListId>(context.parameters, "playlistId") };
auto name{ getParameterAs<std::string>(context.parameters, "name") };
std::vector<TrackId> trackIds{ getMultiParametersAs<TrackId>(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<TrackList>(*name, TrackListType::Playlist, false, user);
}
for (const TrackId trackId : trackIds)
{
Track::pointer track{ Track::find(context.dbSession, trackId) };
if (!track)
continue;
context.dbSession.create<TrackListEntry>(track, tracklist);
}
return Response::createOkResponse(context.serverProtocolVersion);
}
Response handleUpdatePlaylistRequest(RequestContext& context)
{
// Mandatory params
TrackListId id{ getMandatoryParameterAs<TrackListId>(context.parameters, "playlistId") };
// Optional parameters
auto name{ getParameterAs<std::string>(context.parameters, "name") };
auto isPublic{ getParameterAs<bool>(context.parameters, "public") };
std::vector<TrackId> trackIdsToAdd{ getMultiParametersAs<TrackId>(context.parameters, "songIdToAdd") };
std::vector<std::size_t> trackPositionsToRemove{ getMultiParametersAs<std::size_t>(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<std::size_t>());
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<TrackListEntry>(track, tracklist);
}
return Response::createOkResponse(context.serverProtocolVersion);
}
Response handleDeletePlaylistRequest(RequestContext& context)
{
TrackListId id{ getMandatoryParameterAs<TrackListId>(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);
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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);
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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<std::chrono::seconds>(tracklist->getDuration()).count());
playlistNode.setAttribute("public", tracklist->isPublic());
playlistNode.setAttribute("created", reportedDummyDate);
playlistNode.setAttribute("owner", tracklist->getUser()->getLoginName());
return playlistNode;
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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<Database::TrackList>& tracklist, Database::Session& session);
}