Changed coding style
This commit is contained in:
@@ -24,11 +24,11 @@
|
|||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
struct ClientInfo
|
struct ClientInfo
|
||||||
{
|
{
|
||||||
std::string name;
|
std::string name;
|
||||||
std::string user;
|
std::string user;
|
||||||
std::string password;
|
std::string password;
|
||||||
ProtocolVersion version;
|
ProtocolVersion version;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,57 +32,57 @@
|
|||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
std::vector<T> getMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& paramName)
|
std::vector<T> getMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& paramName)
|
||||||
{
|
{
|
||||||
std::vector<T> res;
|
std::vector<T> res;
|
||||||
|
|
||||||
auto it = parameterMap.find(paramName);
|
auto it = parameterMap.find(paramName);
|
||||||
if (it == parameterMap.end())
|
if (it == parameterMap.end())
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
for (const std::string& param : it->second)
|
for (const std::string& param : it->second)
|
||||||
{
|
{
|
||||||
auto value{ StringUtils::readAs<T>(param) };
|
auto value{ StringUtils::readAs<T>(param) };
|
||||||
if (value)
|
if (value)
|
||||||
res.emplace_back(std::move(*value));
|
res.emplace_back(std::move(*value));
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
std::vector<T> getMandatoryMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
std::vector<T> getMandatoryMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||||
{
|
{
|
||||||
std::vector<T> res{ getMultiParametersAs<T>(parameterMap, param) };
|
std::vector<T> res{ getMultiParametersAs<T>(parameterMap, param) };
|
||||||
if (res.empty())
|
if (res.empty())
|
||||||
throw RequiredParameterMissingError{ param };
|
throw RequiredParameterMissingError{ param };
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
std::optional<T> getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
std::optional<T> getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||||
{
|
{
|
||||||
std::vector<T> params{ getMultiParametersAs<T>(parameterMap, param) };
|
std::vector<T> params{ getMultiParametersAs<T>(parameterMap, param) };
|
||||||
|
|
||||||
if (params.size() != 1)
|
if (params.size() != 1)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
return T{ std::move(params.front()) };
|
return T{ std::move(params.front()) };
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
T getMandatoryParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
T getMandatoryParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||||
{
|
{
|
||||||
auto res{ getParameterAs<T>(parameterMap, param) };
|
auto res{ getParameterAs<T>(parameterMap, param) };
|
||||||
if (!res)
|
if (!res)
|
||||||
throw RequiredParameterMissingError{ param };
|
throw RequiredParameterMissingError{ param };
|
||||||
|
|
||||||
return *res;
|
return *res;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasParameter(const Wt::Http::ParameterMap& parameterMap, const std::string& param);
|
bool hasParameter(const Wt::Http::ParameterMap& parameterMap, const std::string& param);
|
||||||
std::string decodePasswordIfNeeded(const std::string& password);
|
std::string decodePasswordIfNeeded(const std::string& password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,36 +21,35 @@
|
|||||||
|
|
||||||
namespace StringUtils
|
namespace StringUtils
|
||||||
{
|
{
|
||||||
template<>
|
template<>
|
||||||
std::optional<API::Subsonic::ProtocolVersion>
|
std::optional<API::Subsonic::ProtocolVersion> readAs(std::string_view str)
|
||||||
readAs(std::string_view str)
|
{
|
||||||
{
|
// Expects "X.Y.Z"
|
||||||
// Expects "X.Y.Z"
|
const auto numbers{ StringUtils::splitString(str, ".") };
|
||||||
const auto numbers {StringUtils::splitString(str, ".")};
|
if (numbers.size() < 2 || numbers.size() > 3)
|
||||||
if (numbers.size() < 2 || numbers.size() > 3)
|
return std::nullopt;
|
||||||
return std::nullopt;
|
|
||||||
|
|
||||||
API::Subsonic::ProtocolVersion version;
|
API::Subsonic::ProtocolVersion version;
|
||||||
|
|
||||||
auto number {StringUtils::readAs<unsigned>(numbers[0])};
|
auto number{ StringUtils::readAs<unsigned>(numbers[0]) };
|
||||||
if (!number)
|
if (!number)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
version.major = *number;
|
version.major = *number;
|
||||||
|
|
||||||
number = {StringUtils::readAs<unsigned>(numbers[1])};
|
number = { StringUtils::readAs<unsigned>(numbers[1]) };
|
||||||
if (!number)
|
if (!number)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
version.minor = *number;
|
version.minor = *number;
|
||||||
|
|
||||||
if (numbers.size() == 3)
|
if (numbers.size() == 3)
|
||||||
{
|
{
|
||||||
number = {StringUtils::readAs<unsigned>(numbers[2])};
|
number = { StringUtils::readAs<unsigned>(numbers[2]) };
|
||||||
if (!number)
|
if (!number)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
version.patch = *number;
|
version.patch = *number;
|
||||||
}
|
}
|
||||||
|
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,18 +23,19 @@
|
|||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
struct ProtocolVersion
|
struct ProtocolVersion
|
||||||
{
|
{
|
||||||
unsigned major {};
|
unsigned major{};
|
||||||
unsigned minor {};
|
unsigned minor{};
|
||||||
unsigned patch {};
|
unsigned patch{};
|
||||||
};
|
};
|
||||||
|
|
||||||
static inline constexpr ProtocolVersion defaultServerProtocolVersion {1, 16, 0};
|
static inline constexpr ProtocolVersion defaultServerProtocolVersion{ 1, 16, 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace StringUtils
|
namespace StringUtils
|
||||||
{
|
{
|
||||||
template<> std::optional<API::Subsonic::ProtocolVersion> readAs(std::string_view str);
|
template<>
|
||||||
|
std::optional<API::Subsonic::ProtocolVersion> readAs(std::string_view str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,18 +29,18 @@
|
|||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
class Session;
|
class Session;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
struct RequestContext
|
struct RequestContext
|
||||||
{
|
{
|
||||||
const Wt::Http::ParameterMap& parameters;
|
const Wt::Http::ParameterMap& parameters;
|
||||||
Database::Session& dbSession;
|
Database::Session& dbSession;
|
||||||
Database::UserId userId;
|
Database::UserId userId;
|
||||||
ClientInfo clientInfo;
|
ClientInfo clientInfo;
|
||||||
ProtocolVersion serverProtocolVersion;
|
ProtocolVersion serverProtocolVersion;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,114 +26,104 @@
|
|||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
std::string
|
std::string idToString(Database::ArtistId id)
|
||||||
idToString(Database::ArtistId id)
|
{
|
||||||
{
|
return "ar-" + id.toString();
|
||||||
return "ar-" + id.toString();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
std::string
|
std::string idToString(Database::ReleaseId id)
|
||||||
idToString(Database::ReleaseId id)
|
{
|
||||||
{
|
return "al-" + id.toString();
|
||||||
return "al-" + id.toString();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
std::string
|
std::string idToString(RootId)
|
||||||
idToString(RootId)
|
{
|
||||||
{
|
return "root";
|
||||||
return "root";
|
}
|
||||||
}
|
|
||||||
|
|
||||||
std::string
|
std::string idToString(Database::TrackId id)
|
||||||
idToString(Database::TrackId id)
|
{
|
||||||
{
|
return "tr-" + id.toString();
|
||||||
return "tr-" + id.toString();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
std::string
|
std::string idToString(Database::TrackListId id)
|
||||||
idToString(Database::TrackListId id)
|
{
|
||||||
{
|
return "pl-" + id.toString();
|
||||||
return "pl-" + id.toString();
|
}
|
||||||
}
|
|
||||||
} // namespace API::Subsonic
|
} // namespace API::Subsonic
|
||||||
|
|
||||||
namespace StringUtils
|
namespace StringUtils
|
||||||
{
|
{
|
||||||
template<>
|
template<>
|
||||||
std::optional<Database::ArtistId>
|
std::optional<Database::ArtistId> readAs(std::string_view str)
|
||||||
readAs(std::string_view str)
|
{
|
||||||
{
|
std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
|
||||||
std::vector<std::string_view> values {StringUtils::splitString(str, "-")};
|
if (values.size() != 2)
|
||||||
if (values.size() != 2)
|
return std::nullopt;
|
||||||
return std::nullopt;
|
|
||||||
|
|
||||||
if (values[0] != "ar")
|
if (values[0] != "ar")
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
if (const auto value {StringUtils::readAs<Database::ArtistId::ValueType>(values[1])})
|
if (const auto value{ StringUtils::readAs<Database::ArtistId::ValueType>(values[1]) })
|
||||||
return Database::ArtistId {*value};
|
return Database::ArtistId{ *value };
|
||||||
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<Database::ReleaseId>
|
std::optional<Database::ReleaseId> readAs(std::string_view str)
|
||||||
readAs(std::string_view str)
|
{
|
||||||
{
|
std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
|
||||||
std::vector<std::string_view> values {StringUtils::splitString(str, "-")};
|
if (values.size() != 2)
|
||||||
if (values.size() != 2)
|
return std::nullopt;
|
||||||
return std::nullopt;
|
|
||||||
|
|
||||||
if (values[0] != "al")
|
if (values[0] != "al")
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
if (const auto value {StringUtils::readAs<Database::ReleaseId::ValueType>(values[1])})
|
if (const auto value{ StringUtils::readAs<Database::ReleaseId::ValueType>(values[1]) })
|
||||||
return Database::ReleaseId {*value};
|
return Database::ReleaseId{ *value };
|
||||||
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<API::Subsonic::RootId>
|
std::optional<API::Subsonic::RootId> readAs(std::string_view str)
|
||||||
readAs(std::string_view str)
|
{
|
||||||
{
|
if (str == "root")
|
||||||
if (str == "root")
|
return API::Subsonic::RootId{};
|
||||||
return API::Subsonic::RootId {};
|
|
||||||
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<Database::TrackId>
|
std::optional<Database::TrackId> readAs(std::string_view str)
|
||||||
readAs(std::string_view str)
|
{
|
||||||
{
|
std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
|
||||||
std::vector<std::string_view> values {StringUtils::splitString(str, "-")};
|
if (values.size() != 2)
|
||||||
if (values.size() != 2)
|
return std::nullopt;
|
||||||
return std::nullopt;
|
|
||||||
|
|
||||||
if (values[0] != "tr")
|
if (values[0] != "tr")
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
if (const auto value {StringUtils::readAs<Database::TrackId::ValueType>(values[1])})
|
if (const auto value{ StringUtils::readAs<Database::TrackId::ValueType>(values[1]) })
|
||||||
return Database::TrackId {*value};
|
return Database::TrackId{ *value };
|
||||||
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<Database::TrackListId>
|
std::optional<Database::TrackListId> readAs(std::string_view str)
|
||||||
readAs(std::string_view str)
|
{
|
||||||
{
|
std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
|
||||||
std::vector<std::string_view> values {StringUtils::splitString(str, "-")};
|
if (values.size() != 2)
|
||||||
if (values.size() != 2)
|
return std::nullopt;
|
||||||
return std::nullopt;
|
|
||||||
|
|
||||||
if (values[0] != "pl")
|
if (values[0] != "pl")
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
if (const auto value {StringUtils::readAs<Database::TrackListId::ValueType>(values[1])})
|
if (const auto value{ StringUtils::readAs<Database::TrackListId::ValueType>(values[1]) })
|
||||||
return Database::TrackListId {*value};
|
return Database::TrackListId{ *value };
|
||||||
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,36 +27,31 @@
|
|||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
struct RootId {};
|
struct RootId {};
|
||||||
|
|
||||||
std::string idToString(Database::ArtistId id);
|
std::string idToString(Database::ArtistId id);
|
||||||
std::string idToString(Database::ReleaseId id);
|
std::string idToString(Database::ReleaseId id);
|
||||||
std::string idToString(Database::TrackId id);
|
std::string idToString(Database::TrackId id);
|
||||||
std::string idToString(Database::TrackListId id);
|
std::string idToString(Database::TrackListId id);
|
||||||
std::string idToString(RootId);
|
std::string idToString(RootId);
|
||||||
} // namespace API::Subsonic
|
} // namespace API::Subsonic
|
||||||
|
|
||||||
// Used to parse parameters
|
// Used to parse parameters
|
||||||
namespace StringUtils
|
namespace StringUtils
|
||||||
{
|
{
|
||||||
template<>
|
template<>
|
||||||
std::optional<API::Subsonic::RootId>
|
std::optional<API::Subsonic::RootId> readAs(std::string_view str);
|
||||||
readAs(std::string_view str);
|
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<Database::ArtistId>
|
std::optional<Database::ArtistId> readAs(std::string_view str);
|
||||||
readAs(std::string_view str);
|
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<Database::ReleaseId>
|
std::optional<Database::ReleaseId> readAs(std::string_view str);
|
||||||
readAs(std::string_view str);
|
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<Database::TrackId>
|
std::optional<Database::TrackId> readAs(std::string_view str);
|
||||||
readAs(std::string_view str);
|
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<Database::TrackListId>
|
std::optional<Database::TrackListId> readAs(std::string_view str);
|
||||||
readAs(std::string_view str);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,378 +54,364 @@ using namespace Database;
|
|||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
|
std::unique_ptr<Wt::WResource> createSubsonicResource(Database::Db& db)
|
||||||
|
{
|
||||||
|
return std::make_unique<SubsonicResource>(db);
|
||||||
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WResource>
|
namespace
|
||||||
createSubsonicResource(Database::Db& db)
|
{
|
||||||
{
|
std::unordered_map<std::string, ProtocolVersion> readConfigProtocolVersions()
|
||||||
return std::make_unique<SubsonicResource>(db);
|
{
|
||||||
}
|
std::unordered_map<std::string, ProtocolVersion> res;
|
||||||
|
|
||||||
static
|
Service<IConfig>::get()->visitStrings("api-subsonic-report-old-server-protocol",
|
||||||
std::unordered_map<std::string, ProtocolVersion>
|
[&](std::string_view client)
|
||||||
readConfigProtocolVersions()
|
{
|
||||||
{
|
res.emplace(std::string{ client }, ProtocolVersion{ 1, 12, 0 });
|
||||||
std::unordered_map<std::string, ProtocolVersion> res;
|
}, { "DSub" });
|
||||||
|
|
||||||
Service<IConfig>::get()->visitStrings("api-subsonic-report-old-server-protocol",
|
return res;
|
||||||
[&](std::string_view client)
|
}
|
||||||
{
|
|
||||||
res.emplace(std::string {client}, ProtocolVersion {1, 12, 0});
|
|
||||||
}, {"DSub"});
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
SubsonicResource::SubsonicResource(Db& db)
|
std::string parameterMapToDebugString(const Wt::Http::ParameterMap& parameterMap)
|
||||||
: _serverProtocolVersionsByClient {readConfigProtocolVersions()}
|
{
|
||||||
, _db {db}
|
auto censorValue = [](const std::string& type, const std::string& value) -> std::string
|
||||||
{
|
{
|
||||||
}
|
if (type == "p" || type == "password")
|
||||||
|
return "*REDACTED*";
|
||||||
|
else
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
static
|
std::string res;
|
||||||
std::string parameterMapToDebugString(const Wt::Http::ParameterMap& parameterMap)
|
|
||||||
{
|
|
||||||
auto censorValue = [](const std::string& type, const std::string& value) -> std::string
|
|
||||||
{
|
|
||||||
if (type == "p" || type == "password")
|
|
||||||
return "*REDACTED*";
|
|
||||||
else
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::string res;
|
for (const auto& params : parameterMap)
|
||||||
|
{
|
||||||
|
res += "{" + params.first + "=";
|
||||||
|
if (params.second.size() == 1)
|
||||||
|
{
|
||||||
|
res += censorValue(params.first, params.second.front());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
res += "{";
|
||||||
|
for (const std::string& param : params.second)
|
||||||
|
res += censorValue(params.first, param) + ",";
|
||||||
|
res += "}";
|
||||||
|
}
|
||||||
|
res += "}, ";
|
||||||
|
}
|
||||||
|
|
||||||
for (const auto& params : parameterMap)
|
return res;
|
||||||
{
|
}
|
||||||
res += "{" + params.first + "=";
|
|
||||||
if (params.second.size() == 1)
|
|
||||||
{
|
|
||||||
res += censorValue(params.first, params.second.front());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
res += "{";
|
|
||||||
for (const std::string& param : params.second)
|
|
||||||
res += censorValue(params.first, param) + ",";
|
|
||||||
res += "}";
|
|
||||||
}
|
|
||||||
res += "}, ";
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
void checkUserTypeIsAllowed(RequestContext& context, EnumSet<Database::UserType> allowedUserTypes)
|
||||||
}
|
{
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
static
|
User::pointer currentUser{ User::find(context.dbSession, context.userId) };
|
||||||
void
|
if (!currentUser)
|
||||||
checkUserTypeIsAllowed(RequestContext& context, EnumSet<Database::UserType> allowedUserTypes)
|
throw RequestedDataNotFoundError{};
|
||||||
{
|
|
||||||
auto transaction {context.dbSession.createSharedTransaction()};
|
|
||||||
|
|
||||||
User::pointer currentUser {User::find(context.dbSession, context.userId)};
|
if (!allowedUserTypes.contains(currentUser->getType()))
|
||||||
if (!currentUser)
|
throw UserNotAuthorizedError{};
|
||||||
throw RequestedDataNotFoundError {};
|
}
|
||||||
|
|
||||||
if (!allowedUserTypes.contains(currentUser->getType()))
|
Response handlePingRequest(RequestContext& context)
|
||||||
throw UserNotAuthorizedError {};
|
{
|
||||||
}
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
|
}
|
||||||
|
|
||||||
static
|
Response handleGetLicenseRequest(RequestContext& context)
|
||||||
Response
|
{
|
||||||
handlePingRequest(RequestContext& context)
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
{
|
|
||||||
return Response::createOkResponse(context.serverProtocolVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
static
|
Response::Node& licenseNode{ response.createNode("license") };
|
||||||
Response
|
licenseNode.setAttribute("licenseExpires", "2025-09-03T14:46:43");
|
||||||
handleGetLicenseRequest(RequestContext& context)
|
licenseNode.setAttribute("email", "foo@bar.com");
|
||||||
{
|
licenseNode.setAttribute("valid", true);
|
||||||
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
|
||||||
|
|
||||||
Response::Node& licenseNode {response.createNode("license")};
|
return response;
|
||||||
licenseNode.setAttribute("licenseExpires", "2025-09-03T14:46:43");
|
}
|
||||||
licenseNode.setAttribute("email", "foo@bar.com");
|
|
||||||
licenseNode.setAttribute("valid", true);
|
|
||||||
|
|
||||||
return response;
|
Response handleNotImplemented(RequestContext&)
|
||||||
}
|
{
|
||||||
|
throw NotImplementedGenericError{};
|
||||||
|
}
|
||||||
|
|
||||||
static
|
|
||||||
Response
|
|
||||||
handleNotImplemented(RequestContext&)
|
|
||||||
{
|
|
||||||
throw NotImplementedGenericError {};
|
|
||||||
}
|
|
||||||
|
|
||||||
using RequestHandlerFunc = std::function<Response(RequestContext& context)>;
|
using RequestHandlerFunc = std::function<Response(RequestContext& context)>;
|
||||||
using CheckImplementedFunc = std::function<void()>;
|
using CheckImplementedFunc = std::function<void()>;
|
||||||
struct RequestEntryPointInfo
|
struct RequestEntryPointInfo
|
||||||
{
|
{
|
||||||
RequestHandlerFunc func;
|
RequestHandlerFunc func;
|
||||||
EnumSet<UserType> allowedUserTypes {UserType::DEMO, UserType::REGULAR, UserType::ADMIN};
|
EnumSet<UserType> allowedUserTypes{ UserType::DEMO, UserType::REGULAR, UserType::ADMIN };
|
||||||
CheckImplementedFunc checkFunc {};
|
CheckImplementedFunc checkFunc{};
|
||||||
};
|
};
|
||||||
|
|
||||||
static const std::unordered_map<std::string_view, RequestEntryPointInfo> requestEntryPoints
|
static const std::unordered_map<std::string_view, RequestEntryPointInfo> requestEntryPoints
|
||||||
{
|
{
|
||||||
// System
|
// System
|
||||||
{"/ping", {handlePingRequest}},
|
{"/ping", {handlePingRequest}},
|
||||||
{"/getLicense", {handleGetLicenseRequest}},
|
{"/getLicense", {handleGetLicenseRequest}},
|
||||||
|
|
||||||
// Browsing
|
// Browsing
|
||||||
{"/getMusicFolders", {handleGetMusicFoldersRequest}},
|
{"/getMusicFolders", {handleGetMusicFoldersRequest}},
|
||||||
{"/getIndexes", {handleGetIndexesRequest}},
|
{"/getIndexes", {handleGetIndexesRequest}},
|
||||||
{"/getMusicDirectory", {handleGetMusicDirectoryRequest}},
|
{"/getMusicDirectory", {handleGetMusicDirectoryRequest}},
|
||||||
{"/getGenres", {handleGetGenresRequest}},
|
{"/getGenres", {handleGetGenresRequest}},
|
||||||
{"/getArtists", {handleGetArtistsRequest}},
|
{"/getArtists", {handleGetArtistsRequest}},
|
||||||
{"/getArtist", {handleGetArtistRequest}},
|
{"/getArtist", {handleGetArtistRequest}},
|
||||||
{"/getAlbum", {handleGetAlbumRequest}},
|
{"/getAlbum", {handleGetAlbumRequest}},
|
||||||
{"/getSong", {handleGetSongRequest}},
|
{"/getSong", {handleGetSongRequest}},
|
||||||
{"/getVideos", {handleNotImplemented}},
|
{"/getVideos", {handleNotImplemented}},
|
||||||
{"/getArtistInfo", {handleGetArtistInfoRequest}},
|
{"/getArtistInfo", {handleGetArtistInfoRequest}},
|
||||||
{"/getArtistInfo2", {handleGetArtistInfo2Request}},
|
{"/getArtistInfo2", {handleGetArtistInfo2Request}},
|
||||||
{"/getAlbumInfo", {handleNotImplemented}},
|
{"/getAlbumInfo", {handleNotImplemented}},
|
||||||
{"/getAlbumInfo2", {handleNotImplemented}},
|
{"/getAlbumInfo2", {handleNotImplemented}},
|
||||||
{"/getSimilarSongs", {handleGetSimilarSongsRequest}},
|
{"/getSimilarSongs", {handleGetSimilarSongsRequest}},
|
||||||
{"/getSimilarSongs2", {handleGetSimilarSongs2Request}},
|
{"/getSimilarSongs2", {handleGetSimilarSongs2Request}},
|
||||||
{"/getTopSongs", {handleNotImplemented}},
|
{"/getTopSongs", {handleNotImplemented}},
|
||||||
|
|
||||||
// Album/song lists
|
// Album/song lists
|
||||||
{"/getAlbumList", {handleGetAlbumListRequest}},
|
{"/getAlbumList", {handleGetAlbumListRequest}},
|
||||||
{"/getAlbumList2", {handleGetAlbumList2Request}},
|
{"/getAlbumList2", {handleGetAlbumList2Request}},
|
||||||
{"/getRandomSongs", {handleGetRandomSongsRequest}},
|
{"/getRandomSongs", {handleGetRandomSongsRequest}},
|
||||||
{"/getSongsByGenre", {handleGetSongsByGenreRequest}},
|
{"/getSongsByGenre", {handleGetSongsByGenreRequest}},
|
||||||
{"/getNowPlaying", {handleNotImplemented}},
|
{"/getNowPlaying", {handleNotImplemented}},
|
||||||
{"/getStarred", {handleGetStarredRequest}},
|
{"/getStarred", {handleGetStarredRequest}},
|
||||||
{"/getStarred2", {handleGetStarred2Request}},
|
{"/getStarred2", {handleGetStarred2Request}},
|
||||||
|
|
||||||
// Searching
|
// Searching
|
||||||
{"/search", {handleNotImplemented}},
|
{"/search", {handleNotImplemented}},
|
||||||
{"/search2", {handleSearch2Request}},
|
{"/search2", {handleSearch2Request}},
|
||||||
{"/search3", {handleSearch3Request}},
|
{"/search3", {handleSearch3Request}},
|
||||||
|
|
||||||
// Playlists
|
// Playlists
|
||||||
{"/getPlaylists", {handleGetPlaylistsRequest}},
|
{"/getPlaylists", {handleGetPlaylistsRequest}},
|
||||||
{"/getPlaylist", {handleGetPlaylistRequest}},
|
{"/getPlaylist", {handleGetPlaylistRequest}},
|
||||||
{"/createPlaylist", {handleCreatePlaylistRequest}},
|
{"/createPlaylist", {handleCreatePlaylistRequest}},
|
||||||
{"/updatePlaylist", {handleUpdatePlaylistRequest}},
|
{"/updatePlaylist", {handleUpdatePlaylistRequest}},
|
||||||
{"/deletePlaylist", {handleDeletePlaylistRequest}},
|
{"/deletePlaylist", {handleDeletePlaylistRequest}},
|
||||||
|
|
||||||
// Media retrieval
|
// Media retrieval
|
||||||
{"/hls", {handleNotImplemented}},
|
{"/hls", {handleNotImplemented}},
|
||||||
{"/getCaptions", {handleNotImplemented}},
|
{"/getCaptions", {handleNotImplemented}},
|
||||||
{"/getLyrics", {handleNotImplemented}},
|
{"/getLyrics", {handleNotImplemented}},
|
||||||
{"/getAvatar", {handleNotImplemented}},
|
{"/getAvatar", {handleNotImplemented}},
|
||||||
|
|
||||||
// Media annotation
|
// Media annotation
|
||||||
{"/star", {handleStarRequest}},
|
{"/star", {handleStarRequest}},
|
||||||
{"/unstar", {handleUnstarRequest}},
|
{"/unstar", {handleUnstarRequest}},
|
||||||
{"/setRating", {handleNotImplemented}},
|
{"/setRating", {handleNotImplemented}},
|
||||||
{"/scrobble", {handleScrobble}},
|
{"/scrobble", {handleScrobble}},
|
||||||
|
|
||||||
// Sharing
|
// Sharing
|
||||||
{"/getShares", {handleNotImplemented}},
|
{"/getShares", {handleNotImplemented}},
|
||||||
{"/createShares", {handleNotImplemented}},
|
{"/createShares", {handleNotImplemented}},
|
||||||
{"/updateShare", {handleNotImplemented}},
|
{"/updateShare", {handleNotImplemented}},
|
||||||
{"/deleteShare", {handleNotImplemented}},
|
{"/deleteShare", {handleNotImplemented}},
|
||||||
|
|
||||||
// Podcast
|
// Podcast
|
||||||
{"/getPodcasts", {handleNotImplemented}},
|
{"/getPodcasts", {handleNotImplemented}},
|
||||||
{"/getNewestPodcasts", {handleNotImplemented}},
|
{"/getNewestPodcasts", {handleNotImplemented}},
|
||||||
{"/refreshPodcasts", {handleNotImplemented}},
|
{"/refreshPodcasts", {handleNotImplemented}},
|
||||||
{"/createPodcastChannel", {handleNotImplemented}},
|
{"/createPodcastChannel", {handleNotImplemented}},
|
||||||
{"/deletePodcastChannel", {handleNotImplemented}},
|
{"/deletePodcastChannel", {handleNotImplemented}},
|
||||||
{"/deletePodcastEpisode", {handleNotImplemented}},
|
{"/deletePodcastEpisode", {handleNotImplemented}},
|
||||||
{"/downloadPodcastEpisode", {handleNotImplemented}},
|
{"/downloadPodcastEpisode", {handleNotImplemented}},
|
||||||
|
|
||||||
// Jukebox
|
// Jukebox
|
||||||
{"/jukeboxControl", {handleNotImplemented}},
|
{"/jukeboxControl", {handleNotImplemented}},
|
||||||
|
|
||||||
// Internet radio
|
// Internet radio
|
||||||
{"/getInternetRadioStations", {handleNotImplemented}},
|
{"/getInternetRadioStations", {handleNotImplemented}},
|
||||||
{"/createInternetRadioStation", {handleNotImplemented}},
|
{"/createInternetRadioStation", {handleNotImplemented}},
|
||||||
{"/updateInternetRadioStation", {handleNotImplemented}},
|
{"/updateInternetRadioStation", {handleNotImplemented}},
|
||||||
{"/deleteInternetRadioStation", {handleNotImplemented}},
|
{"/deleteInternetRadioStation", {handleNotImplemented}},
|
||||||
|
|
||||||
// Chat
|
// Chat
|
||||||
{"/getChatMessages", {handleNotImplemented}},
|
{"/getChatMessages", {handleNotImplemented}},
|
||||||
{"/addChatMessages", {handleNotImplemented}},
|
{"/addChatMessages", {handleNotImplemented}},
|
||||||
|
|
||||||
// User management
|
// User management
|
||||||
{"/getUser", {handleGetUserRequest}},
|
{"/getUser", {handleGetUserRequest}},
|
||||||
{"/getUsers", {handleGetUsersRequest, {UserType::ADMIN}}},
|
{"/getUsers", {handleGetUsersRequest, {UserType::ADMIN}}},
|
||||||
{"/createUser", {handleCreateUserRequest, {UserType::ADMIN}, &Utils::checkSetPasswordImplemented}},
|
{"/createUser", {handleCreateUserRequest, {UserType::ADMIN}, &Utils::checkSetPasswordImplemented}},
|
||||||
{"/updateUser", {handleUpdateUserRequest, {UserType::ADMIN}}},
|
{"/updateUser", {handleUpdateUserRequest, {UserType::ADMIN}}},
|
||||||
{"/deleteUser", {handleDeleteUserRequest, {UserType::ADMIN}}},
|
{"/deleteUser", {handleDeleteUserRequest, {UserType::ADMIN}}},
|
||||||
{"/changePassword", {handleChangePassword, {UserType::REGULAR, UserType::ADMIN}, &Utils::checkSetPasswordImplemented}},
|
{"/changePassword", {handleChangePassword, {UserType::REGULAR, UserType::ADMIN}, &Utils::checkSetPasswordImplemented}},
|
||||||
|
|
||||||
// Bookmarks
|
// Bookmarks
|
||||||
{"/getBookmarks", {handleGetBookmarks}},
|
{"/getBookmarks", {handleGetBookmarks}},
|
||||||
{"/createBookmark", {handleCreateBookmark}},
|
{"/createBookmark", {handleCreateBookmark}},
|
||||||
{"/deleteBookmark", {handleDeleteBookmark}},
|
{"/deleteBookmark", {handleDeleteBookmark}},
|
||||||
{"/getPlayQueue", {handleNotImplemented}},
|
{"/getPlayQueue", {handleNotImplemented}},
|
||||||
{"/savePlayQueue", {handleNotImplemented}},
|
{"/savePlayQueue", {handleNotImplemented}},
|
||||||
|
|
||||||
// Media library scanning
|
// Media library scanning
|
||||||
{"/getScanStatus", {Scan::handleGetScanStatus, {UserType::ADMIN}}},
|
{"/getScanStatus", {Scan::handleGetScanStatus, {UserType::ADMIN}}},
|
||||||
{"/startScan", {Scan::handleStartScan, {UserType::ADMIN}}},
|
{"/startScan", {Scan::handleStartScan, {UserType::ADMIN}}},
|
||||||
};
|
};
|
||||||
|
|
||||||
using MediaRetrievalHandlerFunc = std::function<void(RequestContext&, const Wt::Http::Request&, Wt::Http::Response&)>;
|
using MediaRetrievalHandlerFunc = std::function<void(RequestContext&, const Wt::Http::Request&, Wt::Http::Response&)>;
|
||||||
static std::unordered_map<std::string, MediaRetrievalHandlerFunc> mediaRetrievalHandlers
|
static std::unordered_map<std::string, MediaRetrievalHandlerFunc> mediaRetrievalHandlers
|
||||||
{
|
{
|
||||||
// Media retrieval
|
// Media retrieval
|
||||||
{"/download", handleDownload},
|
{"/download", handleDownload},
|
||||||
{"/stream", handleStream},
|
{"/stream", handleStream},
|
||||||
{"/getCoverArt", handleGetCoverArt},
|
{"/getCoverArt", handleGetCoverArt},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
void
|
|
||||||
SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response)
|
|
||||||
{
|
|
||||||
static std::atomic<std::size_t> curRequestId {};
|
|
||||||
|
|
||||||
const std::size_t requestId {curRequestId++};
|
SubsonicResource::SubsonicResource(Db& db)
|
||||||
|
: _serverProtocolVersionsByClient{ readConfigProtocolVersions() }
|
||||||
|
, _db{ db }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
LMS_LOG(API_SUBSONIC, DEBUG) << "Handling request " << requestId << " '" << request.pathInfo() << "', continuation = " << (request.continuation() ? "true" : "false") << ", params = " << parameterMapToDebugString(request.getParameterMap());
|
void SubsonicResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||||
|
{
|
||||||
|
static std::atomic<std::size_t> curRequestId{};
|
||||||
|
|
||||||
std::string requestPath {request.pathInfo()};
|
const std::size_t requestId{ curRequestId++ };
|
||||||
if (StringUtils::stringEndsWith(requestPath, ".view"))
|
|
||||||
requestPath.resize(requestPath.length() - 5);
|
|
||||||
|
|
||||||
// Optional parameters
|
LMS_LOG(API_SUBSONIC, DEBUG) << "Handling request " << requestId << " '" << request.pathInfo() << "', continuation = " << (request.continuation() ? "true" : "false") << ", params = " << parameterMapToDebugString(request.getParameterMap());
|
||||||
const ResponseFormat format {getParameterAs<std::string>(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml};
|
|
||||||
|
|
||||||
ProtocolVersion protocolVersion {defaultServerProtocolVersion};
|
std::string requestPath{ request.pathInfo() };
|
||||||
|
if (StringUtils::stringEndsWith(requestPath, ".view"))
|
||||||
|
requestPath.resize(requestPath.length() - 5);
|
||||||
|
|
||||||
try
|
// Optional parameters
|
||||||
{
|
const ResponseFormat format{ getParameterAs<std::string>(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml };
|
||||||
// We need to parse client a soon as possible to make sure to answer with the right protocol version
|
|
||||||
protocolVersion = getServerProtocolVersion(getMandatoryParameterAs<std::string>(request.getParameterMap(), "c"));
|
|
||||||
RequestContext requestContext {buildRequestContext(request)};
|
|
||||||
|
|
||||||
auto itEntryPoint {requestEntryPoints.find(requestPath)};
|
ProtocolVersion protocolVersion{ defaultServerProtocolVersion };
|
||||||
if (itEntryPoint != requestEntryPoints.end())
|
|
||||||
{
|
|
||||||
if (itEntryPoint->second.checkFunc)
|
|
||||||
itEntryPoint->second.checkFunc();
|
|
||||||
|
|
||||||
checkUserTypeIsAllowed(requestContext, itEntryPoint->second.allowedUserTypes);
|
try
|
||||||
|
{
|
||||||
|
// We need to parse client a soon as possible to make sure to answer with the right protocol version
|
||||||
|
protocolVersion = getServerProtocolVersion(getMandatoryParameterAs<std::string>(request.getParameterMap(), "c"));
|
||||||
|
RequestContext requestContext{ buildRequestContext(request) };
|
||||||
|
|
||||||
Response resp {(itEntryPoint->second.func)(requestContext)};
|
auto itEntryPoint{ requestEntryPoints.find(requestPath) };
|
||||||
|
if (itEntryPoint != requestEntryPoints.end())
|
||||||
|
{
|
||||||
|
if (itEntryPoint->second.checkFunc)
|
||||||
|
itEntryPoint->second.checkFunc();
|
||||||
|
|
||||||
resp.write(response.out(), format);
|
checkUserTypeIsAllowed(requestContext, itEntryPoint->second.allowedUserTypes);
|
||||||
response.setMimeType(ResponseFormatToMimeType(format));
|
|
||||||
|
|
||||||
LMS_LOG(API_SUBSONIC, DEBUG) << "Request " << requestId << " '" << requestPath << "' handled!";
|
Response resp{ (itEntryPoint->second.func)(requestContext) };
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto itStreamHandler {mediaRetrievalHandlers.find(requestPath)};
|
resp.write(response.out(), format);
|
||||||
if (itStreamHandler != mediaRetrievalHandlers.end())
|
response.setMimeType(ResponseFormatToMimeType(format));
|
||||||
{
|
|
||||||
itStreamHandler->second(requestContext, request, response);
|
|
||||||
LMS_LOG(API_SUBSONIC, DEBUG) << "Request " << requestId << " '" << requestPath << "' handled!";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LMS_LOG(API_SUBSONIC, ERROR) << "Unhandled command '" << requestPath << "'";
|
LMS_LOG(API_SUBSONIC, DEBUG) << "Request " << requestId << " '" << requestPath << "' handled!";
|
||||||
throw UnknownEntryPointGenericError {};
|
return;
|
||||||
}
|
}
|
||||||
catch (const Error& e)
|
|
||||||
{
|
|
||||||
LMS_LOG(API_SUBSONIC, ERROR) << "Error while processing request '" << requestPath << "'"
|
|
||||||
<< ", params = [" << parameterMapToDebugString(request.getParameterMap()) << "]"
|
|
||||||
<< ", code = " << static_cast<int>(e.getCode()) << ", msg = '" << e.getMessage() << "'";
|
|
||||||
Response resp {Response::createFailedResponse(protocolVersion, e)};
|
|
||||||
resp.write(response.out(), format);
|
|
||||||
response.setMimeType(ResponseFormatToMimeType(format));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ProtocolVersion
|
auto itStreamHandler{ mediaRetrievalHandlers.find(requestPath) };
|
||||||
SubsonicResource::getServerProtocolVersion(const std::string& clientName) const
|
if (itStreamHandler != mediaRetrievalHandlers.end())
|
||||||
{
|
{
|
||||||
auto it {_serverProtocolVersionsByClient.find(clientName)};
|
itStreamHandler->second(requestContext, request, response);
|
||||||
if (it == std::cend(_serverProtocolVersionsByClient))
|
LMS_LOG(API_SUBSONIC, DEBUG) << "Request " << requestId << " '" << requestPath << "' handled!";
|
||||||
return defaultServerProtocolVersion;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return it->second;
|
LMS_LOG(API_SUBSONIC, ERROR) << "Unhandled command '" << requestPath << "'";
|
||||||
}
|
throw UnknownEntryPointGenericError{};
|
||||||
|
}
|
||||||
|
catch (const Error& e)
|
||||||
|
{
|
||||||
|
LMS_LOG(API_SUBSONIC, ERROR) << "Error while processing request '" << requestPath << "'"
|
||||||
|
<< ", params = [" << parameterMapToDebugString(request.getParameterMap()) << "]"
|
||||||
|
<< ", code = " << static_cast<int>(e.getCode()) << ", msg = '" << e.getMessage() << "'";
|
||||||
|
Response resp{ Response::createFailedResponse(protocolVersion, e) };
|
||||||
|
resp.write(response.out(), format);
|
||||||
|
response.setMimeType(ResponseFormatToMimeType(format));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void
|
ProtocolVersion SubsonicResource::getServerProtocolVersion(const std::string& clientName) const
|
||||||
SubsonicResource::checkProtocolVersion(ProtocolVersion client, ProtocolVersion server)
|
{
|
||||||
{
|
auto it{ _serverProtocolVersionsByClient.find(clientName) };
|
||||||
if (client.major > server.major)
|
if (it == std::cend(_serverProtocolVersionsByClient))
|
||||||
throw ServerMustUpgradeError {};
|
return defaultServerProtocolVersion;
|
||||||
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
|
return it->second;
|
||||||
SubsonicResource::getClientInfo(const Wt::Http::ParameterMap& parameters)
|
}
|
||||||
{
|
|
||||||
ClientInfo res;
|
|
||||||
|
|
||||||
if (hasParameter(parameters, "t"))
|
void SubsonicResource::checkProtocolVersion(ProtocolVersion client, ProtocolVersion server)
|
||||||
throw TokenAuthenticationNotSupportedForLDAPUsersError {};
|
{
|
||||||
|
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{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mandatory parameters
|
ClientInfo SubsonicResource::getClientInfo(const Wt::Http::ParameterMap& parameters)
|
||||||
res.name = getMandatoryParameterAs<std::string>(parameters, "c");
|
{
|
||||||
res.version = getMandatoryParameterAs<ProtocolVersion>(parameters, "v");
|
ClientInfo res;
|
||||||
res.user = getMandatoryParameterAs<std::string>(parameters, "u");
|
|
||||||
res.password = decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(parameters, "p"));
|
|
||||||
|
|
||||||
return res;
|
if (hasParameter(parameters, "t"))
|
||||||
}
|
throw TokenAuthenticationNotSupportedForLDAPUsersError{};
|
||||||
|
|
||||||
RequestContext
|
// Mandatory parameters
|
||||||
SubsonicResource::buildRequestContext(const Wt::Http::Request& request)
|
res.name = getMandatoryParameterAs<std::string>(parameters, "c");
|
||||||
{
|
res.version = getMandatoryParameterAs<ProtocolVersion>(parameters, "v");
|
||||||
const Wt::Http::ParameterMap& parameters {request.getParameterMap()};
|
res.user = getMandatoryParameterAs<std::string>(parameters, "u");
|
||||||
const ClientInfo clientInfo {getClientInfo(parameters)};
|
res.password = decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(parameters, "p"));
|
||||||
const Database::UserId userId {authenticateUser(request, clientInfo)};
|
|
||||||
|
|
||||||
return {parameters, _db.getTLSSession(), userId, clientInfo, getServerProtocolVersion(clientInfo.name)};
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
Database::UserId
|
RequestContext SubsonicResource::buildRequestContext(const Wt::Http::Request& request)
|
||||||
SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo)
|
{
|
||||||
{
|
const Wt::Http::ParameterMap& parameters{ request.getParameterMap() };
|
||||||
if (auto *authEnvService {Service<::Auth::IEnvService>::get()})
|
const ClientInfo clientInfo{ getClientInfo(parameters) };
|
||||||
{
|
const Database::UserId userId{ authenticateUser(request, clientInfo) };
|
||||||
const auto checkResult {authEnvService->processRequest(request)};
|
|
||||||
if (checkResult.state != ::Auth::IEnvService::CheckResult::State::Granted)
|
|
||||||
throw UserNotAuthorizedError {};
|
|
||||||
|
|
||||||
return *checkResult.userId;
|
return { parameters, _db.getTLSSession(), userId, clientInfo, getServerProtocolVersion(clientInfo.name) };
|
||||||
}
|
}
|
||||||
else if (auto *authPasswordService {Service<::Auth::IPasswordService>::get()})
|
|
||||||
{
|
|
||||||
const auto checkResult {authPasswordService->checkUserPassword(boost::asio::ip::address::from_string(request.clientAddress()),
|
|
||||||
clientInfo.user, clientInfo.password)};
|
|
||||||
|
|
||||||
switch (checkResult.state)
|
Database::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo)
|
||||||
{
|
{
|
||||||
case Auth::IPasswordService::CheckResult::State::Granted:
|
if (auto * authEnvService{ Service<::Auth::IEnvService>::get() })
|
||||||
return *checkResult.userId;
|
{
|
||||||
break;
|
const auto checkResult{ authEnvService->processRequest(request) };
|
||||||
case Auth::IPasswordService::CheckResult::State::Denied:
|
if (checkResult.state != ::Auth::IEnvService::CheckResult::State::Granted)
|
||||||
throw WrongUsernameOrPasswordError {};
|
throw UserNotAuthorizedError{};
|
||||||
case Auth::IPasswordService::CheckResult::State::Throttled:
|
|
||||||
throw LoginThrottledGenericError {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw InternalErrorGenericError {"No service avalaible to authenticate user"};
|
return *checkResult.userId;
|
||||||
}
|
}
|
||||||
|
else if (auto * authPasswordService{ Service<::Auth::IPasswordService>::get() })
|
||||||
|
{
|
||||||
|
const auto checkResult{ authPasswordService->checkUserPassword(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
|
||||||
|
|
||||||
|
|||||||
@@ -30,28 +30,28 @@
|
|||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
class Db;
|
class Db;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
|
|
||||||
class SubsonicResource final : public Wt::WResource
|
class SubsonicResource final : public Wt::WResource
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
SubsonicResource(Database::Db& db);
|
SubsonicResource(Database::Db& db);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) override;
|
void handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) override;
|
||||||
ProtocolVersion getServerProtocolVersion(const std::string& clientName) const;
|
ProtocolVersion getServerProtocolVersion(const std::string& clientName) const;
|
||||||
|
|
||||||
static void checkProtocolVersion(ProtocolVersion client, ProtocolVersion server);
|
static void checkProtocolVersion(ProtocolVersion client, ProtocolVersion server);
|
||||||
ClientInfo getClientInfo(const Wt::Http::ParameterMap& parameters);
|
ClientInfo getClientInfo(const Wt::Http::ParameterMap& parameters);
|
||||||
RequestContext buildRequestContext(const Wt::Http::Request& request);
|
RequestContext buildRequestContext(const Wt::Http::Request& request);
|
||||||
Database::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo);
|
Database::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo);
|
||||||
|
|
||||||
const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient;
|
const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient;
|
||||||
Database::Db& _db;
|
Database::Db& _db;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -34,249 +34,232 @@
|
|||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
|
|
||||||
std::string
|
std::string ResponseFormatToMimeType(ResponseFormat format)
|
||||||
ResponseFormatToMimeType(ResponseFormat format)
|
{
|
||||||
{
|
switch (format)
|
||||||
switch (format)
|
{
|
||||||
{
|
case ResponseFormat::xml: return "text/xml";
|
||||||
case ResponseFormat::xml: return "text/xml";
|
case ResponseFormat::json: return "application/json";
|
||||||
case ResponseFormat::json: return "application/json";
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void Response::Node::setValue(std::string_view value)
|
||||||
Response::Node::setValue(std::string_view value)
|
{
|
||||||
{
|
if (!_children.empty() || !_childrenArrays.empty())
|
||||||
if (!_children.empty() || !_childrenArrays.empty())
|
throw LmsException{ "Node already has children" };
|
||||||
throw LmsException {"Node already has children"};
|
|
||||||
|
|
||||||
_value = std::string {value};
|
_value = std::string{ value };
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void Response::Node::setValue(long long value)
|
||||||
Response::Node::setValue(long long value)
|
{
|
||||||
{
|
if (!_children.empty() || !_childrenArrays.empty())
|
||||||
if (!_children.empty() || !_childrenArrays.empty())
|
throw LmsException{ "Node already has children" };
|
||||||
throw LmsException {"Node already has children"};
|
|
||||||
|
|
||||||
_value = value;
|
_value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void Response::Node::setAttribute(std::string_view key, std::string_view value)
|
||||||
Response::Node::setAttribute(std::string_view key, std::string_view value)
|
{
|
||||||
{
|
_attributes[std::string{ key }] = std::string{ value };
|
||||||
_attributes[std::string {key}] = std::string {value};
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void Response::Node::addChild(const std::string& key, Node node)
|
||||||
Response::Node::addChild(const std::string& key, Node node)
|
{
|
||||||
{
|
if (_value)
|
||||||
if (_value)
|
throw LmsException{ "Node already has a value" };
|
||||||
throw LmsException {"Node already has a value"};
|
|
||||||
|
|
||||||
_children[key].emplace_back(std::move(node));
|
_children[key].emplace_back(std::move(node));
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void Response::Node::addArrayChild(const std::string& key, Node node)
|
||||||
Response::Node::addArrayChild(const std::string& key, Node node)
|
{
|
||||||
{
|
if (_value)
|
||||||
if (_value)
|
throw LmsException{ "Node already has a value" };
|
||||||
throw LmsException {"Node already has a value"};
|
|
||||||
|
|
||||||
_childrenArrays[key].emplace_back(std::move(node));
|
_childrenArrays[key].emplace_back(std::move(node));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Response::Node&
|
Response::Node& Response::Node::createChild(const std::string& key)
|
||||||
Response::Node::createChild(const std::string& key)
|
{
|
||||||
{
|
_children[key].emplace_back();
|
||||||
_children[key].emplace_back();
|
return _children[key].back();
|
||||||
return _children[key].back();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Response::Node&
|
Response::Node& Response::Node::createArrayChild(const std::string& key)
|
||||||
Response::Node::createArrayChild(const std::string& key)
|
{
|
||||||
{
|
_childrenArrays[key].emplace_back();
|
||||||
_childrenArrays[key].emplace_back();
|
return _childrenArrays[key].back();
|
||||||
return _childrenArrays[key].back();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void Response::Node::setVersionAttribute(ProtocolVersion protocolVersion)
|
||||||
Response::Node::setVersionAttribute(ProtocolVersion protocolVersion)
|
{
|
||||||
{
|
setAttribute("version", std::to_string(protocolVersion.major) + "." + std::to_string(protocolVersion.minor) + "." + std::to_string(protocolVersion.patch));
|
||||||
setAttribute("version", std::to_string(protocolVersion.major) + "." + std::to_string(protocolVersion.minor) + "." + std::to_string(protocolVersion.patch));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Response
|
Response Response::createOkResponse(ProtocolVersion protocolVersion)
|
||||||
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.setVersionAttribute(protocolVersion);
|
responseNode.setVersionAttribute(protocolVersion);
|
||||||
responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks
|
responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
Response
|
Response Response::createFailedResponse(ProtocolVersion protocolVersion, 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.setVersionAttribute(protocolVersion);
|
responseNode.setVersionAttribute(protocolVersion);
|
||||||
responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks
|
responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks
|
||||||
|
|
||||||
Node& errorNode {responseNode.createChild("error")};
|
Node& errorNode{ responseNode.createChild("error") };
|
||||||
errorNode.setAttribute("code", static_cast<int>(error.getCode()));
|
errorNode.setAttribute("code", static_cast<int>(error.getCode()));
|
||||||
errorNode.setAttribute("message", error.getMessage());
|
errorNode.setAttribute("message", error.getMessage());
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void Response::addNode(const std::string& key, Node node)
|
||||||
Response::addNode(const std::string& key, Node node)
|
{
|
||||||
{
|
return _root._children["subsonic-response"].front().addChild(key, std::move(node));
|
||||||
return _root._children["subsonic-response"].front().addChild(key, std::move(node));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Response::Node&
|
Response::Node& Response::createNode(const std::string& key)
|
||||||
Response::createNode(const std::string& key)
|
{
|
||||||
{
|
return _root._children["subsonic-response"].front().createChild(key);
|
||||||
return _root._children["subsonic-response"].front().createChild(key);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Response::Node&
|
Response::Node& Response::createArrayNode(const std::string& key)
|
||||||
Response::createArrayNode(const std::string& key)
|
{
|
||||||
{
|
return _root._children["subsonic-response"].front().createArrayChild(key);
|
||||||
return _root._children["subsonic-response"].front().createArrayChild(key);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void Response::write(std::ostream& os, ResponseFormat format)
|
||||||
Response::write(std::ostream& os, ResponseFormat format)
|
{
|
||||||
{
|
switch (format)
|
||||||
switch (format)
|
{
|
||||||
{
|
case ResponseFormat::xml:
|
||||||
case ResponseFormat::xml:
|
writeXML(os);
|
||||||
writeXML(os);
|
break;
|
||||||
break;
|
case ResponseFormat::json:
|
||||||
case ResponseFormat::json:
|
writeJSON(os);
|
||||||
writeJSON(os);
|
break;
|
||||||
break;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void Response::writeXML(std::ostream& os)
|
||||||
Response::writeXML(std::ostream& os)
|
{
|
||||||
{
|
std::function<boost::property_tree::ptree(const Response::Node&)> nodeToPropertyTree = [&](const Response::Node& node)
|
||||||
std::function<boost::property_tree::ptree(const Response::Node&)> nodeToPropertyTree = [&] (const Response::Node& node)
|
{
|
||||||
{
|
boost::property_tree::ptree res;
|
||||||
boost::property_tree::ptree res;
|
|
||||||
|
|
||||||
for (auto itAttribute : node._attributes)
|
for (auto itAttribute : node._attributes)
|
||||||
{
|
{
|
||||||
if (std::holds_alternative<std::string>(itAttribute.second))
|
if (std::holds_alternative<std::string>(itAttribute.second))
|
||||||
res.put("<xmlattr>." + itAttribute.first, std::get<std::string>(itAttribute.second));
|
res.put("<xmlattr>." + itAttribute.first, std::get<std::string>(itAttribute.second));
|
||||||
else if (std::holds_alternative<bool>(itAttribute.second))
|
else if (std::holds_alternative<bool>(itAttribute.second))
|
||||||
res.put("<xmlattr>." + itAttribute.first, std::get<bool>(itAttribute.second));
|
res.put("<xmlattr>." + itAttribute.first, std::get<bool>(itAttribute.second));
|
||||||
else if (std::holds_alternative<long long>(itAttribute.second))
|
else if (std::holds_alternative<long long>(itAttribute.second))
|
||||||
res.put("<xmlattr>." + itAttribute.first, std::get<long long>(itAttribute.second));
|
res.put("<xmlattr>." + itAttribute.first, std::get<long long>(itAttribute.second));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node._value)
|
if (node._value)
|
||||||
{
|
{
|
||||||
const auto& value {*node._value};
|
const auto& value{ *node._value };
|
||||||
|
|
||||||
if (std::holds_alternative<std::string>(value))
|
if (std::holds_alternative<std::string>(value))
|
||||||
res.put_value(std::get<std::string>(value));
|
res.put_value(std::get<std::string>(value));
|
||||||
else if (std::holds_alternative<bool>(value))
|
else if (std::holds_alternative<bool>(value))
|
||||||
res.put_value(std::get<bool>(value));
|
res.put_value(std::get<bool>(value));
|
||||||
else if (std::holds_alternative<long long>(value))
|
else if (std::holds_alternative<long long>(value))
|
||||||
res.put_value(std::get<long long>(value));
|
res.put_value(std::get<long long>(value));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
for (auto itChildNode : node._children)
|
for (auto itChildNode : node._children)
|
||||||
{
|
{
|
||||||
for (const Response::Node& childNode : itChildNode.second)
|
for (const Response::Node& childNode : itChildNode.second)
|
||||||
res.add_child(itChildNode.first, nodeToPropertyTree(childNode));
|
res.add_child(itChildNode.first, nodeToPropertyTree(childNode));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto itChildArrayNode : node._childrenArrays)
|
for (auto itChildArrayNode : node._childrenArrays)
|
||||||
{
|
{
|
||||||
const std::vector<Response::Node>& childArrayNodes {itChildArrayNode.second};
|
const std::vector<Response::Node>& childArrayNodes{ itChildArrayNode.second };
|
||||||
|
|
||||||
for (const Response::Node& childNode : childArrayNodes )
|
for (const Response::Node& childNode : childArrayNodes)
|
||||||
res.add_child(itChildArrayNode.first, nodeToPropertyTree(childNode));
|
res.add_child(itChildArrayNode.first, nodeToPropertyTree(childNode));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
boost::property_tree::ptree root {nodeToPropertyTree(_root)};
|
boost::property_tree::ptree root{ nodeToPropertyTree(_root) };
|
||||||
boost::property_tree::write_xml(os, root);
|
boost::property_tree::write_xml(os, root);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void Response::writeJSON(std::ostream& os)
|
||||||
Response::writeJSON(std::ostream& os)
|
{
|
||||||
{
|
namespace Json = Wt::Json;
|
||||||
namespace Json = Wt::Json;
|
|
||||||
|
|
||||||
std::function<Json::Object(const Response::Node&)> nodeToJsonObject = [&] (const Response::Node& node)
|
std::function<Json::Object(const Response::Node&)> nodeToJsonObject = [&](const Response::Node& node)
|
||||||
{
|
{
|
||||||
Json::Object res;
|
Json::Object res;
|
||||||
|
|
||||||
auto valueToJsonValue {[](const Node::ValueType& value) -> Json::Value
|
auto valueToJsonValue{ [](const Node::ValueType& value) -> Json::Value
|
||||||
{
|
{
|
||||||
if (std::holds_alternative<std::string>(value))
|
if (std::holds_alternative<std::string>(value))
|
||||||
return Json::Value {std::get<std::string>(value)};
|
return Json::Value {std::get<std::string>(value)};
|
||||||
else if (std::holds_alternative<bool>(value))
|
else if (std::holds_alternative<bool>(value))
|
||||||
return Json::Value {std::get<bool>(value)};
|
return Json::Value {std::get<bool>(value)};
|
||||||
else if (std::holds_alternative<long long>(value))
|
else if (std::holds_alternative<long long>(value))
|
||||||
return Json::Value {std::get<long long>(value)};
|
return Json::Value {std::get<long long>(value)};
|
||||||
|
|
||||||
throw LmsException("Unexpected value type");
|
throw LmsException("Unexpected value type");
|
||||||
}};
|
} };
|
||||||
|
|
||||||
for (auto itAttribute : node._attributes)
|
for (auto itAttribute : node._attributes)
|
||||||
res[itAttribute.first] = valueToJsonValue(itAttribute.second);
|
res[itAttribute.first] = valueToJsonValue(itAttribute.second);
|
||||||
|
|
||||||
if (node._value)
|
if (node._value)
|
||||||
{
|
{
|
||||||
res["value"] = valueToJsonValue(*node._value);
|
res["value"] = valueToJsonValue(*node._value);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
for (auto itChildNode : node._children)
|
for (auto itChildNode : node._children)
|
||||||
{
|
{
|
||||||
for (const Response::Node& childNode : itChildNode.second)
|
for (const Response::Node& childNode : itChildNode.second)
|
||||||
res[itChildNode.first] = nodeToJsonObject(childNode);
|
res[itChildNode.first] = nodeToJsonObject(childNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto itChildArrayNode : node._childrenArrays)
|
for (auto itChildArrayNode : node._childrenArrays)
|
||||||
{
|
{
|
||||||
const std::vector<Response::Node>& childArrayNodes {itChildArrayNode .second};
|
const std::vector<Response::Node>& childArrayNodes{ itChildArrayNode.second };
|
||||||
|
|
||||||
Json::Array array;
|
Json::Array array;
|
||||||
for (const Response::Node& childNode : childArrayNodes )
|
for (const Response::Node& childNode : childArrayNodes)
|
||||||
array.emplace_back(nodeToJsonObject(childNode));
|
array.emplace_back(nodeToJsonObject(childNode));
|
||||||
|
|
||||||
res[itChildArrayNode.first] = std::move(array);
|
res[itChildArrayNode.first] = std::move(array);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
Json::Object root {nodeToJsonObject(_root)};
|
Json::Object root{ nodeToJsonObject(_root) };
|
||||||
os << Json::serialize(root);
|
os << Json::serialize(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|||||||
@@ -30,220 +30,220 @@
|
|||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
|
|
||||||
enum class ResponseFormat
|
enum class ResponseFormat
|
||||||
{
|
{
|
||||||
xml,
|
xml,
|
||||||
json,
|
json,
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string ResponseFormatToMimeType(ResponseFormat format);
|
std::string ResponseFormatToMimeType(ResponseFormat format);
|
||||||
|
|
||||||
class Error
|
class Error
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
enum class Code
|
enum class Code
|
||||||
{
|
{
|
||||||
Generic = 0,
|
Generic = 0,
|
||||||
RequiredParameterMissing = 10,
|
RequiredParameterMissing = 10,
|
||||||
ClientMustUpgrade = 20,
|
ClientMustUpgrade = 20,
|
||||||
ServerMustUpgrade = 30,
|
ServerMustUpgrade = 30,
|
||||||
WrongUsernameOrPassword = 40,
|
WrongUsernameOrPassword = 40,
|
||||||
TokenAuthenticationNotSupportedForLDAPUsers = 41,
|
TokenAuthenticationNotSupportedForLDAPUsers = 41,
|
||||||
UserNotAuthorized = 50,
|
UserNotAuthorized = 50,
|
||||||
RequestedDataNotFound = 70,
|
RequestedDataNotFound = 70,
|
||||||
};
|
};
|
||||||
|
|
||||||
Error(Code code) : _code {code} {}
|
Error(Code code) : _code{ code } {}
|
||||||
|
|
||||||
virtual std::string getMessage() const = 0;
|
virtual std::string getMessage() const = 0;
|
||||||
|
|
||||||
Code getCode() const { return _code; }
|
Code getCode() const { return _code; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const Code _code;
|
const Code _code;
|
||||||
};
|
};
|
||||||
|
|
||||||
class GenericError : public Error
|
class GenericError : public Error
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
GenericError() : Error {Code::Generic} {}
|
GenericError() : Error{ Code::Generic } {}
|
||||||
};
|
};
|
||||||
|
|
||||||
class RequiredParameterMissingError : public Error
|
class RequiredParameterMissingError : public Error
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
RequiredParameterMissingError(std::string_view param)
|
RequiredParameterMissingError(std::string_view param)
|
||||||
: Error {Code::RequiredParameterMissing}
|
: Error{ Code::RequiredParameterMissing }
|
||||||
, _param {param}
|
, _param{ param }
|
||||||
{}
|
{}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string getMessage() const override { return "Required parameter '" + _param + "' is missing."; }
|
std::string getMessage() const override { return "Required parameter '" + _param + "' is missing."; }
|
||||||
std::string _param;
|
std::string _param;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ClientMustUpgradeError : public Error
|
class ClientMustUpgradeError : public Error
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ClientMustUpgradeError() : Error {Code::ClientMustUpgrade} {}
|
ClientMustUpgradeError() : Error{ Code::ClientMustUpgrade } {}
|
||||||
private:
|
private:
|
||||||
std::string getMessage() const override { return "Incompatible Subsonic REST protocol version. Client must upgrade."; }
|
std::string getMessage() const override { return "Incompatible Subsonic REST protocol version. Client must upgrade."; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class ServerMustUpgradeError : public Error
|
class ServerMustUpgradeError : public Error
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ServerMustUpgradeError() : Error {Code::ServerMustUpgrade} {}
|
ServerMustUpgradeError() : Error{ Code::ServerMustUpgrade } {}
|
||||||
private:
|
private:
|
||||||
std::string getMessage() const override { return "Incompatible Subsonic REST protocol version. Server must upgrade."; }
|
std::string getMessage() const override { return "Incompatible Subsonic REST protocol version. Server must upgrade."; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class WrongUsernameOrPasswordError : public Error
|
class WrongUsernameOrPasswordError : public Error
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
WrongUsernameOrPasswordError() : Error {Code::WrongUsernameOrPassword} {}
|
WrongUsernameOrPasswordError() : Error{ Code::WrongUsernameOrPassword } {}
|
||||||
private:
|
private:
|
||||||
std::string getMessage() const override { return "Wrong username or password."; }
|
std::string getMessage() const override { return "Wrong username or password."; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class TokenAuthenticationNotSupportedForLDAPUsersError : public Error
|
class TokenAuthenticationNotSupportedForLDAPUsersError : public Error
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
TokenAuthenticationNotSupportedForLDAPUsersError() : Error {Code::TokenAuthenticationNotSupportedForLDAPUsers} {}
|
TokenAuthenticationNotSupportedForLDAPUsersError() : Error{ Code::TokenAuthenticationNotSupportedForLDAPUsers } {}
|
||||||
private:
|
private:
|
||||||
std::string getMessage() const override { return "Token authentication not supported for LDAP users."; }
|
std::string getMessage() const override { return "Token authentication not supported for LDAP users."; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class UserNotAuthorizedError : public Error
|
class UserNotAuthorizedError : public Error
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
UserNotAuthorizedError () : Error {Code::UserNotAuthorized} {}
|
UserNotAuthorizedError() : Error{ Code::UserNotAuthorized } {}
|
||||||
private:
|
private:
|
||||||
std::string getMessage() const override { return "User is not authorized for the given operation."; }
|
std::string getMessage() const override { return "User is not authorized for the given operation."; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class RequestedDataNotFoundError : public Error
|
class RequestedDataNotFoundError : public Error
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
RequestedDataNotFoundError() : Error {Code::RequestedDataNotFound} {}
|
RequestedDataNotFoundError() : Error{ Code::RequestedDataNotFound } {}
|
||||||
private:
|
private:
|
||||||
std::string getMessage() const override { return "The requested data was not found."; }
|
std::string getMessage() const override { return "The requested data was not found."; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class InternalErrorGenericError : public GenericError
|
class InternalErrorGenericError : public GenericError
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
InternalErrorGenericError(const std::string& message) : _message {message} {}
|
InternalErrorGenericError(const std::string& message) : _message{ message } {}
|
||||||
private:
|
private:
|
||||||
std::string getMessage() const override { return "Internal error: " + _message; }
|
std::string getMessage() const override { return "Internal error: " + _message; }
|
||||||
const std::string _message;
|
const std::string _message;
|
||||||
};
|
};
|
||||||
|
|
||||||
class LoginThrottledGenericError : public GenericError
|
class LoginThrottledGenericError : public GenericError
|
||||||
{
|
{
|
||||||
std::string getMessage() const override { return "Login throttled, too many attempts"; }
|
std::string getMessage() const override { return "Login throttled, too many attempts"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class NotImplementedGenericError : public GenericError
|
class NotImplementedGenericError : public GenericError
|
||||||
{
|
{
|
||||||
std::string getMessage() const override { return "Not implemented"; }
|
std::string getMessage() const override { return "Not implemented"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class UnknownEntryPointGenericError : public GenericError
|
class UnknownEntryPointGenericError : public GenericError
|
||||||
{
|
{
|
||||||
std::string getMessage() const override { return "Unknown API method"; }
|
std::string getMessage() const override { return "Unknown API method"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class PasswordTooWeakGenericError : public GenericError
|
class PasswordTooWeakGenericError : public GenericError
|
||||||
{
|
{
|
||||||
std::string getMessage() const override { return "Password too weak"; }
|
std::string getMessage() const override { return "Password too weak"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class PasswordMustMatchLoginNameGenericError : public GenericError
|
class PasswordMustMatchLoginNameGenericError : public GenericError
|
||||||
{
|
{
|
||||||
std::string getMessage() const override { return "Password must match login name"; }
|
std::string getMessage() const override { return "Password must match login name"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class DemoUserCannotChangePasswordGenericError : public GenericError
|
class DemoUserCannotChangePasswordGenericError : public GenericError
|
||||||
{
|
{
|
||||||
std::string getMessage() const override { return "Demo user cannot change its password"; }
|
std::string getMessage() const override { return "Demo user cannot change its password"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class UserAlreadyExistsGenericError : public GenericError
|
class UserAlreadyExistsGenericError : public GenericError
|
||||||
{
|
{
|
||||||
std::string getMessage() const override { return "User already exists"; }
|
std::string getMessage() const override { return "User already exists"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class BadParameterGenericError : public GenericError
|
class BadParameterGenericError : public GenericError
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
BadParameterGenericError(const std::string& parameterName) : _parameterName {parameterName} {}
|
BadParameterGenericError(const std::string& parameterName) : _parameterName{ parameterName } {}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string getMessage() const override { return "Parameter '" + _parameterName + "': bad value"; }
|
std::string getMessage() const override { return "Parameter '" + _parameterName + "': bad value"; }
|
||||||
|
|
||||||
const std::string _parameterName;
|
const std::string _parameterName;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Response
|
class Response
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
class Node
|
class Node
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void setAttribute(std::string_view key, std::string_view value);
|
void setAttribute(std::string_view key, std::string_view value);
|
||||||
|
|
||||||
template <typename T, std::enable_if_t<std::is_arithmetic<T>::value>* = nullptr>
|
template <typename T, std::enable_if_t<std::is_arithmetic<T>::value>* = nullptr>
|
||||||
void setAttribute(std::string_view key, T value)
|
void setAttribute(std::string_view key, T value)
|
||||||
{
|
{
|
||||||
if constexpr (std::is_same<bool, T>::value)
|
if constexpr (std::is_same<bool, T>::value)
|
||||||
_attributes[std::string {key}] = value;
|
_attributes[std::string{ key }] = value;
|
||||||
else
|
else
|
||||||
_attributes[std::string {key}] = static_cast<long long>(value);
|
_attributes[std::string{ key }] = static_cast<long long>(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A Node has either a value or some children
|
// A Node has either a value or some children
|
||||||
void setValue(std::string_view value);
|
void setValue(std::string_view value);
|
||||||
void setValue(long long value);
|
void setValue(long long value);
|
||||||
Node& createChild(const std::string& key);
|
Node& createChild(const std::string& key);
|
||||||
Node& createArrayChild(const std::string& key);
|
Node& createArrayChild(const std::string& key);
|
||||||
|
|
||||||
void addChild(const std::string& key, Node node);
|
void addChild(const std::string& key, Node node);
|
||||||
void addArrayChild(const std::string& key, Node node);
|
void addArrayChild(const std::string& key, Node node);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setVersionAttribute(ProtocolVersion version);
|
void setVersionAttribute(ProtocolVersion version);
|
||||||
|
|
||||||
friend class Response;
|
friend class Response;
|
||||||
using ValueType = std::variant<std::string, bool, long long>;
|
using ValueType = std::variant<std::string, bool, long long>;
|
||||||
std::map<std::string, ValueType> _attributes;
|
std::map<std::string, ValueType> _attributes;
|
||||||
std::optional<ValueType> _value;
|
std::optional<ValueType> _value;
|
||||||
std::map<std::string, std::vector<Node>> _children;
|
std::map<std::string, std::vector<Node>> _children;
|
||||||
std::map<std::string, std::vector<Node>> _childrenArrays;
|
std::map<std::string, std::vector<Node>> _childrenArrays;
|
||||||
};
|
};
|
||||||
|
|
||||||
static Response createOkResponse(ProtocolVersion protocolVersion);
|
static Response createOkResponse(ProtocolVersion protocolVersion);
|
||||||
static Response createFailedResponse(ProtocolVersion protocolVersion, const Error& error);
|
static Response createFailedResponse(ProtocolVersion protocolVersion, const Error& error);
|
||||||
|
|
||||||
virtual ~Response() {}
|
virtual ~Response() {}
|
||||||
Response(const Response&) = delete;
|
Response(const Response&) = delete;
|
||||||
Response& operator=(const Response&) = delete;
|
Response& operator=(const Response&) = delete;
|
||||||
Response(Response&&) = default;
|
Response(Response&&) = default;
|
||||||
Response& operator=(Response&&) = default;
|
Response& operator=(Response&&) = default;
|
||||||
|
|
||||||
void addNode(const std::string& key, Node node);
|
void addNode(const std::string& key, Node node);
|
||||||
Node& createNode(const std::string& key);
|
Node& createNode(const std::string& key);
|
||||||
Node& createArrayNode(const std::string& key);
|
Node& createArrayNode(const std::string& key);
|
||||||
|
|
||||||
void write(std::ostream& os, ResponseFormat format);
|
void write(std::ostream& os, ResponseFormat format);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void writeJSON(std::ostream& os);
|
void writeJSON(std::ostream& os);
|
||||||
void writeXML(std::ostream& os);
|
void writeXML(std::ostream& os);
|
||||||
|
|
||||||
Response() = default;
|
Response() = default;
|
||||||
Node _root;
|
Node _root;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|||||||
@@ -24,48 +24,48 @@
|
|||||||
|
|
||||||
namespace API::Subsonic::Scan
|
namespace API::Subsonic::Scan
|
||||||
{
|
{
|
||||||
using namespace Scanner;
|
using namespace Scanner;
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
Response::Node
|
Response::Node
|
||||||
createStatusResponseNode()
|
createStatusResponseNode()
|
||||||
{
|
{
|
||||||
Response::Node statusResponse;
|
Response::Node statusResponse;
|
||||||
|
|
||||||
const IScannerService::Status scanStatus{ Service<IScannerService>::get()->getStatus() };
|
const IScannerService::Status scanStatus{ Service<IScannerService>::get()->getStatus() };
|
||||||
|
|
||||||
statusResponse.setAttribute("scanning", scanStatus.currentState == IScannerService::State::InProgress);
|
statusResponse.setAttribute("scanning", scanStatus.currentState == IScannerService::State::InProgress);
|
||||||
if (scanStatus.currentState == IScannerService::State::InProgress)
|
if (scanStatus.currentState == IScannerService::State::InProgress)
|
||||||
{
|
{
|
||||||
std::size_t count{};
|
std::size_t count{};
|
||||||
|
|
||||||
if (scanStatus.currentScanStepStats && scanStatus.currentScanStepStats->currentStep == ScanStep::ScanningFiles)
|
if (scanStatus.currentScanStepStats && scanStatus.currentScanStepStats->currentStep == ScanStep::ScanningFiles)
|
||||||
count = scanStatus.currentScanStepStats->processedElems;
|
count = scanStatus.currentScanStepStats->processedElems;
|
||||||
|
|
||||||
statusResponse.setAttribute("count", count);
|
statusResponse.setAttribute("count", count);
|
||||||
}
|
}
|
||||||
|
|
||||||
return statusResponse;
|
return statusResponse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Response handleGetScanStatus(RequestContext& context)
|
Response handleGetScanStatus(RequestContext& context)
|
||||||
{
|
{
|
||||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
response.addNode("scanStatus", createStatusResponseNode());
|
response.addNode("scanStatus", createStatusResponseNode());
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
Response handleStartScan(RequestContext& context)
|
Response handleStartScan(RequestContext& context)
|
||||||
{
|
{
|
||||||
Service<IScannerService>::get()->requestImmediateScan(false);
|
Service<IScannerService>::get()->requestImmediateScan(false);
|
||||||
|
|
||||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
response.addNode("scanStatus", createStatusResponseNode());
|
response.addNode("scanStatus", createStatusResponseNode());
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
namespace API::Subsonic::Scan
|
namespace API::Subsonic::Scan
|
||||||
{
|
{
|
||||||
Response handleGetScanStatus(RequestContext& context);
|
Response handleGetScanStatus(RequestContext& context);
|
||||||
Response handleStartScan(RequestContext& context);
|
Response handleStartScan(RequestContext& context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,164 +37,164 @@ using namespace Database;
|
|||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
namespace {
|
namespace {
|
||||||
Av::Format userTranscodeFormatToAvFormat(AudioFormat format)
|
Av::Format userTranscodeFormatToAvFormat(AudioFormat format)
|
||||||
{
|
{
|
||||||
switch (format)
|
switch (format)
|
||||||
{
|
{
|
||||||
case AudioFormat::MP3: return Av::Format::MP3;
|
case AudioFormat::MP3: return Av::Format::MP3;
|
||||||
case AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS;
|
case AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS;
|
||||||
case AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS;
|
case AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS;
|
||||||
case AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS;
|
case AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS;
|
||||||
case AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS;
|
case AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS;
|
||||||
default: return Av::Format::OGG_OPUS;
|
default: return Av::Format::OGG_OPUS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StreamParameters
|
struct StreamParameters
|
||||||
{
|
{
|
||||||
Av::InputFileParameters inputFileParameters;
|
Av::InputFileParameters inputFileParameters;
|
||||||
std::optional<Av::TranscodeParameters> transcodeParameters;
|
std::optional<Av::TranscodeParameters> transcodeParameters;
|
||||||
bool estimateContentLength{};
|
bool estimateContentLength{};
|
||||||
};
|
};
|
||||||
|
|
||||||
StreamParameters getStreamParameters(RequestContext& context)
|
StreamParameters getStreamParameters(RequestContext& context)
|
||||||
{
|
{
|
||||||
// Mandatory params
|
// Mandatory params
|
||||||
const TrackId id{ getMandatoryParameterAs<TrackId>(context.parameters, "id") };
|
const TrackId id{ getMandatoryParameterAs<TrackId>(context.parameters, "id") };
|
||||||
|
|
||||||
// Optional params
|
// Optional params
|
||||||
std::optional<std::size_t> maxBitRate{ getParameterAs<std::size_t>(context.parameters, "maxBitRate") };
|
std::optional<std::size_t> maxBitRate{ getParameterAs<std::size_t>(context.parameters, "maxBitRate") };
|
||||||
std::optional<std::string> format{ getParameterAs<std::string>(context.parameters, "format") };
|
std::optional<std::string> format{ getParameterAs<std::string>(context.parameters, "format") };
|
||||||
bool estimateContentLength{ getParameterAs<bool>(context.parameters, "estimateContentLength").value_or(false) };
|
bool estimateContentLength{ getParameterAs<bool>(context.parameters, "estimateContentLength").value_or(false) };
|
||||||
|
|
||||||
StreamParameters parameters;
|
StreamParameters parameters;
|
||||||
|
|
||||||
parameters.estimateContentLength = estimateContentLength;
|
parameters.estimateContentLength = estimateContentLength;
|
||||||
|
|
||||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
{
|
{
|
||||||
auto track{ Track::find(context.dbSession, id) };
|
auto track{ Track::find(context.dbSession, id) };
|
||||||
if (!track)
|
if (!track)
|
||||||
throw RequestedDataNotFoundError{};
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
parameters.inputFileParameters.trackPath = track->getPath();
|
parameters.inputFileParameters.trackPath = track->getPath();
|
||||||
parameters.inputFileParameters.duration = track->getDuration();
|
parameters.inputFileParameters.duration = track->getDuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const User::pointer user{ User::find(context.dbSession, context.userId) };
|
const User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
if (!user)
|
if (!user)
|
||||||
throw UserNotAuthorizedError{};
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
// format = "raw" => no transcode. Other format values will be ignored
|
// format = "raw" => no transcode. Other format values will be ignored
|
||||||
const bool transcode{ (!format || (*format != "raw")) && user->getSubsonicTranscodeEnable() };
|
const bool transcode{ (!format || (*format != "raw")) && user->getSubsonicTranscodeEnable() };
|
||||||
if (transcode)
|
if (transcode)
|
||||||
{
|
{
|
||||||
std::size_t bitRate{ user->getSubsonicTranscodeBitrate() / 1000 };
|
std::size_t bitRate{ user->getSubsonicTranscodeBitrate() / 1000 };
|
||||||
|
|
||||||
// "If set to zero, no limit is imposed"
|
// "If set to zero, no limit is imposed"
|
||||||
if (maxBitRate && *maxBitRate != 0)
|
if (maxBitRate && *maxBitRate != 0)
|
||||||
bitRate = Utils::clamp(*maxBitRate, std::size_t{ 48 }, bitRate);
|
bitRate = Utils::clamp(*maxBitRate, std::size_t{ 48 }, bitRate);
|
||||||
|
|
||||||
Av::TranscodeParameters transcodeParameters;
|
Av::TranscodeParameters transcodeParameters;
|
||||||
|
|
||||||
transcodeParameters.bitrate = bitRate * 1000;
|
transcodeParameters.bitrate = bitRate * 1000;
|
||||||
transcodeParameters.format = userTranscodeFormatToAvFormat(user->getSubsonicTranscodeFormat());
|
transcodeParameters.format = userTranscodeFormatToAvFormat(user->getSubsonicTranscodeFormat());
|
||||||
transcodeParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.)
|
transcodeParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.)
|
||||||
|
|
||||||
parameters.transcodeParameters = std::move(transcodeParameters);
|
parameters.transcodeParameters = std::move(transcodeParameters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parameters;
|
return parameters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response)
|
void handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||||
{
|
{
|
||||||
std::shared_ptr<IResourceHandler> resourceHandler;
|
std::shared_ptr<IResourceHandler> resourceHandler;
|
||||||
|
|
||||||
Wt::Http::ResponseContinuation* continuation{ request.continuation() };
|
Wt::Http::ResponseContinuation* continuation{ request.continuation() };
|
||||||
if (!continuation)
|
if (!continuation)
|
||||||
{
|
{
|
||||||
// Mandatory params
|
// Mandatory params
|
||||||
Database::TrackId id{ getMandatoryParameterAs<Database::TrackId>(context.parameters, "id") };
|
Database::TrackId id{ getMandatoryParameterAs<Database::TrackId>(context.parameters, "id") };
|
||||||
|
|
||||||
std::filesystem::path trackPath;
|
std::filesystem::path trackPath;
|
||||||
{
|
{
|
||||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
auto track{ Track::find(context.dbSession, id) };
|
auto track{ Track::find(context.dbSession, id) };
|
||||||
if (!track)
|
if (!track)
|
||||||
throw RequestedDataNotFoundError{};
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
trackPath = track->getPath();
|
trackPath = track->getPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceHandler = createFileResourceHandler(trackPath);
|
resourceHandler = createFileResourceHandler(trackPath);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
|
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
|
||||||
}
|
}
|
||||||
|
|
||||||
continuation = resourceHandler->processRequest(request, response);
|
continuation = resourceHandler->processRequest(request, response);
|
||||||
if (continuation)
|
if (continuation)
|
||||||
continuation->setData(resourceHandler);
|
continuation->setData(resourceHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response)
|
void handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||||
{
|
{
|
||||||
std::shared_ptr<IResourceHandler> resourceHandler;
|
std::shared_ptr<IResourceHandler> resourceHandler;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Wt::Http::ResponseContinuation* continuation = request.continuation();
|
Wt::Http::ResponseContinuation* continuation = request.continuation();
|
||||||
if (!continuation)
|
if (!continuation)
|
||||||
{
|
{
|
||||||
StreamParameters streamParameters{ getStreamParameters(context) };
|
StreamParameters streamParameters{ getStreamParameters(context) };
|
||||||
if (streamParameters.transcodeParameters)
|
if (streamParameters.transcodeParameters)
|
||||||
resourceHandler = Av::createTranscodeResourceHandler(streamParameters.inputFileParameters, *streamParameters.transcodeParameters, streamParameters.estimateContentLength);
|
resourceHandler = Av::createTranscodeResourceHandler(streamParameters.inputFileParameters, *streamParameters.transcodeParameters, streamParameters.estimateContentLength);
|
||||||
else
|
else
|
||||||
resourceHandler = createFileResourceHandler(streamParameters.inputFileParameters.trackPath);
|
resourceHandler = createFileResourceHandler(streamParameters.inputFileParameters.trackPath);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
|
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
|
||||||
}
|
}
|
||||||
|
|
||||||
continuation = resourceHandler->processRequest(request, response);
|
continuation = resourceHandler->processRequest(request, response);
|
||||||
if (continuation)
|
if (continuation)
|
||||||
continuation->setData(resourceHandler);
|
continuation->setData(resourceHandler);
|
||||||
}
|
}
|
||||||
catch (const Av::Exception& e)
|
catch (const Av::Exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(API_SUBSONIC, ERROR) << "Caught Av exception: " << e.what();
|
LMS_LOG(API_SUBSONIC, ERROR) << "Caught Av exception: " << e.what();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
|
void handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
|
||||||
{
|
{
|
||||||
// Mandatory params
|
// Mandatory params
|
||||||
const auto trackId{ getParameterAs<TrackId>(context.parameters, "id") };
|
const auto trackId{ getParameterAs<TrackId>(context.parameters, "id") };
|
||||||
const auto releaseId{ getParameterAs<ReleaseId>(context.parameters, "id") };
|
const auto releaseId{ getParameterAs<ReleaseId>(context.parameters, "id") };
|
||||||
|
|
||||||
if (!trackId && !releaseId)
|
if (!trackId && !releaseId)
|
||||||
throw BadParameterGenericError{ "id" };
|
throw BadParameterGenericError{ "id" };
|
||||||
|
|
||||||
std::size_t size{ getParameterAs<std::size_t>(context.parameters, "size").value_or(1024) };
|
std::size_t size{ getParameterAs<std::size_t>(context.parameters, "size").value_or(1024) };
|
||||||
size = ::Utils::clamp(size, std::size_t{ 32 }, std::size_t{ 2048 });
|
size = ::Utils::clamp(size, std::size_t{ 32 }, std::size_t{ 2048 });
|
||||||
|
|
||||||
std::shared_ptr<Image::IEncodedImage> cover;
|
std::shared_ptr<Image::IEncodedImage> cover;
|
||||||
if (trackId)
|
if (trackId)
|
||||||
cover = Service<Cover::ICoverService>::get()->getFromTrack(*trackId, size);
|
cover = Service<Cover::ICoverService>::get()->getFromTrack(*trackId, size);
|
||||||
else if (releaseId)
|
else if (releaseId)
|
||||||
cover = Service<Cover::ICoverService>::get()->getFromRelease(*releaseId, size);
|
cover = Service<Cover::ICoverService>::get()->getFromRelease(*releaseId, size);
|
||||||
|
|
||||||
response.out().write(reinterpret_cast<const char*>(cover->getData()), cover->getDataSize());
|
response.out().write(reinterpret_cast<const char*>(cover->getData()), cover->getDataSize());
|
||||||
response.setMimeType(std::string{ cover->getMimeType() });
|
response.setMimeType(std::string{ cover->getMimeType() });
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace API::Subsonic
|
} // namespace API::Subsonic
|
||||||
|
|||||||
@@ -26,8 +26,8 @@
|
|||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
void handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response);
|
void handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response);
|
||||||
void handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response);
|
void handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response);
|
||||||
void handleGetCoverArt(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response);
|
void handleGetCoverArt(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,14 +23,14 @@
|
|||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
Response::Node createGenreNode(const Database::Cluster::pointer& cluster)
|
Response::Node createGenreNode(const Database::Cluster::pointer& cluster)
|
||||||
{
|
{
|
||||||
Response::Node clusterNode;
|
Response::Node clusterNode;
|
||||||
|
|
||||||
clusterNode.setValue(cluster->getName());
|
clusterNode.setValue(cluster->getName());
|
||||||
clusterNode.setAttribute("songCount", cluster->getTracksCount());
|
clusterNode.setAttribute("songCount", cluster->getTracksCount());
|
||||||
clusterNode.setAttribute("albumCount", cluster->getReleasesCount());
|
clusterNode.setAttribute("albumCount", cluster->getReleasesCount());
|
||||||
|
|
||||||
return clusterNode;
|
return clusterNode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+204
-204
@@ -33,267 +33,267 @@
|
|||||||
namespace StringUtils
|
namespace StringUtils
|
||||||
{
|
{
|
||||||
|
|
||||||
bool readList(const std::string& str, const std::string& separators, std::list<std::string>& results)
|
bool readList(const std::string& str, const std::string& separators, std::list<std::string>& results)
|
||||||
{
|
{
|
||||||
std::string curStr;
|
std::string curStr;
|
||||||
|
|
||||||
for (char c : str)
|
for (char c : str)
|
||||||
{
|
{
|
||||||
if (separators.find(c) != std::string::npos) {
|
if (separators.find(c) != std::string::npos) {
|
||||||
if (!curStr.empty()) {
|
if (!curStr.empty()) {
|
||||||
results.push_back(curStr);
|
results.push_back(curStr);
|
||||||
curStr.clear();
|
curStr.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (curStr.empty() && std::isspace(c))
|
if (curStr.empty() && std::isspace(c))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
curStr.push_back(c);
|
curStr.push_back(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!curStr.empty())
|
if (!curStr.empty())
|
||||||
results.push_back(curStr);
|
results.push_back(curStr);
|
||||||
|
|
||||||
return !str.empty();
|
return !str.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<std::string> readAs(std::string_view str)
|
std::optional<std::string> readAs(std::string_view str)
|
||||||
{
|
{
|
||||||
return std::string{ str };
|
return std::string{ str };
|
||||||
}
|
}
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<std::string_view> readAs(std::string_view str)
|
std::optional<std::string_view> readAs(std::string_view str)
|
||||||
{
|
{
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<bool> readAs(std::string_view str)
|
std::optional<bool> readAs(std::string_view str)
|
||||||
{
|
{
|
||||||
if (str == "1" || str == "true")
|
if (str == "1" || str == "true")
|
||||||
return true;
|
return true;
|
||||||
else if (str == "0" || str == "false")
|
else if (str == "0" || str == "false")
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> splitStringCopy(std::string_view string, std::string_view separators)
|
std::vector<std::string> splitStringCopy(std::string_view string, std::string_view separators)
|
||||||
{
|
{
|
||||||
std::string str{ stringTrim(string, separators) };
|
std::string str{ stringTrim(string, separators) };
|
||||||
|
|
||||||
std::vector<std::string> res;
|
std::vector<std::string> res;
|
||||||
boost::algorithm::split(res, str, boost::is_any_of(separators), boost::token_compress_on);
|
boost::algorithm::split(res, str, boost::is_any_of(separators), boost::token_compress_on);
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string_view> splitString(std::string_view str, std::string_view separators)
|
std::vector<std::string_view> splitString(std::string_view str, std::string_view separators)
|
||||||
{
|
{
|
||||||
std::vector<std::string_view> res;
|
std::vector<std::string_view> res;
|
||||||
|
|
||||||
std::string_view::size_type strBegin{};
|
std::string_view::size_type strBegin{};
|
||||||
|
|
||||||
while ((strBegin = str.find_first_not_of(separators, strBegin)) != std::string_view::npos)
|
while ((strBegin = str.find_first_not_of(separators, strBegin)) != std::string_view::npos)
|
||||||
{
|
{
|
||||||
auto strEnd{ str.find_first_of(separators, strBegin + 1) };
|
auto strEnd{ str.find_first_of(separators, strBegin + 1) };
|
||||||
if (strEnd == std::string_view::npos)
|
if (strEnd == std::string_view::npos)
|
||||||
{
|
{
|
||||||
res.push_back(str.substr(strBegin, str.size() - strBegin));
|
res.push_back(str.substr(strBegin, str.size() - strBegin));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.push_back(str.substr(strBegin, strEnd - strBegin));
|
res.push_back(str.substr(strBegin, strEnd - strBegin));
|
||||||
strBegin = strEnd + 1;
|
strBegin = strEnd + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string joinStrings(const std::vector<std::string>& strings, const std::string& delimiter)
|
std::string joinStrings(const std::vector<std::string>& strings, const std::string& delimiter)
|
||||||
{
|
{
|
||||||
return boost::algorithm::join(strings, delimiter);
|
return boost::algorithm::join(strings, delimiter);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string_view stringTrim(std::string_view str, std::string_view whitespaces)
|
std::string_view stringTrim(std::string_view str, std::string_view whitespaces)
|
||||||
{
|
{
|
||||||
std::string_view res;
|
std::string_view res;
|
||||||
|
|
||||||
const auto strBegin = str.find_first_not_of(whitespaces);
|
const auto strBegin = str.find_first_not_of(whitespaces);
|
||||||
if (strBegin != std::string_view::npos)
|
if (strBegin != std::string_view::npos)
|
||||||
{
|
{
|
||||||
const auto strEnd{ str.find_last_not_of(whitespaces) };
|
const auto strEnd{ str.find_last_not_of(whitespaces) };
|
||||||
const auto strRange{ strEnd - strBegin + 1 };
|
const auto strRange{ strEnd - strBegin + 1 };
|
||||||
|
|
||||||
res = str.substr(strBegin, strRange);
|
res = str.substr(strBegin, strRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string_view stringTrimEnd(std::string_view str, std::string_view whitespaces)
|
std::string_view stringTrimEnd(std::string_view str, std::string_view whitespaces)
|
||||||
{
|
{
|
||||||
return str.substr(0, str.find_last_not_of(whitespaces) + 1);
|
return str.substr(0, str.find_last_not_of(whitespaces) + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string stringToLower(std::string_view str)
|
std::string stringToLower(std::string_view str)
|
||||||
{
|
{
|
||||||
std::string res;
|
std::string res;
|
||||||
res.reserve(str.size());
|
res.reserve(str.size());
|
||||||
|
|
||||||
std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](unsigned char c) { return std::tolower(c);});
|
std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](unsigned char c) { return std::tolower(c);});
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
void stringToLower(std::string& str)
|
void stringToLower(std::string& str)
|
||||||
{
|
{
|
||||||
std::transform(std::cbegin(str), std::cend(str), std::begin(str), [](unsigned char c) { return std::tolower(c);});
|
std::transform(std::cbegin(str), std::cend(str), std::begin(str), [](unsigned char c) { return std::tolower(c);});
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string stringToUpper(const std::string& str)
|
std::string stringToUpper(const std::string& str)
|
||||||
{
|
{
|
||||||
std::string res;
|
std::string res;
|
||||||
res.reserve(str.size());
|
res.reserve(str.size());
|
||||||
|
|
||||||
std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](char c) { return std::toupper(c);});
|
std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](char c) { return std::toupper(c);});
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string bufferToString(const std::vector<unsigned char>& data)
|
std::string bufferToString(const std::vector<unsigned char>& data)
|
||||||
{
|
{
|
||||||
std::ostringstream oss;
|
std::ostringstream oss;
|
||||||
|
|
||||||
for (unsigned char c : data)
|
for (unsigned char c : data)
|
||||||
{
|
{
|
||||||
oss << std::setw(2) << std::setfill('0') << std::hex << (int)c;
|
oss << std::setw(2) << std::setfill('0') << std::hex << (int)c;
|
||||||
}
|
}
|
||||||
|
|
||||||
return oss.str();
|
return oss.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
void capitalize(std::string& str)
|
void capitalize(std::string& str)
|
||||||
{
|
{
|
||||||
for (auto it{ std::begin(str) }; it != std::end(str); ++it)
|
for (auto it{ std::begin(str) }; it != std::end(str); ++it)
|
||||||
{
|
{
|
||||||
if (std::isspace(*it))
|
if (std::isspace(*it))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (std::isalpha(*it))
|
if (std::isalpha(*it))
|
||||||
*it = std::toupper(*it);
|
*it = std::toupper(*it);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string replaceInString(std::string_view str, const std::string& from, const std::string& to)
|
std::string replaceInString(std::string_view str, const std::string& from, const std::string& to)
|
||||||
{
|
{
|
||||||
std::string res{ str };
|
std::string res{ str };
|
||||||
size_t pos = 0;
|
size_t pos = 0;
|
||||||
|
|
||||||
while ((pos = res.find(from, pos)) != std::string::npos)
|
while ((pos = res.find(from, pos)) != std::string::npos)
|
||||||
{
|
{
|
||||||
res.replace(pos, from.length(), to);
|
res.replace(pos, from.length(), to);
|
||||||
pos += to.length();
|
pos += to.length();
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string jsEscape(const std::string& str)
|
std::string jsEscape(const std::string& str)
|
||||||
{
|
{
|
||||||
static const std::unordered_map<char, std::string_view> escapeMap
|
static const std::unordered_map<char, std::string_view> escapeMap
|
||||||
{
|
{
|
||||||
{ '\\', "\\\\" },
|
{ '\\', "\\\\" },
|
||||||
{ '\n', "\\n" },
|
{ '\n', "\\n" },
|
||||||
{ '\r', "\\r" },
|
{ '\r', "\\r" },
|
||||||
{ '\t', "\\t" },
|
{ '\t', "\\t" },
|
||||||
{ '"', "\\\"" },
|
{ '"', "\\\"" },
|
||||||
{ '\'', "\\\'" },
|
{ '\'', "\\\'" },
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string escaped;
|
std::string escaped;
|
||||||
escaped.reserve(str.length());
|
escaped.reserve(str.length());
|
||||||
|
|
||||||
for (const char c : str)
|
for (const char c : str)
|
||||||
{
|
{
|
||||||
auto it{ escapeMap.find(c) };
|
auto it{ escapeMap.find(c) };
|
||||||
if (it == std::cend(escapeMap))
|
if (it == std::cend(escapeMap))
|
||||||
{
|
{
|
||||||
escaped += c;
|
escaped += c;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
escaped += it->second;
|
escaped += it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
return escaped;
|
return escaped;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar)
|
std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar)
|
||||||
{
|
{
|
||||||
std::string res;
|
std::string res;
|
||||||
res.reserve(str.size());
|
res.reserve(str.size());
|
||||||
|
|
||||||
for (const char c : str)
|
for (const char c : str)
|
||||||
{
|
{
|
||||||
if (std::any_of(std::cbegin(charsToEscape), std::cend(charsToEscape), [c](char charToEscape) { return c == charToEscape; }))
|
if (std::any_of(std::cbegin(charsToEscape), std::cend(charsToEscape), [c](char charToEscape) { return c == charToEscape; }))
|
||||||
res += escapeChar;
|
res += escapeChar;
|
||||||
|
|
||||||
res += c;
|
res += c;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool stringEndsWith(const std::string& str, const std::string& ending)
|
bool stringEndsWith(const std::string& str, const std::string& ending)
|
||||||
{
|
{
|
||||||
return boost::algorithm::ends_with(str, ending);
|
return boost::algorithm::ends_with(str, ending);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::string> stringFromHex(const std::string& str)
|
std::optional<std::string> stringFromHex(const std::string& str)
|
||||||
{
|
{
|
||||||
static const char lut[]{ "0123456789ABCDEF" };
|
static const char lut[]{ "0123456789ABCDEF" };
|
||||||
|
|
||||||
if (str.length() % 2 != 0)
|
if (str.length() % 2 != 0)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
std::string res;
|
std::string res;
|
||||||
res.reserve(str.length() / 2);
|
res.reserve(str.length() / 2);
|
||||||
|
|
||||||
auto it{ std::cbegin(str) };
|
auto it{ std::cbegin(str) };
|
||||||
while (it != std::cend(str))
|
while (it != std::cend(str))
|
||||||
{
|
{
|
||||||
unsigned val{};
|
unsigned val{};
|
||||||
|
|
||||||
auto itHigh{ std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++))) };
|
auto itHigh{ std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++))) };
|
||||||
auto itLow{ std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++))) };
|
auto itLow{ std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++))) };
|
||||||
|
|
||||||
if (itHigh == std::cend(lut) || itLow == std::cend(lut))
|
if (itHigh == std::cend(lut) || itLow == std::cend(lut))
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
val = std::distance(std::cbegin(lut), itHigh) << 4;
|
val = std::distance(std::cbegin(lut), itHigh) << 4;
|
||||||
val += std::distance(std::cbegin(lut), itLow);
|
val += std::distance(std::cbegin(lut), itLow);
|
||||||
|
|
||||||
res.push_back(static_cast<char>(val));
|
res.push_back(static_cast<char>(val));
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string toISO8601String(const Wt::WDateTime& dateTime)
|
std::string toISO8601String(const Wt::WDateTime& dateTime)
|
||||||
{
|
{
|
||||||
return dateTime.toString("yyyy-MM-ddThh:mm:ss.zzz", false).toUTF8();
|
return dateTime.toString("yyyy-MM-ddThh:mm:ss.zzz", false).toUTF8();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string toISO8601String(const Wt::WDate& date)
|
std::string toISO8601String(const Wt::WDate& date)
|
||||||
{
|
{
|
||||||
return date.toString("yyyy-MM-dd").toUTF8();
|
return date.toString("yyyy-MM-dd").toUTF8();
|
||||||
}
|
}
|
||||||
} // StringUtils
|
} // StringUtils
|
||||||
|
|
||||||
|
|||||||
@@ -31,65 +31,65 @@
|
|||||||
|
|
||||||
namespace Wt
|
namespace Wt
|
||||||
{
|
{
|
||||||
class WDate;
|
class WDate;
|
||||||
class WDateTime;
|
class WDateTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace StringUtils {
|
namespace StringUtils {
|
||||||
|
|
||||||
[[nodiscard]] std::vector<std::string> splitStringCopy(std::string_view string, std::string_view separators);
|
[[nodiscard]] std::vector<std::string> splitStringCopy(std::string_view string, std::string_view separators);
|
||||||
|
|
||||||
[[nodiscard]] std::vector<std::string_view> splitString(std::string_view string, std::string_view separators);
|
[[nodiscard]] std::vector<std::string_view> splitString(std::string_view string, std::string_view separators);
|
||||||
|
|
||||||
[[nodiscard]] std::string joinStrings(const std::vector<std::string>& strings, const std::string& delimiter);
|
[[nodiscard]] std::string joinStrings(const std::vector<std::string>& strings, const std::string& delimiter);
|
||||||
|
|
||||||
[[nodiscard]] std::string_view stringTrim(std::string_view str, std::string_view whitespaces = " \t");
|
[[nodiscard]] std::string_view stringTrim(std::string_view str, std::string_view whitespaces = " \t");
|
||||||
|
|
||||||
[[nodiscard]] std::string_view stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t");
|
[[nodiscard]] std::string_view stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t");
|
||||||
|
|
||||||
[[nodiscard]] std::string stringToLower(std::string_view str);
|
[[nodiscard]] std::string stringToLower(std::string_view str);
|
||||||
|
|
||||||
void stringToLower(std::string& str);
|
void stringToLower(std::string& str);
|
||||||
|
|
||||||
[[nodiscard]] std::string stringToUpper(const std::string& str);
|
[[nodiscard]] std::string stringToUpper(const std::string& str);
|
||||||
|
|
||||||
[[nodiscard]] std::string bufferToString(const std::vector<unsigned char>& data);
|
[[nodiscard]] std::string bufferToString(const std::vector<unsigned char>& data);
|
||||||
|
|
||||||
void capitalize(std::string& str);
|
void capitalize(std::string& str);
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
[[nodiscard]] std::optional<T> readAs(std::string_view str)
|
[[nodiscard]] std::optional<T> readAs(std::string_view str)
|
||||||
{
|
{
|
||||||
T res;
|
T res;
|
||||||
|
|
||||||
std::istringstream iss{ std::string {str} };
|
std::istringstream iss{ std::string {str} };
|
||||||
iss >> res;
|
iss >> res;
|
||||||
if (iss.fail())
|
if (iss.fail())
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
[[nodiscard]] std::optional<std::string> readAs(std::string_view str);
|
[[nodiscard]] std::optional<std::string> readAs(std::string_view str);
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
[[nodiscard]] std::optional<std::string_view> readAs(std::string_view str);
|
[[nodiscard]] std::optional<std::string_view> readAs(std::string_view str);
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
[[nodiscard]] std::optional<bool> readAs(std::string_view str);
|
[[nodiscard]] std::optional<bool> readAs(std::string_view str);
|
||||||
|
|
||||||
[[nodiscard]] std::string replaceInString(std::string_view str, const std::string& from, const std::string& to);
|
[[nodiscard]] std::string replaceInString(std::string_view str, const std::string& from, const std::string& to);
|
||||||
|
|
||||||
[[nodiscard]] std::string jsEscape(const std::string& str);
|
[[nodiscard]] std::string jsEscape(const std::string& str);
|
||||||
|
|
||||||
[[nodiscard]] std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar);
|
[[nodiscard]] std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar);
|
||||||
|
|
||||||
[[nodiscard]] bool stringEndsWith(const std::string& str, const std::string& ending);
|
[[nodiscard]] bool stringEndsWith(const std::string& str, const std::string& ending);
|
||||||
|
|
||||||
[[nodiscard]] std::optional<std::string> stringFromHex(const std::string& str);
|
[[nodiscard]] std::optional<std::string> stringFromHex(const std::string& str);
|
||||||
|
|
||||||
[[nodiscard]] std::string toISO8601String(const Wt::WDateTime& dateTime);
|
[[nodiscard]] std::string toISO8601String(const Wt::WDateTime& dateTime);
|
||||||
[[nodiscard]] std::string toISO8601String(const Wt::WDate& date);
|
[[nodiscard]] std::string toISO8601String(const Wt::WDate& date);
|
||||||
|
|
||||||
} // StringUtils
|
} // StringUtils
|
||||||
Reference in New Issue
Block a user