diff --git a/src/api/subsonic/SubsonicId.cpp b/src/api/subsonic/SubsonicId.cpp index 02e270e7..b23a2df4 100644 --- a/src/api/subsonic/SubsonicId.cpp +++ b/src/api/subsonic/SubsonicId.cpp @@ -28,6 +28,9 @@ namespace API::Subsonic boost::optional IdFromString(const std::string& id) { + if (id == "root") + return Id{Id::Type::Root}; + std::vector values {splitString(id, "-")}; if (values.size() != 2) { @@ -69,6 +72,8 @@ IdToString(const Id& id) switch (id.type) { + case Id::Type::Root: + return "root"; case Id::Type::Artist: res = "artist-"; break; diff --git a/src/api/subsonic/SubsonicId.hpp b/src/api/subsonic/SubsonicId.hpp index 60604dbf..afc2e137 100644 --- a/src/api/subsonic/SubsonicId.hpp +++ b/src/api/subsonic/SubsonicId.hpp @@ -28,13 +28,14 @@ struct Id { enum class Type { + Root, // Where all artists artistless albums reside Track, Release, Artist, }; Type type; - Database::IdType id; + Database::IdType id {}; }; boost::optional IdFromString(const std::string& id); diff --git a/src/api/subsonic/SubsonicResource.cpp b/src/api/subsonic/SubsonicResource.cpp index 9bb1f019..3633aa29 100644 --- a/src/api/subsonic/SubsonicResource.cpp +++ b/src/api/subsonic/SubsonicResource.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include "av/AvTranscoder.hpp" @@ -164,6 +165,13 @@ getClientInfo(const Wt::Http::ParameterMap& parameters) param = getParameterAs(parameters, "p"); if (!param) return {}; + + if (param->find("enc:") == 0) + { + param = stringFromHex(param->substr(4)); + if (!param) + return {}; + } res->password = *param; // Optional parameters @@ -173,6 +181,20 @@ getClientInfo(const Wt::Http::ParameterMap& parameters) return res; } +static +bool +checkPassword(Database::Handler& db, const ClientInfo& clientInfo) +{ + auto authUser {db.getUserDatabase().findWithIdentity(Wt::Auth::Identity::LoginName, clientInfo.user)}; + if (!authUser.isValid()) + { + LMS_LOG(API_SUBSONIC, ERROR) << "Cannot find user '" << clientInfo.user << "'"; + return false; + } + + return db.getPasswordService().verifyPassword(authUser, clientInfo.password) == Wt::Auth::PasswordResult::PasswordValid; +} + SubsonicResource::SubsonicResource(Wt::Dbo::SqlConnectionPool& connectionPool) : _db {connectionPool} { @@ -206,10 +228,6 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp LMS_LOG(API_SUBSONIC, DEBUG) << "\t'" << value << "'"; } - std::string s{std::istreambuf_iterator(request.in()), {}}; - - LMS_LOG(API_SUBSONIC, DEBUG) << "BODY = '" << s << "'"; - auto clientInfo {getClientInfo(parameters)}; if (!clientInfo) { @@ -223,6 +241,9 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp std::unique_lock lock{mutex}; // For now just handle request s one by one + if (!checkPassword(_db, *clientInfo)) + throw Error {Error::Code::WrongUsernameOrPassword}; + auto itHandler {requestHandlers.find(request.path())}; if (itHandler != requestHandlers.end()) { @@ -321,9 +342,15 @@ releaseToResponseNode(const Database::Release::pointer& release) if (!artists.empty()) { if (artists.size() > 1) + { albumNode.setAttribute("artist", "Various Artists"); + albumNode.setAttribute("parent", IdToString({Id::Type::Root})); + } else + { albumNode.setAttribute("artist", artists.front()->getName()); + albumNode.setAttribute("parent", IdToString({Id::Type::Artist, artists.front().id()})); + } } return albumNode; @@ -338,6 +365,7 @@ artistToResponseNode(const Database::Artist::pointer& artist) artistNode.setAttribute("id", IdToString({Id::Type::Artist, artist.id()})); artistNode.setAttribute("name", artist->getName()); artistNode.setAttribute("albumCount", std::to_string(artist->getReleases().size())); + artistNode.setAttribute("parent", IdToString({Id::Type::Root})); return artistNode; } @@ -512,6 +540,19 @@ handleGetMusicDirectoryRequest(const Wt::Http::ParameterMap& request, Database:: switch (id->type) { + case Id::Type::Root: + { + Wt::Dbo::Transaction transaction {db.getSession()}; + + directoryNode.setAttribute("name", "Music"); + + auto artists {Database::Artist::getAll(db.getSession())}; + for (const Database::Artist::pointer& artist : artists) + directoryNode.addArrayChild("child", artistToResponseNode(artist)); + + break; + } + case Id::Type::Artist: { Wt::Dbo::Transaction transaction {db.getSession()}; @@ -520,6 +561,8 @@ handleGetMusicDirectoryRequest(const Wt::Http::ParameterMap& request, Database:: if (!artist) throw Error {Error::Code::RequestedDataNotFound}; + directoryNode.setAttribute("name", artist->getName()); + auto releases {artist->getReleases()}; for (const Database::Release::pointer& release : releases) directoryNode.addArrayChild("child", releaseToResponseNode(release)); @@ -535,6 +578,8 @@ handleGetMusicDirectoryRequest(const Wt::Http::ParameterMap& request, Database:: if (!release) throw Error {Error::Code::RequestedDataNotFound}; + directoryNode.setAttribute("name", release->getName()); + auto tracks {release->getTracks()}; for (const Database::Track::pointer& track : tracks) directoryNode.addArrayChild("child", trackToResponseNode(track)); @@ -556,7 +601,7 @@ handleGetMusicFoldersRequest(const Wt::Http::ParameterMap& request, Database::Ha Response::Node& musicFoldersNode {response.createNode("musicFolders")}; Response::Node& musicFolderNode {musicFoldersNode.createArrayChild("musicFolder")}; - musicFolderNode.setAttribute("id", "1"); + musicFolderNode.setAttribute("id", IdToString({Id::Type::Root})); musicFolderNode.setAttribute("name", "Music"); return response; @@ -624,7 +669,7 @@ Response handleGetStarredRequest(const Wt::Http::ParameterMap& request, Database::Handler& db) { Response response {Response::createOkResponse()}; - response.createArrayNode("starred"); + response.createNode("starred"); return response; } @@ -633,7 +678,7 @@ Response handleGetStarred2Request(const Wt::Http::ParameterMap& request, Database::Handler& db) { Response response {Response::createOkResponse()}; - response.createArrayNode("starred2"); + response.createNode("starred2"); return response; } @@ -642,7 +687,7 @@ Response handleGetPlaylistsRequest(const Wt::Http::ParameterMap& request, Database::Handler& db) { Response response {Response::createOkResponse()}; - response.createArrayNode("playlists"); + response.createNode("playlists"); return response; } @@ -813,7 +858,7 @@ handleGetCoverArt(const Wt::Http::Request& request, Database::Handler& db, Wt::H cover = getServices().coverArtGrabber->getFromRelease(db.getSession(), id->id, Image::Format::JPEG, *size); break; default: - throw Error {"bad id format"}; + throw Error {"Bad id format"}; } response.setMimeType( Image::format_to_mimeType(Image::Format::JPEG) ); diff --git a/src/api/subsonic/SubsonicResponse.cpp b/src/api/subsonic/SubsonicResponse.cpp index dbd0d900..a4895c22 100644 --- a/src/api/subsonic/SubsonicResponse.cpp +++ b/src/api/subsonic/SubsonicResponse.cpp @@ -19,6 +19,8 @@ #include "SubsonicResponse.hpp" +#include + #include #include @@ -227,9 +229,13 @@ Response::write(std::ostream& os, ResponseFormat format) boost::property_tree::write_xml(os, root); break; case ResponseFormat::json: - boost::property_tree::write_json(os, root); + { + // property_tree does not support empty json array + std::ostringstream oss; + boost::property_tree::write_json(oss, root); + os << std::regex_replace(oss.str(), std::regex {R"(\[[\r\n]*\s*\"\"[\r\n]*\s*\])"}, R"(\{\})"); break; - + } } } diff --git a/src/api/subsonic/SubsonicResponse.hpp b/src/api/subsonic/SubsonicResponse.hpp index 4081e493..95a706bd 100644 --- a/src/api/subsonic/SubsonicResponse.hpp +++ b/src/api/subsonic/SubsonicResponse.hpp @@ -93,6 +93,7 @@ class Response Node& createNode(const std::string& key); Node& createArrayNode(const std::string& key); + void writeJson(std::ostream& os); void write(std::ostream& os, ResponseFormat format); private: diff --git a/src/database/DatabaseHandler.cpp b/src/database/DatabaseHandler.cpp index 5ba449f4..26c78e14 100644 --- a/src/database/DatabaseHandler.cpp +++ b/src/database/DatabaseHandler.cpp @@ -47,7 +47,7 @@ namespace Database { namespace { Wt::Auth::AuthService authService; - Wt::Auth::PasswordService passwordService(authService); + Wt::Auth::PasswordService passwordService {authService}; } diff --git a/src/database/DatabaseHandler.hpp b/src/database/DatabaseHandler.hpp index f57a096d..46d8fe5a 100644 --- a/src/database/DatabaseHandler.hpp +++ b/src/database/DatabaseHandler.hpp @@ -50,7 +50,7 @@ class Handler Wt::Dbo::ptr createUser(const Wt::Auth::User& authUser); Wt::Auth::AbstractUserDatabase& getUserDatabase(); - Wt::Auth::Login& getLogin() { return _login; } + Wt::Auth::Login& getLogin() { return _login; } // TODO move // Long living shared associated services static void configureAuth(); diff --git a/src/utils/Utils.cpp b/src/utils/Utils.cpp index db292efd..043ce800 100644 --- a/src/utils/Utils.cpp +++ b/src/utils/Utils.cpp @@ -116,4 +116,36 @@ replaceInString(std::string str, const std::string& from, const std::string& to) return str; } +boost::optional +stringFromHex(const std::string& str) +{ + static const char lut[] {"0123456789ABCDEF"}; + + if (str.length() % 2 != 0) + return boost::none; + + std::string res; + res.reserve(str.length() / 2); + + auto it {std::cbegin(str)}; + while (it != std::cend(str)) + { + unsigned val {}; + + 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++)))}; + + if (itHigh == std::cend(lut) || itLow == std::cend(lut)) + return {}; + + val = std::distance(std::cbegin(lut), itHigh) << 4; + val += std::distance(std::cbegin(lut), itLow ); + + res.push_back(static_cast(val)); + } + + return res; + +} + diff --git a/src/utils/Utils.hpp b/src/utils/Utils.hpp index 7f40d51a..cc06157c 100644 --- a/src/utils/Utils.hpp +++ b/src/utils/Utils.hpp @@ -64,6 +64,9 @@ boost::optional readAs(const std::string& str) std::string replaceInString(std::string str, const std::string& from, const std::string& to); +boost::optional +stringFromHex(const std::string& str); + // warning: not efficient template::value_type> void uniqueAndSortedByOccurence(In first, In last, Out out)