First working jukebox using Subsonic API

This commit is contained in:
emeric
2026-02-17 23:16:52 +01:00
parent c338521843
commit 3963969cec
39 changed files with 1464 additions and 277 deletions
+5 -3
View File
@@ -6,6 +6,7 @@ add_library(lmssubsonic STATIC
impl/endpoints/AlbumSongLists.cpp
impl/endpoints/Bookmarks.cpp
impl/endpoints/Browsing.cpp
impl/endpoints/Jukebox.cpp
impl/endpoints/MediaAnnotation.cpp
impl/endpoints/MediaLibraryScanning.cpp
impl/endpoints/MediaRetrieval.cpp
@@ -54,18 +55,19 @@ target_include_directories(lmssubsonic PRIVATE
)
target_link_libraries(lmssubsonic PRIVATE
std::filesystem
lmsartwork
lmsauth
lmsaudio
lmsauth
lmscore
lmsdatabase
lmsfeedback
lmsjukebox
lmspodcast
lmsrecommendation
lmsscanner
lmsscrobbling
lmstranscoding
lmscore
std::filesystem
)
target_link_libraries(lmssubsonic PUBLIC
+2 -1
View File
@@ -42,6 +42,7 @@
#include "endpoints/AlbumSongLists.hpp"
#include "endpoints/Bookmarks.hpp"
#include "endpoints/Browsing.hpp"
#include "endpoints/Jukebox.hpp"
#include "endpoints/MediaAnnotation.hpp"
#include "endpoints/MediaLibraryScanning.hpp"
#include "endpoints/MediaRetrieval.hpp"
@@ -208,7 +209,7 @@ namespace lms::api::subsonic
{ "/getPodcastEpisode", { handleGetPodcastEpisode } },
// Jukebox
{ "/jukeboxControl", { handleNotImplemented } },
{ "/jukeboxControl", { handleJukeboxControl } },
// Internet radio
{ "/getInternetRadioStations", { handleNotImplemented } },
@@ -0,0 +1,195 @@
/*
* Copyright (C) 2026 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 "Jukebox.hpp"
#include <functional>
#include "core/Service.hpp"
#include "database/Session.hpp"
#include "database/objects/Track.hpp"
#include "database/objects/User.hpp"
#include "responses/Song.hpp"
#include "services/jukebox/IJukeboxService.hpp"
#include "ParameterParsing.hpp"
#include "SubsonicId.hpp"
#include "SubsonicResponse.hpp"
namespace lms::api::subsonic
{
namespace detail
{
Response::Node createJukeboxStatusNode(const jukebox::IJukeboxService& jukeboxService)
{
Response::Node statusNode;
statusNode.setAttribute("currentIndex", jukeboxService.getCurrentTrackIndex() ? *jukeboxService.getCurrentTrackIndex() : -1); // required
statusNode.setAttribute("playing", !jukeboxService.isPaused()); // required
statusNode.setAttribute("position", std::chrono::duration_cast<std::chrono::seconds>(jukeboxService.getPlaybackTrackTime()).count());
statusNode.setAttribute("gain", 1.f);
return statusNode;
}
Response handleJukeboxGet(RequestContext& context, jukebox::IJukeboxService& jukeboxService)
{
Response response{ Response::createOkResponse(context.getServerProtocolVersion()) };
Response::Node jukeboxPlaylistNode{ createJukeboxStatusNode(jukeboxService) };
{
auto transaction{ context.getDbSession().createReadTransaction() };
for (const db::TrackId trackId : jukeboxService.getTracks())
{
if (const db::Track::pointer track{ db::Track::find(context.getDbSession(), trackId) })
jukeboxPlaylistNode.addArrayChild("entry", createSongNode(context, track, true));
}
}
response.addNode("jukeboxPlaylist", std::move(jukeboxPlaylistNode));
return response;
}
Response handleJukeboxStatus(RequestContext& context, jukebox::IJukeboxService& jukeboxService)
{
Response response{ Response::createOkResponse(context.getServerProtocolVersion()) };
response.addNode("jukeboxStatus", createJukeboxStatusNode(jukeboxService));
return response;
}
Response handleJukeboxSet(RequestContext& context, jukebox::IJukeboxService& jukeboxService)
{
const auto trackIds{ getMultiParametersAs<db::TrackId>(context.getParameters(), "id") };
// set is similar to a clear followed by a add, but will not change the currently playing track
jukeboxService.clearTracks();
jukeboxService.appendTracks(trackIds);
Response response{ Response::createOkResponse(context.getServerProtocolVersion()) };
response.addNode("jukeboxStatus", createJukeboxStatusNode(jukeboxService));
return response;
}
Response handleJukeboxStart(RequestContext& context, jukebox::IJukeboxService& jukeboxService)
{
jukeboxService.resume();
Response response{ Response::createOkResponse(context.getServerProtocolVersion()) };
response.addNode("jukeboxStatus", createJukeboxStatusNode(jukeboxService));
return response;
}
Response handleJukeboxStop(RequestContext& context, jukebox::IJukeboxService& jukeboxService)
{
jukeboxService.pause();
Response response{ Response::createOkResponse(context.getServerProtocolVersion()) };
response.addNode("jukeboxStatus", createJukeboxStatusNode(jukeboxService));
return response;
}
Response handleJukeboxSkip(RequestContext& context, jukebox::IJukeboxService& jukeboxService)
{
const auto index{ getMandatoryParameterAs<std::size_t>(context.getParameters(), "index") };
const auto offset{ getParameterAs<std::chrono::seconds::rep>(context.getParameters(), "offset").value_or(0) };
// do not report potential range error
jukeboxService.play(index, std::chrono::seconds{ offset });
Response response{ Response::createOkResponse(context.getServerProtocolVersion()) };
response.addNode("jukeboxStatus", createJukeboxStatusNode(jukeboxService));
return response;
}
Response handleJukeboxAdd(RequestContext& context, jukebox::IJukeboxService& jukeboxService)
{
const auto trackIds{ getMandatoryMultiParametersAs<db::TrackId>(context.getParameters(), "id") };
jukeboxService.appendTracks(trackIds);
Response response{ Response::createOkResponse(context.getServerProtocolVersion()) };
response.addNode("jukeboxStatus", createJukeboxStatusNode(jukeboxService));
return response;
}
Response handleJukeboxClear(RequestContext& context, jukebox::IJukeboxService& jukeboxService)
{
jukeboxService.clearTracks();
Response response{ Response::createOkResponse(context.getServerProtocolVersion()) };
response.addNode("jukeboxStatus", createJukeboxStatusNode(jukeboxService));
return response;
}
Response handleJukeboxRemove(RequestContext& context, jukebox::IJukeboxService& jukeboxService)
{
const auto index{ getMandatoryParameterAs<std::size_t>(context.getParameters(), "index") };
jukeboxService.removeTrack(index);
Response response{ Response::createOkResponse(context.getServerProtocolVersion()) };
response.addNode("jukeboxStatus", createJukeboxStatusNode(jukeboxService));
return response;
}
Response handleJukeboxShuffle(RequestContext& context, jukebox::IJukeboxService& jukeboxService)
{
jukeboxService.shuffleTracks();
Response response{ Response::createOkResponse(context.getServerProtocolVersion()) };
response.addNode("jukeboxStatus", createJukeboxStatusNode(jukeboxService));
return response;
}
using Actionhandler = std::function<Response(RequestContext& context, jukebox::IJukeboxService& jukeboxService)>;
static const std::unordered_map<std::string, Actionhandler> actionHandlers{
{ "get", detail::handleJukeboxGet },
{ "status", detail::handleJukeboxStatus },
{ "set", detail::handleJukeboxSet },
{ "start", detail::handleJukeboxStart },
{ "stop", detail::handleJukeboxStop },
{ "skip", detail::handleJukeboxSkip },
{ "add", detail::handleJukeboxAdd },
{ "clear", detail::handleJukeboxClear },
{ "remove", detail::handleJukeboxRemove },
{ "shuffle", detail::handleJukeboxShuffle },
{ "setGain", detail::handleJukeboxStatus }, // not implemented
};
} // namespace detail
Response handleJukeboxControl(RequestContext& context)
{
const std::string action{ getMandatoryParameterAs<std::string>(context.getParameters(), "action") };
jukebox::IJukeboxService* jukeboxService{ core::Service<jukebox::IJukeboxService>::get() };
if (!jukeboxService)
throw InternalErrorGenericError{ "Jukebox not available!" };
if (!context.getUser()->isAdmin())
throw UserNotAuthorizedError{};
auto itActionHandler{ detail::actionHandlers.find(action) };
if (itActionHandler == std::end(detail::actionHandlers))
throw BadParameterGenericError{ "action" };
return itActionHandler->second(context, *jukeboxService);
}
} // namespace lms::api::subsonic
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2026 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 lms::api::subsonic
{
Response handleJukeboxControl(RequestContext& context);
} // namespace lms::api::subsonic
+1 -1
View File
@@ -42,7 +42,7 @@ namespace lms::api::subsonic
userNode.setAttribute("commentRole", false); // Whether the user is allowed to create and edit comments and ratings
userNode.setAttribute("podcastRole", user->isAdmin()); // Whether the user is allowed to administrate Podcasts
userNode.setAttribute("streamRole", true); // Whether the user is allowed to play files
userNode.setAttribute("jukeboxRole", false); // not supported
userNode.setAttribute("jukeboxRole", user->isAdmin()); // Whether the user is allowed to control the jukebox
userNode.setAttribute("shareRole", false); // not supported
// users can access all libraries