From 3ee937a409d85b788edc6457fe1b7f075d062a3b Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 3 Apr 2019 23:53:41 +0200 Subject: [PATCH] API Subsonic: better support, added genre --- src/api/subsonic/SubsonicResource.cpp | 205 +++++++++++++++++++++++--- src/api/subsonic/SubsonicResponse.cpp | 113 +++++++++----- src/api/subsonic/SubsonicResponse.hpp | 28 ++-- src/database/Artist.cpp | 24 +-- src/database/Artist.hpp | 10 +- src/database/Release.cpp | 22 ++- src/database/Release.hpp | 5 +- src/database/Track.cpp | 12 +- src/database/Track.hpp | 4 +- src/utils/Utils.hpp | 7 + 10 files changed, 332 insertions(+), 98 deletions(-) diff --git a/src/api/subsonic/SubsonicResource.cpp b/src/api/subsonic/SubsonicResource.cpp index 91c0bbd2..9bb1f019 100644 --- a/src/api/subsonic/SubsonicResource.cpp +++ b/src/api/subsonic/SubsonicResource.cpp @@ -27,6 +27,7 @@ #include "av/AvTranscoder.hpp" #include "cover/CoverArtGrabber.hpp" #include "database/Artist.hpp" +#include "database/Cluster.hpp" #include "database/Release.hpp" #include "database/Track.hpp" #include "main/Services.hpp" @@ -40,11 +41,16 @@ #define GET_LICENSE_URL "/rest/getLicense.view" #define GET_RANDOM_SONGS_URL "/rest/getRandomSongs.view" #define GET_ALBUM_LIST_URL "/rest/getAlbumList.view" +#define GET_ALBUM_LIST2_URL "/rest/getAlbumList2.view" #define GET_MUSIC_DIRECTORY_URL "/rest/getMusicDirectory.view" -#define GET_STARRED_URL "/rest/getStarred.view" #define GET_MUSIC_FOLDERS_URL "/rest/getMusicFolders.view" +#define GET_GENRES_URL "/rest/getGenres.view" #define GET_INDEXES_URL "/rest/getIndexes.view" #define GET_ARTISTS_URL "/rest/getArtists.view" +#define GET_STARRED_URL "/rest/getStarred.view" +#define GET_STARRED2_URL "/rest/getStarred2.view" +#define GET_PLAYLISTS_URL "/rest/getPlaylists.view" +#define GET_SONGS_BY_GENRE_URL "/rest/getSongsByGenre.view" // MediaRetrievals #define STREAM_URL "/rest/stream.view" @@ -59,10 +65,16 @@ static Response handlePingRequest(const Wt::Http::ParameterMap& request, Databas static Response handleGetLicenseRequest(const Wt::Http::ParameterMap& request, Database::Handler& db); static Response handleGetRandomSongsRequest(const Wt::Http::ParameterMap& request, Database::Handler& db); static Response handleGetAlbumListRequest(const Wt::Http::ParameterMap& request, Database::Handler& db); +static Response handleGetAlbumList2Request(const Wt::Http::ParameterMap& request, Database::Handler& db); static Response handleGetMusicDirectoryRequest(const Wt::Http::ParameterMap& request, Database::Handler& db); static Response handleGetMusicFoldersRequest(const Wt::Http::ParameterMap& request, Database::Handler& db); +static Response handleGetGenresRequest(const Wt::Http::ParameterMap& request, Database::Handler& db); static Response handleGetIndexesRequest(const Wt::Http::ParameterMap& request, Database::Handler& db); static Response handleGetArtistsRequest(const Wt::Http::ParameterMap& request, Database::Handler& db); +static Response handleGetStarredRequest(const Wt::Http::ParameterMap& request, Database::Handler& db); +static Response handleGetStarred2Request(const Wt::Http::ParameterMap& request, Database::Handler& db); +static Response handleGetPlaylistsRequest(const Wt::Http::ParameterMap& request, Database::Handler& db); +static Response handleGetSongsByGenreRequest(const Wt::Http::ParameterMap& request, Database::Handler& db); // MediaRetrievals using MediaRetrivalHandlerFunc = std::function; @@ -75,11 +87,16 @@ static std::map requestHandlers {GET_LICENSE_URL, handleGetLicenseRequest}, {GET_RANDOM_SONGS_URL, handleGetRandomSongsRequest}, {GET_ALBUM_LIST_URL, handleGetAlbumListRequest}, + {GET_ALBUM_LIST2_URL, handleGetAlbumList2Request}, {GET_MUSIC_DIRECTORY_URL, handleGetMusicDirectoryRequest}, - {GET_STARRED_URL, handlePingRequest}, // TODO {GET_MUSIC_FOLDERS_URL, handleGetMusicFoldersRequest}, + {GET_GENRES_URL, handleGetGenresRequest}, {GET_INDEXES_URL, handleGetIndexesRequest}, {GET_ARTISTS_URL, handleGetArtistsRequest}, + {GET_STARRED_URL, handleGetStarredRequest}, + {GET_STARRED2_URL, handleGetStarred2Request}, + {GET_PLAYLISTS_URL, handleGetPlaylistsRequest}, + {GET_SONGS_BY_GENRE_URL, handleGetSongsByGenreRequest}, }; static std::map mediaRetrievalHandlers @@ -104,6 +121,21 @@ getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& pa return readAs(it->second.front()); } +template<> +boost::optional +getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param) +{ + boost::optional res; + + auto it = parameterMap.find(param); + if (it == parameterMap.end()) + return res; + + if (it->second.size() != 1) + return res; + + return it->second.front(); +} struct ClientInfo { @@ -195,7 +227,7 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp if (itHandler != requestHandlers.end()) { Response resp {(itHandler->second)(request.getParameterMap(), _db)}; - responseToStream(resp, clientInfo->format, response.out()); + resp.write(response.out(), clientInfo->format); response.setMimeType(ResponseFormatToMimeType(clientInfo->format)); return; } @@ -213,7 +245,7 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp catch (const Error& e) { Response resp {Response::createFailedResponse(e)}; - responseToStream(resp, clientInfo->format, response.out()); + resp.write(response.out(), clientInfo->format); response.setMimeType(ResponseFormatToMimeType(clientInfo->format)); } } @@ -301,15 +333,30 @@ static Response::Node artistToResponseNode(const Database::Artist::pointer& artist) { - Response::Node artistResponse; + Response::Node artistNode; - artistResponse.setAttribute("id", IdToString({Id::Type::Artist, artist.id()})); - artistResponse.setAttribute("name", artist->getName()); - artistResponse.setAttribute("albumCount", std::to_string(artist->getReleases().size())); + artistNode.setAttribute("id", IdToString({Id::Type::Artist, artist.id()})); + artistNode.setAttribute("name", artist->getName()); + artistNode.setAttribute("albumCount", std::to_string(artist->getReleases().size())); - return artistResponse; + return artistNode; } +static +Response::Node +clusterToResponseNode(const Database::Cluster::pointer& cluster) +{ + Response::Node clusterNode; + + clusterNode.setValue(cluster->getName()); + clusterNode.setAttribute("songCount", std::to_string(cluster->getTrackIds().size())); + { + auto releases {Database::Release::getByFilter(*cluster.session(), {cluster.id()})}; + clusterNode.setAttribute("albumCount", std::to_string(releases.size())); + } + + return clusterNode; +} // Handlers Response @@ -339,6 +386,8 @@ handleGetRandomSongsRequest(const Wt::Http::ParameterMap& parameters, Database:: if (!size) size = 50; + *size = std::min(*size, std::size_t {500}); + Wt::Dbo::Transaction transaction {db.getSession()}; auto tracks {Database::Track::getAllRandom(db.getSession(), *size)}; @@ -419,6 +468,10 @@ handleGetAlbumListRequest(const Wt::Http::ParameterMap& request, Database::Handl auto after {Wt::WLocalDateTime::currentServerDateTime().toUTC().addMonths(-1)}; releases = Database::Release::getLastAdded(db.getSession(), after, offset, size); } + else if (*type == "alphabeticalByName") + { + releases = Database::Release::getAll(db.getSession(), offset, size); + } else throw Error {"Unsupported request"}; @@ -433,6 +486,15 @@ handleGetAlbumListRequest(const Wt::Http::ParameterMap& request, Database::Handl return response; } +Response +handleGetAlbumList2Request(const Wt::Http::ParameterMap& request, Database::Handler& db) +{ + Response response {Response::createOkResponse()}; + response.createNode("albumList2"); + + return response; +} + Response handleGetMusicDirectoryRequest(const Wt::Http::ParameterMap& request, Database::Handler& db) { @@ -445,8 +507,6 @@ handleGetMusicDirectoryRequest(const Wt::Http::ParameterMap& request, Database:: if (!id) throw Error {"Bad id"}; - Wt::Dbo::Transaction transaction {db.getSession()}; - Response response {Response::createOkResponse()}; Response::Node& directoryNode {response.createNode("directory")}; @@ -454,6 +514,8 @@ handleGetMusicDirectoryRequest(const Wt::Http::ParameterMap& request, Database:: { case Id::Type::Artist: { + Wt::Dbo::Transaction transaction {db.getSession()}; + auto artist {Database::Artist::getById(db.getSession(), id->id)}; if (!artist) throw Error {Error::Code::RequestedDataNotFound}; @@ -467,6 +529,8 @@ handleGetMusicDirectoryRequest(const Wt::Http::ParameterMap& request, Database:: case Id::Type::Release: { + Wt::Dbo::Transaction transaction {db.getSession()}; + auto release {Database::Release::getById(db.getSession(), id->id)}; if (!release) throw Error {Error::Code::RequestedDataNotFound}; @@ -499,14 +563,36 @@ handleGetMusicFoldersRequest(const Wt::Http::ParameterMap& request, Database::Ha } Response -handleGetIndexesRequest(const Wt::Http::ParameterMap& request, Database::Handler& db) +handleGetGenresRequest(const Wt::Http::ParameterMap& request, Database::Handler& db) { + Response response {Response::createOkResponse()}; + + Response::Node& genresNode {response.createNode("genres")}; + Wt::Dbo::Transaction transaction {db.getSession()}; + auto clusterType {Database::ClusterType::getByName(db.getSession(), "GENRE")}; + if (clusterType) + { + auto clusters {clusterType->getClusters()}; + + for (const Database::Cluster::pointer& cluster : clusters) + genresNode.addArrayChild("genre", clusterToResponseNode(cluster)); + } + + return response; +} + +Response +handleGetIndexesRequest(const Wt::Http::ParameterMap& request, Database::Handler& db) +{ Response response {Response::createOkResponse()}; Response::Node& artistsNode {response.createNode("indexes")}; Response::Node& indexNode {artistsNode.createArrayChild("index")}; + indexNode.setAttribute("name", "?"); + + Wt::Dbo::Transaction transaction {db.getSession()}; auto artists {Database::Artist::getAll(db.getSession())}; for (const Database::Artist::pointer& artist : artists) @@ -519,15 +605,86 @@ handleGetIndexesRequest(const Wt::Http::ParameterMap& request, Database::Handler Response handleGetArtistsRequest(const Wt::Http::ParameterMap& request, Database::Handler& db) { - Wt::Dbo::Transaction transaction {db.getSession()}; - - // TODO factorize with handleGetIndexesRequest Response response {Response::createOkResponse()}; Response::Node& artistsNode {response.createNode("artists")}; + Response::Node& indexNode {artistsNode.createArrayChild("index")}; + indexNode.setAttribute("name", "?"); + + Wt::Dbo::Transaction transaction {db.getSession()}; + auto artists {Database::Artist::getAll(db.getSession())}; for (const Database::Artist::pointer& artist : artists) - artistsNode.addArrayChild("artist", artistToResponseNode(artist)); + indexNode.addArrayChild("artist", artistToResponseNode(artist)); + + return response; +} + +Response +handleGetStarredRequest(const Wt::Http::ParameterMap& request, Database::Handler& db) +{ + Response response {Response::createOkResponse()}; + response.createArrayNode("starred"); + + return response; +} + +Response +handleGetStarred2Request(const Wt::Http::ParameterMap& request, Database::Handler& db) +{ + Response response {Response::createOkResponse()}; + response.createArrayNode("starred2"); + + return response; +} + +Response +handleGetPlaylistsRequest(const Wt::Http::ParameterMap& request, Database::Handler& db) +{ + Response response {Response::createOkResponse()}; + response.createArrayNode("playlists"); + + return response; +} + +Response +handleGetSongsByGenreRequest(const Wt::Http::ParameterMap& request, Database::Handler& db) +{ + // Mandatory params + auto genre {getParameterAs(request, "genre")}; + if (!genre) + throw Error {Error::Code::RequiredParameterMissing}; + + // Optional params + auto size {getParameterAs(request, "count")}; + if (!size) + size = 10; + + *size = std::min(*size, std::size_t {500}); + + auto offset {getParameterAs(request, "offset")}; + if (!offset) + offset = 0; + + LMS_LOG(API_SUBSONIC, DEBUG) << "genre ='" << *genre << "'"; + + Wt::Dbo::Transaction transaction {db.getSession()}; + + auto clusterType {Database::ClusterType::getByName(db.getSession(), "GENRE")}; + if (!clusterType) + throw Error {Error::Code::RequestedDataNotFound}; + + auto cluster {clusterType->getCluster(*genre)}; + if (!cluster) + throw Error {Error::Code::RequestedDataNotFound}; + + Response response {Response::createOkResponse()}; + Response::Node& songsByGenreNode {response.createNode("songsByGenre")}; + + bool more; + auto tracks {Database::Track::getByFilter(db.getSession(), {cluster.id()}, {}, offset, size, more)}; + for (const Database::Track::pointer& track : tracks) + songsByGenreNode.addArrayChild("song", trackToResponseNode(track)); return response; } @@ -545,6 +702,13 @@ createTranscoder(const Wt::Http::ParameterMap& request, Database::Handler& db) if (!id || id->type != Id::Type::Track) throw Error {"bad id format"}; + // Optional params + auto maxBitRate {getParameterAs(request, "maxBitRate")}; + if (!maxBitRate) + maxBitRate = 128; + + *maxBitRate = clamp(*maxBitRate, std::size_t {48}, std::size_t {320}); + boost::filesystem::path trackPath; { Wt::Dbo::Transaction transaction {db.getSession()}; @@ -561,7 +725,7 @@ createTranscoder(const Wt::Http::ParameterMap& request, Database::Handler& db) Av::TranscodeParameters parameters {}; - parameters.bitrate = 128000; + parameters.bitrate = *maxBitRate * 1000; parameters.encoding = Av::Encoding::MP3; return std::make_shared(trackPath, parameters); @@ -572,6 +736,7 @@ handleStream(const Wt::Http::Request& request, Database::Handler& db, Wt::Http:: { LMS_LOG(API_SUBSONIC, DEBUG) << "STREAM"; + // TODO store only weak ptrs and use a ring container to store shared_ptr? std::shared_ptr transcoder; Wt::Http::ResponseContinuation* continuation {request.continuation()}; @@ -583,7 +748,7 @@ handleStream(const Wt::Http::Request& request, Database::Handler& db, Wt::Http:: } else { - LMS_LOG(UI, DEBUG) << "Continuation! "; + LMS_LOG(UI, DEBUG) << "Continuation!"; transcoder = Wt::cpp17::any_cast>(continuation->data()); } @@ -633,9 +798,11 @@ handleGetCoverArt(const Wt::Http::Request& request, Database::Handler& db, Wt::H throw Error {"bad id format"}; auto size {getParameterAs(request.getParameterMap(), "size")}; - if (!size || *size == 0) + if (!size) size = 256; + *size = clamp(*size, std::size_t {32}, std::size_t {1024}); + std::vector cover; switch (id->type) { diff --git a/src/api/subsonic/SubsonicResponse.cpp b/src/api/subsonic/SubsonicResponse.cpp index fbc830e6..dbd0d900 100644 --- a/src/api/subsonic/SubsonicResponse.cpp +++ b/src/api/subsonic/SubsonicResponse.cpp @@ -22,6 +22,8 @@ #include #include +#include "utils/Exception.hpp" + #define API_VERSION "1.12.0" namespace API::Subsonic @@ -74,37 +76,52 @@ _message {message} { } +void +Response::Node::setValue(const std::string& value) +{ + if (!_children.empty() || !_childrenArrays.empty()) + throw LmsException {"Node already has children"}; + + _value = value; +} + void Response::Node::setAttribute(const std::string& key, const std::string& value) { - attributes[key] = value; + _attributes[key] = value; } void Response::Node::addChild(const std::string& key, Node node) { - children[key].emplace_back(std::move(node)); + if (!_value.empty()) + throw LmsException {"Node already has a value"}; + + _children[key].emplace_back(std::move(node)); } void Response::Node::addArrayChild(const std::string& key, Node node) { - childrenArrays[key].emplace_back(std::move(node)); + if (!_value.empty()) + throw LmsException {"Node already has a value"}; + + _childrenArrays[key].emplace_back(std::move(node)); } Response::Node& Response::Node::createChild(const std::string& key) { - children[key].emplace_back(); - return children[key].back(); + _children[key].emplace_back(); + return _children[key].back(); } Response::Node& Response::Node::createArrayChild(const std::string& key) { - childrenArrays[key].emplace_back(); - return childrenArrays[key].back(); + _childrenArrays[key].emplace_back(); + return _childrenArrays[key].back(); } Response @@ -138,55 +155,71 @@ Response::createFailedResponse(const Error& error) Response::Node& Response::createNode(const std::string& key) { - return _root.children["subsonic-response"].front().createChild(key); + return _root._children["subsonic-response"].front().createChild(key); } -boost::property_tree::ptree -NodeToPropertyTree(const Response::Node& node, ResponseFormat format) +Response::Node& +Response::createArrayNode(const std::string& key) { - boost::property_tree::ptree res; + return _root._children["subsonic-response"].front().createArrayChild(key); +} - for (auto itChildNode : node.children) +void +Response::write(std::ostream& os, ResponseFormat format) +{ + std::function nodeToPropertyTree = [&] (const Response::Node& node, ResponseFormat format) { - for (const Response::Node& childNode : itChildNode.second) - res.add_child(itChildNode.first, NodeToPropertyTree(childNode, format)); - } + boost::property_tree::ptree res; - for (auto itChildArrayNode : node.childrenArrays) - { - const std::vector& childArrayNodes {itChildArrayNode .second}; - - if (format == ResponseFormat::json) + for (auto itAttribute : node._attributes) { - boost::property_tree::ptree array; + std::string key {format == ResponseFormat::xml ? "." : ""}; - for (const Response::Node& childNode : childArrayNodes ) - array.push_back(std::make_pair("", NodeToPropertyTree(childNode, format))); + key += itAttribute.first; - res.add_child(itChildArrayNode.first, array); + res.put(key, itAttribute.second); + } + if (!node._value.empty()) + { + if (format == ResponseFormat::json) + res.put("value", node._value); + else + res.put_value(node._value); } else { - for (const Response::Node& childNode : childArrayNodes ) - res.add_child(itChildArrayNode.first, NodeToPropertyTree(childNode, format)); + for (auto itChildNode : node._children) + { + for (const Response::Node& childNode : itChildNode.second) + res.add_child(itChildNode.first, nodeToPropertyTree(childNode, format)); + } + + for (auto itChildArrayNode : node._childrenArrays) + { + const std::vector& childArrayNodes {itChildArrayNode .second}; + + if (format == ResponseFormat::json) + { + boost::property_tree::ptree array; + + for (const Response::Node& childNode : childArrayNodes ) + array.push_back(std::make_pair("", nodeToPropertyTree(childNode, format))); + + res.add_child(itChildArrayNode.first, array); + } + else + { + for (const Response::Node& childNode : childArrayNodes ) + res.add_child(itChildArrayNode.first, nodeToPropertyTree(childNode, format)); + } + } } - } - for (auto itAttribute : node.attributes) - { - std::string key {format == ResponseFormat::xml ? "." : ""}; + return res; + }; - key += itAttribute.first; - res.put(key, itAttribute.second); - } - - return res; -} - -void responseToStream(const Response& response, ResponseFormat format, std::ostream& os) -{ - boost::property_tree::ptree root {NodeToPropertyTree(response._root, format)}; + boost::property_tree::ptree root {nodeToPropertyTree(_root, format)}; switch (format) { diff --git a/src/api/subsonic/SubsonicResponse.hpp b/src/api/subsonic/SubsonicResponse.hpp index 7154e4a1..4081e493 100644 --- a/src/api/subsonic/SubsonicResponse.hpp +++ b/src/api/subsonic/SubsonicResponse.hpp @@ -60,20 +60,25 @@ class Error class Response { public: - struct Node + class Node { - std::map attributes; - std::map> children; - std::map> childrenArrays; + public: + void setAttribute(const std::string& key, const std::string& value); - // Helpers - void setAttribute(const std::string& key, const std::string& value); + // A Node has either a value or some children + void setValue(const std::string& value); + Node& createChild(const std::string& key); + Node& createArrayChild(const std::string& key); - Node& createChild(const std::string& key); - Node& createArrayChild(const std::string& key); + void addChild(const std::string& key, Node node); + void addArrayChild(const std::string& key, Node node); - void addChild(const std::string& key, Node node); - void addArrayChild(const std::string& key, Node node); + private: + friend class Response; + std::map _attributes; + std::string _value; + std::map> _children; + std::map> _childrenArrays; }; static Response createOkResponse(); @@ -86,9 +91,10 @@ class Response Response& operator=(Response&&) = default; Node& createNode(const std::string& key); + Node& createArrayNode(const std::string& key); + void write(std::ostream& os, ResponseFormat format); private: - friend void responseToStream(const Response& response, ResponseFormat format, std::ostream& os); Response() = default; Node _root; diff --git a/src/database/Artist.cpp b/src/database/Artist.cpp index 2d98966d..81834fa1 100644 --- a/src/database/Artist.cpp +++ b/src/database/Artist.cpp @@ -63,9 +63,13 @@ Artist::create(Wt::Dbo::Session& session, const std::string& name, const std::st } std::vector -Artist::getAll(Wt::Dbo::Session& session, int offset, int size) +Artist::getAll(Wt::Dbo::Session& session, boost::optional offset, boost::optional size) { - Wt::Dbo::collection res = session.find().offset(offset).limit(size); + Wt::Dbo::collection res = session.find() + .offset(offset ? static_cast(*offset) : -1) + .limit(size ? static_cast(*size) : -1) + .orderBy("name COLLATE NOCASE"); + return std::vector(res.begin(), res.end()); } @@ -123,15 +127,17 @@ std::vector Artist::getByFilter(Wt::Dbo::Session& session, const std::set& clusters, const std::vector keywords, - int offset, int size, bool& moreResults) + boost::optional offset, + boost::optional size, + bool& moreResults) { Wt::Dbo::collection collection = getQuery(session, clusters, keywords) - .limit(size != -1 ? size + 1 : -1) - .offset(offset); + .limit(size ? static_cast(*size) + 1 : -1) + .offset(offset ? static_cast(*offset) : -1); - auto res = std::vector(collection.begin(), collection.end()); + auto res {std::vector(collection.begin(), collection.end())}; - if (size != -1 && res.size() == static_cast(size) + 1) + if (size && res.size() == static_cast(*size) + 1) { moreResults = true; res.pop_back(); @@ -143,13 +149,13 @@ Artist::getByFilter(Wt::Dbo::Session& session, } std::vector -Artist::getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, int limit) +Artist::getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, boost::optional limit) { Wt::Dbo::collection res = session.query("SELECT a from artist a INNER JOIN track_artist t_a ON t_a.artist_id = a.id INNER JOIN track t ON t.id = t_a.track_id") .where("t.file_added > ?").bind(after) .groupBy("a.id") .orderBy("t.file_added DESC") - .limit(limit); + .limit(limit ? static_cast(*limit) : -1); return std::vector(res.begin(), res.end()); } diff --git a/src/database/Artist.hpp b/src/database/Artist.hpp index 9f73635e..c415e15a 100644 --- a/src/database/Artist.hpp +++ b/src/database/Artist.hpp @@ -22,6 +22,8 @@ #include #include +#include + #include #include @@ -51,13 +53,13 @@ class Artist : public Wt::Dbo::Dbo static std::vector getByFilter(Wt::Dbo::Session& session, const std::set& clusters, // at least one track that belongs to these clusters const std::vector keywords, // name must match all of these keywords - int offset, - int size, + boost::optional offset, + boost::optional size, bool& moreExpected); - static std::vector getAll(Wt::Dbo::Session& session, int offset = -1, int size = -1); + static std::vector getAll(Wt::Dbo::Session& session, boost::optional offset = {}, boost::optional size = {}); static std::vector getAllOrphans(Wt::Dbo::Session& session); // No track related - static std::vector getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, int size = 1); + static std::vector getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, boost::optional size = {}); // Accessors const std::string& getName(void) const { return _name; } diff --git a/src/database/Release.cpp b/src/database/Release.cpp index 60060479..eac09554 100644 --- a/src/database/Release.cpp +++ b/src/database/Release.cpp @@ -73,7 +73,8 @@ Release::getAll(Wt::Dbo::Session& session, boost::optional offset, { Wt::Dbo::collection res = session.find() .offset(offset ? static_cast(*offset) : -1) - .limit(size ? static_cast(*size) : - 1); + .limit(size ? static_cast(*size) : - 1) + .orderBy("name COLLATE NOCASE"); return std::vector(res.begin(), res.end()); } @@ -150,19 +151,28 @@ getQuery(Wt::Dbo::Session& session, return query; } +std::vector +Release::getByFilter(Wt::Dbo::Session& session, const std::set& clusterIds) +{ + bool moreResults; + return getByFilter(session, clusterIds, {}, {}, {}, moreResults); +} + std::vector Release::getByFilter(Wt::Dbo::Session& session, const std::set& clusterIds, const std::vector keywords, - int offset, int size, bool& moreResults) + boost::optional offset, + boost::optional size, + bool& moreResults) { Wt::Dbo::collection collection = getQuery(session, clusterIds, keywords) - .limit(size != -1 ? size + 1 : -1) - .offset(offset); + .limit(size ? static_cast(*size) + 1 : -1) + .offset(offset ? static_cast(*offset) : -1); - auto res = std::vector(collection.begin(), collection.end()); + auto res {std::vector(collection.begin(), collection.end())}; - if (size != -1 && res.size() == static_cast(size) + 1) + if (size && res.size() == static_cast(*size) + 1) { moreResults = true; res.pop_back(); diff --git a/src/database/Release.hpp b/src/database/Release.hpp index 588612ec..10497eff 100644 --- a/src/database/Release.hpp +++ b/src/database/Release.hpp @@ -53,11 +53,12 @@ class Release : public Wt::Dbo::Dbo static std::vector getAllRandom(Wt::Dbo::Session& session, boost::optional size = {}); static std::vector getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, boost::optional offset = {}, boost::optional size = {}); + static std::vector getByFilter(Wt::Dbo::Session& session, const std::set& clusters); static std::vector getByFilter(Wt::Dbo::Session& session, const std::set& clusters, // at least one track that belongs to these clusters const std::vector keywords, // name must match all of these keywords - int offset, - int size, + boost::optional offset, + boost::optional size, bool& moreExpected); std::vector> getTracks(const std::set& clusters = std::set()) const; diff --git a/src/database/Track.cpp b/src/database/Track.cpp index 4a14d2c8..738fc71f 100644 --- a/src/database/Track.cpp +++ b/src/database/Track.cpp @@ -202,15 +202,17 @@ std::vector Track::getByFilter(Wt::Dbo::Session& session, const std::set& clusterIds, const std::vector keywords, - int offset, int size, bool& moreResults) + boost::optional offset, + boost::optional size, + bool& moreResults) { Wt::Dbo::collection collection = getQuery(session, clusterIds, keywords) - .limit(size != -1 ? size + 1 : -1) - .offset(offset); + .limit(size ? static_cast(*size) + 1 : -1) + .offset(offset ? static_cast(*offset) : -1); auto res = std::vector(collection.begin(), collection.end()); - if (size != -1 && res.size() == static_cast(size) + 1) + if (size && res.size() == static_cast(*size) + 1) { moreResults = true; res.pop_back(); @@ -227,7 +229,7 @@ Track::getByFilter(Wt::Dbo::Session& session, { bool moreResults; - return getByFilter(session, clusters, std::vector(), -1, -1, moreResults); + return getByFilter(session, clusters, std::vector{}, -1, -1, moreResults); } void diff --git a/src/database/Track.hpp b/src/database/Track.hpp index c272c537..e6026802 100644 --- a/src/database/Track.hpp +++ b/src/database/Track.hpp @@ -59,8 +59,8 @@ class Track : public Wt::Dbo::Dbo static std::vector getByFilter(Wt::Dbo::Session& session, const std::set& clusters, // tracks that belong to these clusters const std::vector keywords, // name must match all of these keywords - int offset, - int size, + boost::optional offset, + boost::optional size, bool& moreExpected); static Wt::Dbo::collection< pointer > getAll(Wt::Dbo::Session& session, boost::optional limit = {}); diff --git a/src/utils/Utils.hpp b/src/utils/Utils.hpp index fa148c9b..7f40d51a 100644 --- a/src/utils/Utils.hpp +++ b/src/utils/Utils.hpp @@ -94,4 +94,11 @@ void uniqueAndSortedByOccurence(In first, In last, Out out) *out++ = occurence.elem; } +template> +constexpr const T& clamp( T v, T lo, T hi, Compare comp = {}) +{ + assert(!comp(hi, lo)); + return comp(v, lo) ? lo : comp(hi, v) ? hi : v; +} +