diff --git a/src/api/subsonic/SubsonicResource.cpp b/src/api/subsonic/SubsonicResource.cpp index 0d68c5d5..fbcbcd88 100644 --- a/src/api/subsonic/SubsonicResource.cpp +++ b/src/api/subsonic/SubsonicResource.cpp @@ -73,11 +73,54 @@ readAs(const std::string& str) namespace API::Subsonic { +struct ClientVersion +{ + unsigned major; + unsigned minor; + unsigned patch; +}; +} + +template<> +boost::optional +readAs(const std::string& str) +{ + // Expects "X.Y.Z" + const auto numbers {splitString(str, ".")}; + if (numbers.size() != 3) + return boost::none; + + API::Subsonic::ClientVersion version; + + auto number {readAs(numbers[0])}; + if (!number) + return boost::none; + version.major = *number; + + number = {readAs(numbers[1])}; + if (!number) + return boost::none; + version.minor = *number; + + number = {readAs(numbers[2])}; + if (!number) + return boost::none; + version.patch = *number; + + + return version; +} + + +namespace API::Subsonic +{ + struct ClientInfo { std::string name; std::string user; std::string password; + ClientVersion version; }; struct RequestContext @@ -215,6 +258,7 @@ getClientInfo(const Wt::Http::ParameterMap& parameters) res.name = getMandatoryParameterAs(parameters, "c"); res.user = getMandatoryParameterAs(parameters, "u"); res.password = decodePasswordIfNeeded(getMandatoryParameterAs(parameters, "p")); + res.version = getMandatoryParameterAs(parameters, "v"); return res; } @@ -757,6 +801,10 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) { releases = Release::getAll(context.dbSession, offset, size); } + else if (type == "alphabeticalByArtist") + { + releases = Release::getAllOrderedByArtist(context.dbSession, offset, size); + } else if (type == "byYear") { int fromYear {getMandatoryParameterAs(context.parameters, "fromYear")}; @@ -764,6 +812,10 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) releases = Release::getByYear(context.dbSession, fromYear, toYear, offset, size); } + else if (type == "starred") + { + releases = user->getStarredReleases(offset, size); + } else if (type == "byGenre") { // Mandatory param @@ -878,7 +930,7 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3) throw Error {Error::CustomType::BadIdFormat}; // Optional params - std::size_t count {getParameterAs(context.parameters, "count").get_value_or(10)}; + std::size_t count {getParameterAs(context.parameters, "count").get_value_or(20)}; Response response {Response::createOkResponse()}; Response::Node& artistInfoNode {response.createNode(id3 ? "artistInfo2" : "artistInfo")}; @@ -1814,9 +1866,16 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp try { - Session& dbSession {getOrCreateDbSession(_db)}; - + // Mandatory parameters const ClientInfo clientInfo {getClientInfo(parameters)}; + if (clientInfo.version.major > API_VERSION_MAJOR) + throw Error {Error::Code::ServerMustUpgrade}; + if (clientInfo.version.major < API_VERSION_MAJOR) + throw Error {Error::Code::ClientMustUpgrade}; + if (clientInfo.version.minor > API_VERSION_MINOR) + throw Error {Error::Code::ServerMustUpgrade}; + + Session& dbSession {getOrCreateDbSession(_db)}; switch (getService()->checkUserPassword(dbSession, boost::asio::ip::address::from_string(request.clientAddress()), diff --git a/src/api/subsonic/SubsonicResponse.cpp b/src/api/subsonic/SubsonicResponse.cpp index cb79b1eb..02ebf249 100644 --- a/src/api/subsonic/SubsonicResponse.cpp +++ b/src/api/subsonic/SubsonicResponse.cpp @@ -29,8 +29,6 @@ #include "utils/Exception.hpp" -#define API_VERSION "1.12.0" - namespace API::Subsonic { @@ -163,7 +161,7 @@ Response::createOkResponse() Node& responseNode {response._root.createChild("subsonic-response")}; responseNode.setAttribute("status", "ok"); - responseNode.setAttribute("version", API_VERSION); + responseNode.setAttribute("version", API_VERSION_STR); return response; } @@ -175,7 +173,7 @@ Response::createFailedResponse(const Error& error) Node& responseNode {response._root.createChild("subsonic-response")}; responseNode.setAttribute("status", "failed"); - responseNode.setAttribute("version", API_VERSION); + responseNode.setAttribute("version", API_VERSION_STR); Node& errorNode {responseNode.createChild("error")}; errorNode.setAttribute("code", std::to_string(static_cast(error.getCode()))); diff --git a/src/api/subsonic/SubsonicResponse.hpp b/src/api/subsonic/SubsonicResponse.hpp index 6f4dc9f9..0a97b98e 100644 --- a/src/api/subsonic/SubsonicResponse.hpp +++ b/src/api/subsonic/SubsonicResponse.hpp @@ -21,9 +21,15 @@ #include #include + namespace API::Subsonic { +#define API_VERSION_MAJOR 1 +#define API_VERSION_MINOR 12 +#define API_VERSION_PATCH 0 +#define API_VERSION_STR "1.12.0" + enum class ResponseFormat { xml, diff --git a/src/database/Release.cpp b/src/database/Release.cpp index 19bc053e..99c1b7e9 100644 --- a/src/database/Release.cpp +++ b/src/database/Release.cpp @@ -96,6 +96,23 @@ Release::getAll(Session& session, boost::optional offset, boost::op return std::vector(res.begin(), res.end()); } +std::vector +Release::getAllOrderedByArtist(Session& session, boost::optional offset, boost::optional size) +{ + session.checkSharedLocked(); + + Wt::Dbo::collection res = session.getDboSession().query>( + "SELECT DISTINCT r FROM release r" + " INNER JOIN track t ON r.id = t.release_id" + " INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id" + " INNER JOIN artist a ON t_a_l.artist_id = a.id") + .offset(offset ? static_cast(*offset) : -1) + .limit(size ? static_cast(*size) : -1) + .orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE"); + + return std::vector(res.begin(), res.end()); +} + std::vector Release::getAllRandom(Session& session, boost::optional size) { diff --git a/src/database/Release.hpp b/src/database/Release.hpp index a0a581df..af5a5401 100644 --- a/src/database/Release.hpp +++ b/src/database/Release.hpp @@ -52,6 +52,7 @@ class Release : public Wt::Dbo::Dbo static pointer getById(Session& session, IdType id); static std::vector getAllOrphans(Session& session); // no track related static std::vector getAll(Session& session, boost::optional offset = {}, boost::optional size = {}); + static std::vector getAllOrderedByArtist(Session& session, boost::optional offset = {}, boost::optional size = {}); static std::vector getAllRandom(Session& session, boost::optional size = {}); static std::vector getLastAdded(Session& session, const Wt::WDateTime& after, boost::optional offset = {}, boost::optional size = {}); static std::vector getByYear(Session& session, int yearFrom, int yearTo, boost::optional offset = {}, boost::optional size = {}); diff --git a/src/database/User.cpp b/src/database/User.cpp index 5ba9778c..afc6e95c 100644 --- a/src/database/User.cpp +++ b/src/database/User.cpp @@ -247,9 +247,13 @@ User::hasStarredRelease(Wt::Dbo::ptr release) const } std::vector> -User::getStarredReleases() const +User::getStarredReleases(boost::optional offset, boost::optional limit) const { - return std::vector>(_starredReleases.begin(), _starredReleases.end()); + Wt::Dbo::collection> res = _starredReleases.find() + .offset(offset ? static_cast(*offset) : -1) + .limit(limit ? static_cast(*limit) : -1); + + return std::vector>(res.begin(), res.end()); } diff --git a/src/database/User.hpp b/src/database/User.hpp index 40c2bf50..0b074024 100644 --- a/src/database/User.hpp +++ b/src/database/User.hpp @@ -21,6 +21,8 @@ #include +#include + #include #include @@ -160,7 +162,7 @@ class User : public Wt::Dbo::Dbo void starRelease(Wt::Dbo::ptr release); void unstarRelease(Wt::Dbo::ptr release); bool hasStarredRelease(Wt::Dbo::ptr release) const; - std::vector> getStarredReleases() const; + std::vector> getStarredReleases(boost::optional offset = {}, boost::optional size = {}) const; void starTrack(Wt::Dbo::ptr track); void unstarTrack(Wt::Dbo::ptr track);