Changed coding style

This commit is contained in:
emeric
2023-10-02 20:32:28 +02:00
parent 4fbacd691d
commit 7c7fbba319
18 changed files with 1265 additions and 1311 deletions
+7 -7
View File
@@ -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;
}; };
} }
+41 -41
View File
@@ -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);
} }
+25 -26
View File
@@ -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;
} }
} }
+9 -8
View File
@@ -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);
} }
+9 -9
View File
@@ -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;
}; };
} }
+75 -85
View File
@@ -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;
} }
} }
+16 -21
View File
@@ -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);
} }
+297 -311
View File
@@ -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
+15 -15
View File
@@ -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
+182 -199
View File
@@ -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
+176 -176
View File
@@ -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);
} }
+8 -8
View File
@@ -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
View File
@@ -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
+35 -35
View File
@@ -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