Subsonic API: better compability with clients
This commit is contained in:
@@ -48,13 +48,13 @@ __Notes on the self-organizing map__:
|
|||||||
* to enable the audio similarity source, you have to enable it first in the administration panel.
|
* to enable the audio similarity source, you have to enable it first in the administration panel.
|
||||||
|
|
||||||
## Subsonic API
|
## Subsonic API
|
||||||
The API version implemented is 1.12.0 and has been tested on _Android_ using the official application, _Ultrasonic_ and _DSub_.
|
The API version implemented is 1.16.0 and has been tested on _Android_ using _Subsonic Player_, _Ultrasonic_ and _DSub_.
|
||||||
|
|
||||||
Since _LMS_ uses metadata tags to organize music, a compatibility mode is used to navigate through the collection using the directory browsing commands.
|
Since _LMS_ uses metadata tags to organize music, a compatibility mode is used to navigate through the collection when using the directory browsing commands.
|
||||||
|
|
||||||
The Subsonic API is enabled by default.
|
The Subsonic API is enabled by default.
|
||||||
|
|
||||||
__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method defined from version 1.13.0.
|
__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method. You may need to check your client to make sure to use the __password__ authentication method.
|
||||||
|
|
||||||
## About tags
|
## About tags
|
||||||
_LMS_ relies exclusively on tags to organize your music collection.
|
_LMS_ relies exclusively on tags to organize your music collection.
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ login-throttler-max-entries = 10000;
|
|||||||
# API
|
# API
|
||||||
api-subsonic = true;
|
api-subsonic = true;
|
||||||
|
|
||||||
|
# Use this list to make the reported server version to 1.12.0 depending on the client's name
|
||||||
|
# Main usage is to make auto detections for the 'p' (password) parameter work
|
||||||
|
api-subsonic-report-old-server-protocol = ("DSub");
|
||||||
|
|
||||||
# Turn on this option to allow the demo account creation/use
|
# Turn on this option to allow the demo account creation/use
|
||||||
demo = false;
|
demo = false;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
add_library(lmssubsonic SHARED
|
add_library(lmssubsonic SHARED
|
||||||
|
impl/ProtocolVersion.cpp
|
||||||
impl/Scan.cpp
|
impl/Scan.cpp
|
||||||
impl/Stream.cpp
|
impl/Stream.cpp
|
||||||
impl/SubsonicId.cpp
|
impl/SubsonicId.cpp
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 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 <string>
|
||||||
|
#include "ProtocolVersion.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
struct ClientInfo
|
||||||
|
{
|
||||||
|
std::string name;
|
||||||
|
std::string user;
|
||||||
|
std::string password;
|
||||||
|
ProtocolVersion version;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* copyright (c) 2021 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 "ProtocolVersion.hpp"
|
||||||
|
|
||||||
|
namespace StringUtils
|
||||||
|
{
|
||||||
|
template<>
|
||||||
|
std::optional<API::Subsonic::ProtocolVersion>
|
||||||
|
readAs(std::string_view str)
|
||||||
|
{
|
||||||
|
// Expects "X.Y.Z"
|
||||||
|
const auto numbers {StringUtils::splitString(str, ".")};
|
||||||
|
if (numbers.size() < 2 || numbers.size() > 3)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
API::Subsonic::ProtocolVersion version;
|
||||||
|
|
||||||
|
auto number {StringUtils::readAs<unsigned>(numbers[0])};
|
||||||
|
if (!number)
|
||||||
|
return std::nullopt;
|
||||||
|
version.major = *number;
|
||||||
|
|
||||||
|
number = {StringUtils::readAs<unsigned>(numbers[1])};
|
||||||
|
if (!number)
|
||||||
|
return std::nullopt;
|
||||||
|
version.minor = *number;
|
||||||
|
|
||||||
|
if (numbers.size() == 3)
|
||||||
|
{
|
||||||
|
number = {StringUtils::readAs<unsigned>(numbers[2])};
|
||||||
|
if (!number)
|
||||||
|
return std::nullopt;
|
||||||
|
version.patch = *number;
|
||||||
|
}
|
||||||
|
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* copyright (c) 2021 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 "utils/String.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
struct ProtocolVersion
|
||||||
|
{
|
||||||
|
unsigned major {};
|
||||||
|
unsigned minor {};
|
||||||
|
unsigned patch {};
|
||||||
|
};
|
||||||
|
|
||||||
|
static inline constexpr ProtocolVersion defaultServerProtocolVersion {1, 16, 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace StringUtils
|
||||||
|
{
|
||||||
|
template<> std::optional<API::Subsonic::ProtocolVersion> readAs(std::string_view str);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -24,6 +24,8 @@
|
|||||||
#include <Wt/Http/Request.h>
|
#include <Wt/Http/Request.h>
|
||||||
|
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
|
#include "ClientInfo.hpp"
|
||||||
|
#include "ProtocolVersion.hpp"
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
@@ -37,7 +39,8 @@ namespace API::Subsonic
|
|||||||
const Wt::Http::ParameterMap& parameters;
|
const Wt::Http::ParameterMap& parameters;
|
||||||
Database::Session& dbSession;
|
Database::Session& dbSession;
|
||||||
Database::UserId userId;
|
Database::UserId userId;
|
||||||
std::string clientName;
|
ClientInfo clientInfo;
|
||||||
|
ProtocolVersion serverProtocolVersion;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ namespace API::Subsonic::Scan
|
|||||||
Response
|
Response
|
||||||
handleGetScanStatus(RequestContext& context)
|
handleGetScanStatus(RequestContext& context)
|
||||||
{
|
{
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
response.addNode("scanStatus", createStatusResponseNode());
|
response.addNode("scanStatus", createStatusResponseNode());
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@@ -63,7 +63,7 @@ namespace API::Subsonic::Scan
|
|||||||
{
|
{
|
||||||
Service<IScanner>::get()->requestImmediateScan(false);
|
Service<IScanner>::get()->requestImmediateScan(false);
|
||||||
|
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
response.addNode("scanStatus", createStatusResponseNode());
|
response.addNode("scanStatus", createStatusResponseNode());
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
#include "subsonic/SubsonicResource.hpp"
|
|
||||||
|
#include "SubsonicResource.hpp"
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
@@ -39,12 +40,14 @@
|
|||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
#include "recommendation/IEngine.hpp"
|
#include "recommendation/IEngine.hpp"
|
||||||
#include "scrobbling/IScrobbling.hpp"
|
#include "scrobbling/IScrobbling.hpp"
|
||||||
|
#include "utils/IConfig.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
#include "utils/Random.hpp"
|
#include "utils/Random.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
#include "utils/Utils.hpp"
|
#include "utils/Utils.hpp"
|
||||||
#include "ParameterParsing.hpp"
|
#include "ParameterParsing.hpp"
|
||||||
|
#include "ProtocolVersion.hpp"
|
||||||
#include "RequestContext.hpp"
|
#include "RequestContext.hpp"
|
||||||
#include "Scan.hpp"
|
#include "Scan.hpp"
|
||||||
#include "Stream.hpp"
|
#include "Stream.hpp"
|
||||||
@@ -58,56 +61,18 @@ static const std::string reportedStarredDate {"2000-01-01T00:00:00"};
|
|||||||
static const std::string reportedDummyDate {"2000-01-01T00:00:00"};
|
static const std::string reportedDummyDate {"2000-01-01T00:00:00"};
|
||||||
static const unsigned long long reportedDummyDateULong {946684800000ULL}; // 2000-01-01T00:00:00 UTC
|
static const unsigned long long reportedDummyDateULong {946684800000ULL}; // 2000-01-01T00:00:00 UTC
|
||||||
|
|
||||||
namespace API::Subsonic
|
|
||||||
{
|
|
||||||
struct ClientVersion
|
|
||||||
{
|
|
||||||
unsigned major {};
|
|
||||||
unsigned minor {};
|
|
||||||
unsigned patch {};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace StringUtils
|
|
||||||
{
|
|
||||||
template<>
|
|
||||||
std::optional<API::Subsonic::ClientVersion>
|
|
||||||
readAs(std::string_view str)
|
|
||||||
{
|
|
||||||
// Expects "X.Y.Z"
|
|
||||||
const auto numbers {StringUtils::splitString(str, ".")};
|
|
||||||
if (numbers.size() < 2 || numbers.size() > 3)
|
|
||||||
return std::nullopt;
|
|
||||||
|
|
||||||
API::Subsonic::ClientVersion version;
|
|
||||||
|
|
||||||
auto number {StringUtils::readAs<unsigned>(numbers[0])};
|
|
||||||
if (!number)
|
|
||||||
return std::nullopt;
|
|
||||||
version.major = *number;
|
|
||||||
|
|
||||||
number = {StringUtils::readAs<unsigned>(numbers[1])};
|
|
||||||
if (!number)
|
|
||||||
return std::nullopt;
|
|
||||||
version.minor = *number;
|
|
||||||
|
|
||||||
if (numbers.size() == 3)
|
|
||||||
{
|
|
||||||
number = {StringUtils::readAs<unsigned>(numbers[2])};
|
|
||||||
if (!number)
|
|
||||||
return std::nullopt;
|
|
||||||
version.patch = *number;
|
|
||||||
}
|
|
||||||
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
|
|
||||||
|
std::unique_ptr<Wt::WResource>
|
||||||
|
createSubsonicResource(Database::Db& db)
|
||||||
|
{
|
||||||
|
return std::make_unique<SubsonicResource>(db);
|
||||||
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
void
|
void
|
||||||
checkSetPasswordImplemented()
|
checkSetPasswordImplemented()
|
||||||
@@ -140,38 +105,24 @@ decodePasswordIfNeeded(const std::string& password)
|
|||||||
return password;
|
return password;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ClientInfo
|
|
||||||
{
|
|
||||||
std::string name;
|
|
||||||
std::string user;
|
|
||||||
std::string password;
|
|
||||||
ClientVersion version;
|
|
||||||
};
|
|
||||||
|
|
||||||
static
|
static
|
||||||
ClientInfo
|
std::unordered_map<std::string, ProtocolVersion>
|
||||||
getClientInfo(const Wt::Http::ParameterMap& parameters)
|
readConfigProtocolVersions()
|
||||||
{
|
{
|
||||||
ClientInfo res;
|
std::unordered_map<std::string, ProtocolVersion> res;
|
||||||
|
|
||||||
// Mandatory parameters
|
Service<IConfig>::get()->visitStrings("api-subsonic-report-old-server-protocol",
|
||||||
res.name = getMandatoryParameterAs<std::string>(parameters, "c");
|
[&](std::string_view client)
|
||||||
res.version = getMandatoryParameterAs<ClientVersion>(parameters, "v");
|
{
|
||||||
if (res.version.major > API_VERSION_MAJOR)
|
res.emplace(std::string {client}, ProtocolVersion {1, 12, 0});
|
||||||
throw ServerMustUpgradeError {};
|
}, {"DSub"});
|
||||||
if (res.version.major < API_VERSION_MAJOR)
|
|
||||||
throw ClientMustUpgradeError {};
|
|
||||||
if (res.version.minor > Response::getAPIMinorVersion(res.name))
|
|
||||||
throw ServerMustUpgradeError {};
|
|
||||||
|
|
||||||
res.user = getMandatoryParameterAs<std::string>(parameters, "u");
|
|
||||||
res.password = decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(parameters, "p"));
|
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
SubsonicResource::SubsonicResource(Db& db)
|
SubsonicResource::SubsonicResource(Db& db)
|
||||||
: _db {db}
|
: _serverProtocolVersionsByClient {readConfigProtocolVersions()}
|
||||||
|
, _db {db}
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,7 +481,7 @@ static
|
|||||||
Response
|
Response
|
||||||
handlePingRequest(RequestContext& context)
|
handlePingRequest(RequestContext& context)
|
||||||
{
|
{
|
||||||
return Response::createOkResponse(context);
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
@@ -570,7 +521,7 @@ handleChangePassword(RequestContext& context)
|
|||||||
throw UserNotAuthorizedError {};
|
throw UserNotAuthorizedError {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response::createOkResponse(context);
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
@@ -620,7 +571,7 @@ handleCreatePlaylistRequest(RequestContext& context)
|
|||||||
TrackListEntry::create(context.dbSession, track, tracklist );
|
TrackListEntry::create(context.dbSession, track, tracklist );
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response::createOkResponse(context);
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
@@ -671,7 +622,7 @@ handleCreateUserRequest(RequestContext& context)
|
|||||||
throw UserNotAuthorizedError {};
|
throw UserNotAuthorizedError {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response::createOkResponse(context);
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
@@ -696,7 +647,7 @@ handleDeletePlaylistRequest(RequestContext& context)
|
|||||||
|
|
||||||
tracklist.remove();
|
tracklist.remove();
|
||||||
|
|
||||||
return Response::createOkResponse(context);
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
@@ -717,14 +668,14 @@ handleDeleteUserRequest(RequestContext& context)
|
|||||||
|
|
||||||
user.remove();
|
user.remove();
|
||||||
|
|
||||||
return Response::createOkResponse(context);
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
Response
|
Response
|
||||||
handleGetLicenseRequest(RequestContext& context)
|
handleGetLicenseRequest(RequestContext& context)
|
||||||
{
|
{
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
|
|
||||||
Response::Node& licenseNode {response.createNode("license")};
|
Response::Node& licenseNode {response.createNode("license")};
|
||||||
licenseNode.setAttribute("licenseExpires", "2025-09-03T14:46:43");
|
licenseNode.setAttribute("licenseExpires", "2025-09-03T14:46:43");
|
||||||
@@ -750,7 +701,7 @@ handleGetRandomSongsRequest(RequestContext& context)
|
|||||||
|
|
||||||
auto tracks {Track::getAllRandom(context.dbSession, {}, size)};
|
auto tracks {Track::getAllRandom(context.dbSession, {}, size)};
|
||||||
|
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
|
|
||||||
Response::Node& randomSongsNode {response.createNode("randomSongs")};
|
Response::Node& randomSongsNode {response.createNode("randomSongs")};
|
||||||
for (const Track::pointer& track : tracks)
|
for (const Track::pointer& track : tracks)
|
||||||
@@ -839,7 +790,7 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
|
|||||||
else
|
else
|
||||||
throw NotImplementedGenericError {};
|
throw NotImplementedGenericError {};
|
||||||
|
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
Response::Node& albumListNode {response.createNode(id3 ? "albumList2" : "albumList")};
|
Response::Node& albumListNode {response.createNode(id3 ? "albumList2" : "albumList")};
|
||||||
|
|
||||||
for (const Release::pointer& release : releases)
|
for (const Release::pointer& release : releases)
|
||||||
@@ -879,7 +830,7 @@ handleGetAlbumRequest(RequestContext& context)
|
|||||||
if (!user)
|
if (!user)
|
||||||
throw UserNotAuthorizedError {};
|
throw UserNotAuthorizedError {};
|
||||||
|
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
Response::Node releaseNode {releaseToResponseNode(release, context.dbSession, user, true /* id3 */)};
|
Response::Node releaseNode {releaseToResponseNode(release, context.dbSession, user, true /* id3 */)};
|
||||||
|
|
||||||
auto tracks {release->getTracks()};
|
auto tracks {release->getTracks()};
|
||||||
@@ -908,7 +859,7 @@ handleGetArtistRequest(RequestContext& context)
|
|||||||
if (!user)
|
if (!user)
|
||||||
throw UserNotAuthorizedError {};
|
throw UserNotAuthorizedError {};
|
||||||
|
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
Response::Node artistNode {artistToResponseNode(user, artist, true /* id3 */)};
|
Response::Node artistNode {artistToResponseNode(user, artist, true /* id3 */)};
|
||||||
|
|
||||||
auto releases {artist->getReleases()};
|
auto releases {artist->getReleases()};
|
||||||
@@ -930,7 +881,7 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3)
|
|||||||
// Optional params
|
// Optional params
|
||||||
std::size_t count {getParameterAs<std::size_t>(context.parameters, "count").value_or(20)};
|
std::size_t count {getParameterAs<std::size_t>(context.parameters, "count").value_or(20)};
|
||||||
|
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
Response::Node& artistInfoNode {response.createNode(id3 ? "artistInfo2" : "artistInfo")};
|
Response::Node& artistInfoNode {response.createNode(id3 ? "artistInfo2" : "artistInfo")};
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -986,7 +937,7 @@ static
|
|||||||
Response
|
Response
|
||||||
handleGetArtistsRequest(RequestContext& context)
|
handleGetArtistsRequest(RequestContext& context)
|
||||||
{
|
{
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
|
|
||||||
Response::Node& artistsNode {response.createNode("artists")};
|
Response::Node& artistsNode {response.createNode("artists")};
|
||||||
artistsNode.setAttribute("ignoredArticles", "");
|
artistsNode.setAttribute("ignoredArticles", "");
|
||||||
@@ -1040,7 +991,7 @@ handleGetMusicDirectoryRequest(RequestContext& context)
|
|||||||
if (!root && !artistId && !releaseId && !trackId)
|
if (!root && !artistId && !releaseId && !trackId)
|
||||||
throw BadParameterGenericError {"id"};
|
throw BadParameterGenericError {"id"};
|
||||||
|
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
Response::Node& directoryNode {response.createNode("directory")};
|
Response::Node& directoryNode {response.createNode("directory")};
|
||||||
|
|
||||||
auto transaction {context.dbSession.createSharedTransaction()};
|
auto transaction {context.dbSession.createSharedTransaction()};
|
||||||
@@ -1097,7 +1048,7 @@ static
|
|||||||
Response
|
Response
|
||||||
handleGetMusicFoldersRequest(RequestContext& context)
|
handleGetMusicFoldersRequest(RequestContext& context)
|
||||||
{
|
{
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
Response::Node& musicFoldersNode {response.createNode("musicFolders")};
|
Response::Node& musicFoldersNode {response.createNode("musicFolders")};
|
||||||
|
|
||||||
Response::Node& musicFolderNode {musicFoldersNode.createArrayChild("musicFolder")};
|
Response::Node& musicFolderNode {musicFoldersNode.createArrayChild("musicFolder")};
|
||||||
@@ -1111,7 +1062,7 @@ static
|
|||||||
Response
|
Response
|
||||||
handleGetGenresRequest(RequestContext& context)
|
handleGetGenresRequest(RequestContext& context)
|
||||||
{
|
{
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
|
|
||||||
Response::Node& genresNode {response.createNode("genres")};
|
Response::Node& genresNode {response.createNode("genres")};
|
||||||
|
|
||||||
@@ -1133,7 +1084,7 @@ static
|
|||||||
Response
|
Response
|
||||||
handleGetIndexesRequest(RequestContext& context)
|
handleGetIndexesRequest(RequestContext& context)
|
||||||
{
|
{
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
|
|
||||||
Response::Node& artistsNode {response.createNode("indexes")};
|
Response::Node& artistsNode {response.createNode("indexes")};
|
||||||
artistsNode.setAttribute("ignoredArticles", "");
|
artistsNode.setAttribute("ignoredArticles", "");
|
||||||
@@ -1216,7 +1167,7 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
|
|||||||
|
|
||||||
Random::shuffleContainer(tracks);
|
Random::shuffleContainer(tracks);
|
||||||
|
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
Response::Node& similarSongsNode {response.createNode(id3 ? "similarSongs2" : "similarSongs")};
|
Response::Node& similarSongsNode {response.createNode(id3 ? "similarSongs2" : "similarSongs")};
|
||||||
for (const Track::pointer& track : tracks)
|
for (const Track::pointer& track : tracks)
|
||||||
similarSongsNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
|
similarSongsNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
|
||||||
@@ -1248,7 +1199,7 @@ handleGetStarredRequestCommon(RequestContext& context, bool id3)
|
|||||||
if (!user)
|
if (!user)
|
||||||
throw UserNotAuthorizedError {};
|
throw UserNotAuthorizedError {};
|
||||||
|
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
Response::Node& starredNode {response.createNode(id3 ? "starred2" : "starred")};
|
Response::Node& starredNode {response.createNode(id3 ? "starred2" : "starred")};
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -1324,7 +1275,7 @@ handleGetPlaylistRequest(RequestContext& context)
|
|||||||
if (!tracklist)
|
if (!tracklist)
|
||||||
throw RequestedDataNotFoundError {};
|
throw RequestedDataNotFoundError {};
|
||||||
|
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
Response::Node playlistNode {tracklistToResponseNode(tracklist, context.dbSession)};
|
Response::Node playlistNode {tracklistToResponseNode(tracklist, context.dbSession)};
|
||||||
|
|
||||||
auto entries {tracklist->getEntries()};
|
auto entries {tracklist->getEntries()};
|
||||||
@@ -1346,7 +1297,7 @@ handleGetPlaylistsRequest(RequestContext& context)
|
|||||||
if (!user)
|
if (!user)
|
||||||
throw UserNotAuthorizedError {};
|
throw UserNotAuthorizedError {};
|
||||||
|
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
Response::Node& playlistsNode {response.createNode("playlists")};
|
Response::Node& playlistsNode {response.createNode("playlists")};
|
||||||
|
|
||||||
auto tracklists {TrackList::getAll(context.dbSession, user, TrackList::Type::Playlist)};
|
auto tracklists {TrackList::getAll(context.dbSession, user, TrackList::Type::Playlist)};
|
||||||
@@ -1383,7 +1334,7 @@ handleGetSongsByGenreRequest(RequestContext& context)
|
|||||||
if (!user)
|
if (!user)
|
||||||
throw UserNotAuthorizedError {};
|
throw UserNotAuthorizedError {};
|
||||||
|
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
Response::Node& songsByGenreNode {response.createNode("songsByGenre")};
|
Response::Node& songsByGenreNode {response.createNode("songsByGenre")};
|
||||||
|
|
||||||
bool more;
|
bool more;
|
||||||
@@ -1408,7 +1359,7 @@ handleGetUserRequest(RequestContext& context)
|
|||||||
if (!user)
|
if (!user)
|
||||||
throw RequestedDataNotFoundError {};
|
throw RequestedDataNotFoundError {};
|
||||||
|
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
response.addNode("user", userToResponseNode(user));
|
response.addNode("user", userToResponseNode(user));
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@@ -1420,7 +1371,7 @@ handleGetUsersRequest(RequestContext& context)
|
|||||||
{
|
{
|
||||||
auto transaction {context.dbSession.createSharedTransaction()};
|
auto transaction {context.dbSession.createSharedTransaction()};
|
||||||
|
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
Response::Node& usersNode {response.createNode("users")};
|
Response::Node& usersNode {response.createNode("users")};
|
||||||
|
|
||||||
const auto users {User::getAll(context.dbSession)};
|
const auto users {User::getAll(context.dbSession)};
|
||||||
@@ -1453,7 +1404,7 @@ handleSearchRequestCommon(RequestContext& context, bool id3)
|
|||||||
if (!user)
|
if (!user)
|
||||||
throw UserNotAuthorizedError {};
|
throw UserNotAuthorizedError {};
|
||||||
|
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
Response::Node& searchResult2Node {response.createNode(id3 ? "searchResult3" : "searchResult2")};
|
Response::Node& searchResult2Node {response.createNode(id3 ? "searchResult3" : "searchResult2")};
|
||||||
|
|
||||||
bool more;
|
bool more;
|
||||||
@@ -1538,7 +1489,7 @@ handleStarRequest(RequestContext& context)
|
|||||||
user.modify()->starTrack(track);
|
user.modify()->starTrack(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response::createOkResponse(context);
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
@@ -1595,7 +1546,7 @@ handleUnstarRequest(RequestContext& context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return Response::createOkResponse(context);
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
@@ -1639,7 +1590,7 @@ handleScrobble(RequestContext& context)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response::createOkResponse(context);
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
@@ -1682,7 +1633,7 @@ handleUpdateUserRequest(RequestContext& context)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response::createOkResponse(context);
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
@@ -1741,7 +1692,7 @@ handleUpdatePlaylistRequest(RequestContext& context)
|
|||||||
TrackListEntry::create(context.dbSession, track, tracklist);
|
TrackListEntry::create(context.dbSession, track, tracklist);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response::createOkResponse(context);
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
@@ -1756,7 +1707,7 @@ handleGetBookmarks(RequestContext& context)
|
|||||||
|
|
||||||
const auto bookmarks {TrackBookmark::getByUser(context.dbSession, user)};
|
const auto bookmarks {TrackBookmark::getByUser(context.dbSession, user)};
|
||||||
|
|
||||||
Response response {Response::createOkResponse(context)};
|
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||||
Response::Node& bookmarksNode {response.createNode("bookmarks")};
|
Response::Node& bookmarksNode {response.createNode("bookmarks")};
|
||||||
|
|
||||||
for (const TrackBookmark::pointer& bookmark : bookmarks)
|
for (const TrackBookmark::pointer& bookmark : bookmarks)
|
||||||
@@ -1798,7 +1749,7 @@ handleCreateBookmark(RequestContext& context)
|
|||||||
if (comment)
|
if (comment)
|
||||||
bookmark.modify()->setComment(*comment);
|
bookmark.modify()->setComment(*comment);
|
||||||
|
|
||||||
return Response::createOkResponse(context);
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
@@ -1824,7 +1775,7 @@ handleDeleteBookmark(RequestContext& context)
|
|||||||
|
|
||||||
bookmark.remove();
|
bookmark.remove();
|
||||||
|
|
||||||
return Response::createOkResponse(context);
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
@@ -1867,7 +1818,7 @@ struct RequestEntryPointInfo
|
|||||||
CheckImplementedFunc checkFunc {};
|
CheckImplementedFunc checkFunc {};
|
||||||
};
|
};
|
||||||
|
|
||||||
static std::unordered_map<std::string, RequestEntryPointInfo> requestEntryPoints
|
static const std::unordered_map<std::string, RequestEntryPointInfo> requestEntryPoints
|
||||||
{
|
{
|
||||||
// System
|
// System
|
||||||
{"ping", {handlePingRequest}},
|
{"ping", {handlePingRequest}},
|
||||||
@@ -1981,38 +1932,6 @@ static std::unordered_map<std::string, MediaRetrievalHandlerFunc> mediaRetrieval
|
|||||||
{"getCoverArt", handleGetCoverArt},
|
{"getCoverArt", handleGetCoverArt},
|
||||||
};
|
};
|
||||||
|
|
||||||
static
|
|
||||||
Database::UserId
|
|
||||||
authenticateUser(const Wt::Http::Request &request, const ClientInfo& clientInfo, Session& dbSession)
|
|
||||||
{
|
|
||||||
if (auto *authEnvService {Service<::Auth::IEnvService>::get()})
|
|
||||||
{
|
|
||||||
const auto checkResult {authEnvService->processRequest(dbSession, request)};
|
|
||||||
if (checkResult.state != ::Auth::IEnvService::CheckResult::State::Granted)
|
|
||||||
throw UserNotAuthorizedError {};
|
|
||||||
|
|
||||||
return *checkResult.userId;
|
|
||||||
}
|
|
||||||
else if (auto *authPasswordService {Service<::Auth::IPasswordService>::get()})
|
|
||||||
{
|
|
||||||
const auto checkResult {authPasswordService->checkUserPassword(dbSession,
|
|
||||||
boost::asio::ip::address::from_string(request.clientAddress()),
|
|
||||||
clientInfo.user, clientInfo.password)};
|
|
||||||
|
|
||||||
switch (checkResult.state)
|
|
||||||
{
|
|
||||||
case Auth::IPasswordService::CheckResult::State::Granted:
|
|
||||||
return *checkResult.userId;
|
|
||||||
break;
|
|
||||||
case Auth::IPasswordService::CheckResult::State::Denied:
|
|
||||||
throw WrongUsernameOrPasswordError {};
|
|
||||||
case Auth::IPasswordService::CheckResult::State::Throttled:
|
|
||||||
throw LoginThrottledGenericError {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw InternalErrorGenericError {"No service avalaible to authenticate user"};
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response)
|
SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response)
|
||||||
@@ -2027,24 +1946,16 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp
|
|||||||
if (StringUtils::stringEndsWith(requestPath, ".view"))
|
if (StringUtils::stringEndsWith(requestPath, ".view"))
|
||||||
requestPath.resize(requestPath.length() - 5);
|
requestPath.resize(requestPath.length() - 5);
|
||||||
|
|
||||||
const Wt::Http::ParameterMap& parameters {request.getParameterMap()};
|
|
||||||
|
|
||||||
// Optional parameters
|
// Optional parameters
|
||||||
const ResponseFormat format {getParameterAs<std::string>(parameters, "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml};
|
const ResponseFormat format {getParameterAs<std::string>(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml};
|
||||||
|
|
||||||
std::string clientName;
|
ProtocolVersion protocolVersion {defaultServerProtocolVersion};
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Mandatory parameters
|
// We need to parse client a soon as possible to make sure to answer with the right protocol version
|
||||||
const ClientInfo clientInfo {getClientInfo(parameters)};
|
protocolVersion = getServerProtocolVersion(getMandatoryParameterAs<std::string>(request.getParameterMap(), "c"));
|
||||||
|
RequestContext requestContext {buildRequestContext(request)};
|
||||||
clientName = clientInfo.name;
|
|
||||||
|
|
||||||
Session& dbSession {_db.getTLSSession()};
|
|
||||||
|
|
||||||
const Database::UserId userId {authenticateUser(request, clientInfo, dbSession)};
|
|
||||||
RequestContext requestContext {parameters, dbSession, userId, clientInfo.name};
|
|
||||||
|
|
||||||
auto itEntryPoint {requestEntryPoints.find(requestPath)};
|
auto itEntryPoint {requestEntryPoints.find(requestPath)};
|
||||||
if (itEntryPoint != requestEntryPoints.end())
|
if (itEntryPoint != requestEntryPoints.end())
|
||||||
@@ -2079,11 +1990,97 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp
|
|||||||
LMS_LOG(API_SUBSONIC, ERROR) << "Error while processing request '" << requestPath << "'"
|
LMS_LOG(API_SUBSONIC, ERROR) << "Error while processing request '" << requestPath << "'"
|
||||||
<< ", params = [" << parameterMapToDebugString(request.getParameterMap()) << "]"
|
<< ", params = [" << parameterMapToDebugString(request.getParameterMap()) << "]"
|
||||||
<< ", code = " << static_cast<int>(e.getCode()) << ", msg = '" << e.getMessage() << "'";
|
<< ", code = " << static_cast<int>(e.getCode()) << ", msg = '" << e.getMessage() << "'";
|
||||||
Response resp {Response::createFailedResponse(clientName, e)};
|
Response resp {Response::createFailedResponse(protocolVersion, e)};
|
||||||
resp.write(response.out(), format);
|
resp.write(response.out(), format);
|
||||||
response.setMimeType(ResponseFormatToMimeType(format));
|
response.setMimeType(ResponseFormatToMimeType(format));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ProtocolVersion
|
||||||
|
SubsonicResource::getServerProtocolVersion(const std::string& clientName) const
|
||||||
|
{
|
||||||
|
auto it {_serverProtocolVersionsByClient.find(clientName)};
|
||||||
|
if (it == std::cend(_serverProtocolVersionsByClient))
|
||||||
|
return defaultServerProtocolVersion;
|
||||||
|
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SubsonicResource::checkProtocolVersion(ProtocolVersion client, ProtocolVersion server)
|
||||||
|
{
|
||||||
|
if (client.major > server.major)
|
||||||
|
throw ServerMustUpgradeError {};
|
||||||
|
if (client.major < server.major)
|
||||||
|
throw ClientMustUpgradeError {};
|
||||||
|
if (client.minor > server.minor)
|
||||||
|
throw ServerMustUpgradeError {};
|
||||||
|
else if (client.minor == server.minor)
|
||||||
|
{
|
||||||
|
if (client.patch > server.patch)
|
||||||
|
throw ServerMustUpgradeError {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientInfo
|
||||||
|
SubsonicResource::getClientInfo(const Wt::Http::ParameterMap& parameters)
|
||||||
|
{
|
||||||
|
ClientInfo res;
|
||||||
|
|
||||||
|
// Mandatory parameters
|
||||||
|
res.name = getMandatoryParameterAs<std::string>(parameters, "c");
|
||||||
|
res.version = getMandatoryParameterAs<ProtocolVersion>(parameters, "v");
|
||||||
|
res.user = getMandatoryParameterAs<std::string>(parameters, "u");
|
||||||
|
res.password = decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(parameters, "p"));
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestContext
|
||||||
|
SubsonicResource::buildRequestContext(const Wt::Http::Request& request)
|
||||||
|
{
|
||||||
|
const Wt::Http::ParameterMap& parameters {request.getParameterMap()};
|
||||||
|
|
||||||
|
const ClientInfo clientInfo {getClientInfo(parameters)};
|
||||||
|
|
||||||
|
Session& dbSession {_db.getTLSSession()};
|
||||||
|
|
||||||
|
const Database::UserId userId {authenticateUser(request, clientInfo, dbSession)};
|
||||||
|
|
||||||
|
return {parameters, dbSession, userId, clientInfo, getServerProtocolVersion(clientInfo.name)};
|
||||||
|
}
|
||||||
|
|
||||||
|
Database::UserId
|
||||||
|
SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo, Session& dbSession)
|
||||||
|
{
|
||||||
|
if (auto *authEnvService {Service<::Auth::IEnvService>::get()})
|
||||||
|
{
|
||||||
|
const auto checkResult {authEnvService->processRequest(dbSession, request)};
|
||||||
|
if (checkResult.state != ::Auth::IEnvService::CheckResult::State::Granted)
|
||||||
|
throw UserNotAuthorizedError {};
|
||||||
|
|
||||||
|
return *checkResult.userId;
|
||||||
|
}
|
||||||
|
else if (auto *authPasswordService {Service<::Auth::IPasswordService>::get()})
|
||||||
|
{
|
||||||
|
const auto checkResult {authPasswordService->checkUserPassword(dbSession,
|
||||||
|
boost::asio::ip::address::from_string(request.clientAddress()),
|
||||||
|
clientInfo.user, clientInfo.password)};
|
||||||
|
|
||||||
|
switch (checkResult.state)
|
||||||
|
{
|
||||||
|
case Auth::IPasswordService::CheckResult::State::Granted:
|
||||||
|
return *checkResult.userId;
|
||||||
|
break;
|
||||||
|
case Auth::IPasswordService::CheckResult::State::Denied:
|
||||||
|
throw WrongUsernameOrPasswordError {};
|
||||||
|
case Auth::IPasswordService::CheckResult::State::Throttled:
|
||||||
|
throw LoginThrottledGenericError {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw InternalErrorGenericError {"No service avalaible to authenticate user"};
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace api::subsonic
|
} // namespace api::subsonic
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019 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 <string>
|
||||||
|
#include <Wt/WResource.h>
|
||||||
|
#include <Wt/Http/Response.h>
|
||||||
|
|
||||||
|
#include "database/Types.hpp"
|
||||||
|
#include "ClientInfo.hpp"
|
||||||
|
#include "RequestContext.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class Db;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
|
||||||
|
class SubsonicResource final : public Wt::WResource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SubsonicResource(Database::Db& db);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) override;
|
||||||
|
ProtocolVersion getServerProtocolVersion(const std::string& clientName) const;
|
||||||
|
|
||||||
|
static void checkProtocolVersion(ProtocolVersion client, ProtocolVersion server);
|
||||||
|
ClientInfo getClientInfo(const Wt::Http::ParameterMap& parameters);
|
||||||
|
RequestContext buildRequestContext(const Wt::Http::Request& request);
|
||||||
|
Database::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo, Database::Session& dbSession);
|
||||||
|
|
||||||
|
const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient;
|
||||||
|
Database::Db& _db;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
|
|
||||||
#include "utils/Exception.hpp"
|
#include "utils/Exception.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
|
#include "ProtocolVersion.hpp"
|
||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
@@ -102,26 +103,32 @@ Response::Node::createArrayChild(const std::string& key)
|
|||||||
return _childrenArrays[key].back();
|
return _childrenArrays[key].back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Response::Node::setVersionAttribute(ProtocolVersion protocolVersion)
|
||||||
|
{
|
||||||
|
setAttribute("version", std::to_string(protocolVersion.major) + "." + std::to_string(protocolVersion.minor) + "." + std::to_string(protocolVersion.patch));
|
||||||
|
}
|
||||||
|
|
||||||
Response
|
Response
|
||||||
Response::createOkResponse(const RequestContext& context)
|
Response::createOkResponse(ProtocolVersion protocolVersion)
|
||||||
{
|
{
|
||||||
Response response;
|
Response response;
|
||||||
Node& responseNode {response._root.createChild("subsonic-response")};
|
Node& responseNode {response._root.createChild("subsonic-response")};
|
||||||
|
|
||||||
responseNode.setAttribute("status", "ok");
|
responseNode.setAttribute("status", "ok");
|
||||||
responseNode.setAttribute("version", std::string {QUOTEME(API_VERSION_MAJOR) "."} + std::to_string(getAPIMinorVersion(context.clientName)) + ".0");
|
responseNode.setVersionAttribute(protocolVersion);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
Response
|
Response
|
||||||
Response::createFailedResponse(std::string_view clientName, const Error& error)
|
Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& error)
|
||||||
{
|
{
|
||||||
Response response;
|
Response response;
|
||||||
Node& responseNode {response._root.createChild("subsonic-response")};
|
Node& responseNode {response._root.createChild("subsonic-response")};
|
||||||
|
|
||||||
responseNode.setAttribute("status", "failed");
|
responseNode.setAttribute("status", "failed");
|
||||||
responseNode.setAttribute("version", std::string {QUOTEME(API_VERSION_MAJOR) "."} + std::to_string(getAPIMinorVersion(clientName)) + ".0");
|
responseNode.setVersionAttribute(protocolVersion);
|
||||||
|
|
||||||
Node& errorNode {responseNode.createChild("error")};
|
Node& errorNode {responseNode.createChild("error")};
|
||||||
errorNode.setAttribute("code", std::to_string(static_cast<int>(error.getCode())));
|
errorNode.setAttribute("code", std::to_string(static_cast<int>(error.getCode())));
|
||||||
@@ -214,18 +221,6 @@ Response::writeXML(std::ostream& os)
|
|||||||
boost::property_tree::write_xml(os, root);
|
boost::property_tree::write_xml(os, root);
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned
|
|
||||||
Response::getAPIMinorVersion(std::string_view clientName)
|
|
||||||
{
|
|
||||||
// Some clients do not rely on version to enable the clear text password auth scheme
|
|
||||||
if (clientName == "Audinaut")
|
|
||||||
return 16;
|
|
||||||
else if (clientName == "Sublime Music")
|
|
||||||
return 16;
|
|
||||||
else
|
|
||||||
return 12;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
Response::writeJSON(std::ostream& os)
|
Response::writeJSON(std::ostream& os)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,8 +27,6 @@
|
|||||||
|
|
||||||
#include "RequestContext.hpp"
|
#include "RequestContext.hpp"
|
||||||
|
|
||||||
#define API_VERSION_MAJOR 1
|
|
||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -205,6 +203,9 @@ class Response
|
|||||||
void addArrayChild(const std::string& key, Node node);
|
void addArrayChild(const std::string& key, Node node);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
|
void setVersionAttribute(ProtocolVersion version);
|
||||||
|
|
||||||
friend class Response;
|
friend class Response;
|
||||||
using Value = std::variant<std::string, bool, long long>;
|
using Value = std::variant<std::string, bool, long long>;
|
||||||
std::map<std::string, Value> _attributes;
|
std::map<std::string, Value> _attributes;
|
||||||
@@ -213,8 +214,8 @@ class Response
|
|||||||
std::map<std::string, std::vector<Node>> _childrenArrays;
|
std::map<std::string, std::vector<Node>> _childrenArrays;
|
||||||
};
|
};
|
||||||
|
|
||||||
static Response createOkResponse(const RequestContext& context);
|
static Response createOkResponse(ProtocolVersion protocolVersion);
|
||||||
static Response createFailedResponse(std::string_view clientName, const Error& error);
|
static Response createFailedResponse(ProtocolVersion protocolVersion, const Error& error);
|
||||||
|
|
||||||
virtual ~Response() {}
|
virtual ~Response() {}
|
||||||
Response(const Response&) = delete;
|
Response(const Response&) = delete;
|
||||||
@@ -228,9 +229,7 @@ class Response
|
|||||||
|
|
||||||
void write(std::ostream& os, ResponseFormat format);
|
void write(std::ostream& os, ResponseFormat format);
|
||||||
|
|
||||||
static unsigned getAPIMinorVersion(std::string_view clientName);
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
void writeJSON(std::ostream& os);
|
void writeJSON(std::ostream& os);
|
||||||
void writeXML(std::ostream& os);
|
void writeXML(std::ostream& os);
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,9 @@
|
|||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include <Wt/WResource.h>
|
#include <Wt/WResource.h>
|
||||||
#include <Wt/Http/Response.h>
|
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
@@ -28,18 +29,5 @@ namespace Database
|
|||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
|
std::unique_ptr<Wt::WResource> createSubsonicResource(Database::Db& db);
|
||||||
class SubsonicResource final : public Wt::WResource
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
SubsonicResource(Database::Db& db);
|
|
||||||
|
|
||||||
static std::string getPath() { return "rest/"; }
|
|
||||||
private:
|
|
||||||
|
|
||||||
void handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) override;
|
|
||||||
|
|
||||||
Database::Db& _db;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ Config::Config(const std::filesystem::path& p)
|
|||||||
std::string_view
|
std::string_view
|
||||||
Config::getString(std::string_view setting, std::string_view def)
|
Config::getString(std::string_view setting, std::string_view def)
|
||||||
{
|
{
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
return static_cast<const char*>(_config.lookup(std::string {setting}));
|
return static_cast<const char*>(_config.lookup(std::string {setting}));
|
||||||
}
|
}
|
||||||
catch (libconfig::ConfigException&)
|
catch (libconfig::ConfigException&)
|
||||||
@@ -59,10 +60,30 @@ Config::getString(std::string_view setting, std::string_view def)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Config::visitStrings(std::string_view setting, std::function<void(std::string_view)> _func, std::initializer_list<std::string_view> defs)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const libconfig::Setting& values {_config.lookup(std::string {setting})};
|
||||||
|
for (int i {}; i < values.getLength(); ++i)
|
||||||
|
_func(static_cast<const char*>(values[i]));
|
||||||
|
}
|
||||||
|
catch (const libconfig::SettingNotFoundException&)
|
||||||
|
{
|
||||||
|
for (std::string_view def : defs)
|
||||||
|
_func(def);
|
||||||
|
}
|
||||||
|
catch (libconfig::ConfigException&)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::filesystem::path
|
std::filesystem::path
|
||||||
Config::getPath(std::string_view setting, const std::filesystem::path& path)
|
Config::getPath(std::string_view setting, const std::filesystem::path& path)
|
||||||
{
|
{
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
const char* res {_config.lookup(std::string {setting})};
|
const char* res {_config.lookup(std::string {setting})};
|
||||||
return std::filesystem::path {std::string(res)};
|
return std::filesystem::path {std::string(res)};
|
||||||
}
|
}
|
||||||
@@ -75,7 +96,8 @@ Config::getPath(std::string_view setting, const std::filesystem::path& path)
|
|||||||
unsigned long
|
unsigned long
|
||||||
Config::getULong(std::string_view setting, unsigned long def)
|
Config::getULong(std::string_view setting, unsigned long def)
|
||||||
{
|
{
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
return static_cast<unsigned int>(_config.lookup(std::string {setting}));
|
return static_cast<unsigned int>(_config.lookup(std::string {setting}));
|
||||||
}
|
}
|
||||||
catch (libconfig::ConfigException&)
|
catch (libconfig::ConfigException&)
|
||||||
@@ -87,7 +109,8 @@ Config::getULong(std::string_view setting, unsigned long def)
|
|||||||
long
|
long
|
||||||
Config::getLong(std::string_view setting, long def)
|
Config::getLong(std::string_view setting, long def)
|
||||||
{
|
{
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
return _config.lookup(std::string {setting});
|
return _config.lookup(std::string {setting});
|
||||||
}
|
}
|
||||||
catch (libconfig::ConfigException&)
|
catch (libconfig::ConfigException&)
|
||||||
@@ -99,7 +122,8 @@ Config::getLong(std::string_view setting, long def)
|
|||||||
bool
|
bool
|
||||||
Config::getBool(std::string_view setting, bool def)
|
Config::getBool(std::string_view setting, bool def)
|
||||||
{
|
{
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
return _config.lookup(std::string {setting});
|
return _config.lookup(std::string {setting});
|
||||||
}
|
}
|
||||||
catch (libconfig::ConfigException&)
|
catch (libconfig::ConfigException&)
|
||||||
@@ -108,4 +132,3 @@ Config::getBool(std::string_view setting, bool def)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class Config final : public IConfig
|
|||||||
|
|
||||||
// Default values are returned in case of setting not found
|
// Default values are returned in case of setting not found
|
||||||
std::string_view getString(std::string_view setting, std::string_view def = "") override;
|
std::string_view getString(std::string_view setting, std::string_view def = "") override;
|
||||||
|
void visitStrings(std::string_view setting, std::function<void(std::string_view)> _func, std::initializer_list<std::string_view> defs) override;
|
||||||
std::filesystem::path getPath(std::string_view setting, const std::filesystem::path& def = std::filesystem::path()) override;
|
std::filesystem::path getPath(std::string_view setting, const std::filesystem::path& def = std::filesystem::path()) override;
|
||||||
unsigned long getULong(std::string_view setting, unsigned long def = 0) override;
|
unsigned long getULong(std::string_view setting, unsigned long def = 0) override;
|
||||||
long getLong(std::string_view setting, long def = 0) override;
|
long getLong(std::string_view setting, long def = 0) override;
|
||||||
|
|||||||
@@ -18,8 +18,9 @@
|
|||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string_view>
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <functional>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
// Used to get config values from configuration files
|
// Used to get config values from configuration files
|
||||||
class IConfig
|
class IConfig
|
||||||
@@ -30,10 +31,11 @@ class IConfig
|
|||||||
|
|
||||||
// Default values are returned in case of setting not found
|
// Default values are returned in case of setting not found
|
||||||
virtual std::string_view getString(std::string_view setting, std::string_view def = "") = 0;
|
virtual std::string_view getString(std::string_view setting, std::string_view def = "") = 0;
|
||||||
|
virtual void visitStrings(std::string_view setting, std::function<void(std::string_view)> _func, std::initializer_list<std::string_view> def = {}) = 0;
|
||||||
virtual std::filesystem::path getPath(std::string_view setting, const std::filesystem::path& def = std::filesystem::path()) = 0;
|
virtual std::filesystem::path getPath(std::string_view setting, const std::filesystem::path& def = std::filesystem::path()) = 0;
|
||||||
virtual unsigned long getULong(std::string_view setting, unsigned long def = 0) = 0;
|
virtual unsigned long getULong(std::string_view setting, unsigned long def = 0) = 0;
|
||||||
virtual long getLong(std::string_view setting, long def = 0) = 0;
|
virtual long getLong(std::string_view setting, long def = 0) = 0;
|
||||||
virtual bool getBool(std::string_view setting, bool def = false) = 0;
|
virtual bool getBool(std::string_view setting, bool def = false) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -267,11 +267,14 @@ int main(int argc, char* argv[])
|
|||||||
|
|
||||||
Service<Scrobbling::IScrobbling> scrobblingService {Scrobbling::createScrobbling(ioContext, database)};
|
Service<Scrobbling::IScrobbling> scrobblingService {Scrobbling::createScrobbling(ioContext, database)};
|
||||||
|
|
||||||
API::Subsonic::SubsonicResource subsonicResource {database};
|
std::unique_ptr<Wt::WResource> subsonicResource;
|
||||||
|
|
||||||
// bind API resources
|
// bind API resources
|
||||||
if (config->getBool("api-subsonic", true))
|
if (config->getBool("api-subsonic", true))
|
||||||
server.addResource(&subsonicResource, subsonicResource.getPath());
|
{
|
||||||
|
subsonicResource = API::Subsonic::createSubsonicResource(database);
|
||||||
|
server.addResource(subsonicResource.get(), "rest/");
|
||||||
|
}
|
||||||
|
|
||||||
// bind UI entry point
|
// bind UI entry point
|
||||||
server.addEntryPoint(Wt::EntryPointType::Application,
|
server.addEntryPoint(Wt::EntryPointType::Application,
|
||||||
|
|||||||
Reference in New Issue
Block a user