Subsonic API: various fixes to make Sublime Music happy
This commit is contained in:
@@ -496,12 +496,25 @@ std::chrono::milliseconds
|
|||||||
Release::getDuration() const
|
Release::getDuration() const
|
||||||
{
|
{
|
||||||
assert(self());
|
assert(self());
|
||||||
assert(self()->id() != Wt::Dbo::dbo_traits<Artist>::invalidId() );
|
assert(self()->id() != Wt::Dbo::dbo_traits<Artist>::invalidId());
|
||||||
assert(session());
|
assert(session());
|
||||||
|
|
||||||
using milli = std::chrono::duration<int, std::milli>;
|
using milli = std::chrono::duration<int, std::milli>;
|
||||||
|
|
||||||
Wt::Dbo::Query<milli> query {session()->query<milli>("SELECT SUM(duration) FROM track t INNER JOIN release r ON t.release_id = r.id")
|
Wt::Dbo::Query<milli> query {session()->query<milli>("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN release r ON t.release_id = r.id")
|
||||||
|
.where("r.id = ?").bind(self()->id())};
|
||||||
|
|
||||||
|
return query.resultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::WDateTime
|
||||||
|
Release::getLastWritten() const
|
||||||
|
{
|
||||||
|
assert(self());
|
||||||
|
assert(self()->id() != Wt::Dbo::dbo_traits<Artist>::invalidId());
|
||||||
|
assert(session());
|
||||||
|
|
||||||
|
Wt::Dbo::Query<Wt::WDateTime> query {session()->query<Wt::WDateTime>("SELECT COALESCE(MAX(file_last_write), '1970-01-01T00:00:00') FROM track t INNER JOIN release r ON t.release_id = r.id")
|
||||||
.where("r.id = ?").bind(self()->id())};
|
.where("r.id = ?").bind(self()->id())};
|
||||||
|
|
||||||
return query.resultValue();
|
return query.resultValue();
|
||||||
|
|||||||
@@ -410,7 +410,7 @@ TrackList::getDuration() const
|
|||||||
|
|
||||||
using milli = std::chrono::duration<int, std::milli>;
|
using milli = std::chrono::duration<int, std::milli>;
|
||||||
|
|
||||||
Wt::Dbo::Query<milli> query {session()->query<milli>("SELECT SUM(duration) FROM track t INNER JOIN tracklist_entry p_e ON t.id = p_e.track_id")
|
Wt::Dbo::Query<milli> query {session()->query<milli>("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN tracklist_entry p_e ON t.id = p_e.track_id")
|
||||||
.where("p_e.tracklist_id = ?").bind(self()->id())};
|
.where("p_e.tracklist_id = ?").bind(self()->id())};
|
||||||
|
|
||||||
return query.resultValue();
|
return query.resultValue();
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ class Release : public Wt::Dbo::Dbo<Release>
|
|||||||
std::optional<std::size_t> getTotalTrack() const;
|
std::optional<std::size_t> getTotalTrack() const;
|
||||||
std::optional<std::size_t> getTotalDisc() const;
|
std::optional<std::size_t> getTotalDisc() const;
|
||||||
std::chrono::milliseconds getDuration() const;
|
std::chrono::milliseconds getDuration() const;
|
||||||
|
Wt::WDateTime getLastWritten() const;
|
||||||
|
|
||||||
// Get the artists of this release
|
// Get the artists of this release
|
||||||
std::vector<Wt::Dbo::ptr<Artist> > getArtists(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const;
|
std::vector<Wt::Dbo::ptr<Artist> > getArtists(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ namespace API::Subsonic::Scan
|
|||||||
|
|
||||||
const IMediaScanner::Status scanStatus {ServiceProvider<IMediaScanner>::get()->getStatus()};
|
const IMediaScanner::Status scanStatus {ServiceProvider<IMediaScanner>::get()->getStatus()};
|
||||||
|
|
||||||
statusResponse.setAttribute("scanning", scanStatus.currentState == IMediaScanner::State::InProgress ? "true" : "false");
|
statusResponse.setAttribute("scanning", scanStatus.currentState == IMediaScanner::State::InProgress);
|
||||||
if (scanStatus.currentState == IMediaScanner::State::InProgress && scanStatus.inProgressScanStats)
|
if (scanStatus.currentState == IMediaScanner::State::InProgress && scanStatus.inProgressScanStats)
|
||||||
statusResponse.setAttribute("count", scanStatus.inProgressScanStats->processedFiles);
|
statusResponse.setAttribute("count", scanStatus.inProgressScanStats->processedFiles);
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ struct StreamParameters
|
|||||||
std::optional<Av::TranscodeParameters> transcodeParameters;
|
std::optional<Av::TranscodeParameters> transcodeParameters;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
static
|
static
|
||||||
StreamParameters
|
StreamParameters
|
||||||
getStreamParameters(RequestContext& context)
|
getStreamParameters(RequestContext& context)
|
||||||
@@ -110,7 +109,44 @@ getStreamParameters(RequestContext& context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
handle(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response)
|
handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||||
|
{
|
||||||
|
std::shared_ptr<IResourceHandler> resourceHandler;
|
||||||
|
|
||||||
|
Wt::Http::ResponseContinuation *continuation = request.continuation();
|
||||||
|
if (!continuation)
|
||||||
|
{
|
||||||
|
// Mandatory params
|
||||||
|
Id id {getMandatoryParameterAs<Id>(context.parameters, "id")};
|
||||||
|
|
||||||
|
std::filesystem::path trackPath;
|
||||||
|
{
|
||||||
|
auto transaction {context.dbSession.createSharedTransaction()};
|
||||||
|
|
||||||
|
auto track {Track::getById(context.dbSession, id.value)};
|
||||||
|
if (!track)
|
||||||
|
throw RequestedDataNotFoundError {};
|
||||||
|
|
||||||
|
trackPath = track->getPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceHandler = createFileResourceHandler(trackPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceHandler->processRequest(request, response);
|
||||||
|
if (!resourceHandler->isFinished())
|
||||||
|
{
|
||||||
|
Wt::Http::ResponseContinuation *continuation = response.createContinuation();
|
||||||
|
continuation->setData(resourceHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||||
{
|
{
|
||||||
std::shared_ptr<IResourceHandler> resourceHandler;
|
std::shared_ptr<IResourceHandler> resourceHandler;
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
namespace API::Subsonic::Stream
|
namespace API::Subsonic::Stream
|
||||||
{
|
{
|
||||||
void handle(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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include "subsonic/SubsonicResource.hpp"
|
#include "subsonic/SubsonicResource.hpp"
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <iomanip>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
#include <Wt/WLocalDateTime.h>
|
#include <Wt/WLocalDateTime.h>
|
||||||
@@ -50,8 +51,7 @@ using namespace Database;
|
|||||||
|
|
||||||
static const std::string genreClusterName {"GENRE"};
|
static const std::string genreClusterName {"GENRE"};
|
||||||
static const std::string reportedStarredDate {"2000-01-01T00:00:00"};
|
static const std::string reportedStarredDate {"2000-01-01T00:00:00"};
|
||||||
static const std::string reportedCreatedBookmarkDate {"2000-01-01T00:00:00"};
|
static const std::string reportedDummyDate {"2000-01-01T00:00:00"};
|
||||||
static const std::string reportedChangedBookmarkDate {"2000-01-01T00:00:00"};
|
|
||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
@@ -287,7 +287,7 @@ trackToResponseNode(const Track::pointer& track, Session& dbSession, const User:
|
|||||||
Response::Node trackResponse;
|
Response::Node trackResponse;
|
||||||
|
|
||||||
trackResponse.setAttribute("id", IdToString({Id::Type::Track, track.id()}));
|
trackResponse.setAttribute("id", IdToString({Id::Type::Track, track.id()}));
|
||||||
trackResponse.setAttribute("isDir", "false");
|
trackResponse.setAttribute("isDir", false);
|
||||||
trackResponse.setAttribute("title", track->getName());
|
trackResponse.setAttribute("title", track->getName());
|
||||||
if (track->getTrackNumber())
|
if (track->getTrackNumber())
|
||||||
trackResponse.setAttribute("track", *track->getTrackNumber());
|
trackResponse.setAttribute("track", *track->getTrackNumber());
|
||||||
@@ -358,8 +358,8 @@ trackBookmarkToResponseNode(const TrackBookmark::pointer& trackBookmark)
|
|||||||
trackBookmarkNode.setAttribute("position", trackBookmark->getOffset().count());
|
trackBookmarkNode.setAttribute("position", trackBookmark->getOffset().count());
|
||||||
if (!trackBookmark->getComment().empty())
|
if (!trackBookmark->getComment().empty())
|
||||||
trackBookmarkNode.setAttribute("comment", trackBookmark->getComment());
|
trackBookmarkNode.setAttribute("comment", trackBookmark->getComment());
|
||||||
trackBookmarkNode.setAttribute("created", reportedCreatedBookmarkDate);
|
trackBookmarkNode.setAttribute("created", reportedDummyDate);
|
||||||
trackBookmarkNode.setAttribute("changed", reportedChangedBookmarkDate);
|
trackBookmarkNode.setAttribute("changed", reportedDummyDate);
|
||||||
trackBookmarkNode.setAttribute("username", trackBookmark->getUser()->getLoginName());
|
trackBookmarkNode.setAttribute("username", trackBookmark->getUser()->getLoginName());
|
||||||
|
|
||||||
return trackBookmarkNode;
|
return trackBookmarkNode;
|
||||||
@@ -380,7 +380,13 @@ releaseToResponseNode(const Release::pointer& release, Session& dbSession, const
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
albumNode.setAttribute("title", release->getName());
|
albumNode.setAttribute("title", release->getName());
|
||||||
albumNode.setAttribute("isDir", "true");
|
albumNode.setAttribute("isDir", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::time_t t {release->getLastWritten().toTime_t()};
|
||||||
|
std::ostringstream oss; oss << std::put_time(std::gmtime(&t), "%FT%T");
|
||||||
|
albumNode.setAttribute("created", oss.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
albumNode.setAttribute("id", IdToString({Id::Type::Release, release.id()}));
|
albumNode.setAttribute("id", IdToString({Id::Type::Release, release.id()}));
|
||||||
@@ -471,18 +477,18 @@ userToResponseNode(const User::pointer& user)
|
|||||||
Response::Node userNode;
|
Response::Node userNode;
|
||||||
|
|
||||||
userNode.setAttribute("username", user->getLoginName());
|
userNode.setAttribute("username", user->getLoginName());
|
||||||
userNode.setAttribute("scrobblingEnabled", "false");
|
userNode.setAttribute("scrobblingEnabled", false);
|
||||||
userNode.setAttribute("adminRole", user->isAdmin() ? "true" : "false");
|
userNode.setAttribute("adminRole", user->isAdmin());
|
||||||
userNode.setAttribute("settingsRole", "true");
|
userNode.setAttribute("settingsRole", true);
|
||||||
userNode.setAttribute("downloadRole", "false");
|
userNode.setAttribute("downloadRole", true);
|
||||||
userNode.setAttribute("uploadRole", "false");
|
userNode.setAttribute("uploadRole", false);
|
||||||
userNode.setAttribute("playlistRole", "true");
|
userNode.setAttribute("playlistRole", true);
|
||||||
userNode.setAttribute("coverArtRole", "false");
|
userNode.setAttribute("coverArtRole", false);
|
||||||
userNode.setAttribute("commentRole", "false");
|
userNode.setAttribute("commentRole", false);
|
||||||
userNode.setAttribute("podcastRole", "false");
|
userNode.setAttribute("podcastRole", false);
|
||||||
userNode.setAttribute("streamRole", "true");
|
userNode.setAttribute("streamRole", true);
|
||||||
userNode.setAttribute("jukeboxRole", "false");
|
userNode.setAttribute("jukeboxRole", false);
|
||||||
userNode.setAttribute("shareRole", "false");
|
userNode.setAttribute("shareRole", false);
|
||||||
|
|
||||||
Response::Node folder;
|
Response::Node folder;
|
||||||
folder.setValue("0");
|
folder.setValue("0");
|
||||||
@@ -661,7 +667,7 @@ handleGetLicenseRequest(RequestContext& context)
|
|||||||
Response::Node& licenseNode {response.createNode("license")};
|
Response::Node& licenseNode {response.createNode("license")};
|
||||||
licenseNode.setAttribute("licenseExpires", "2025-09-03T14:46:43");
|
licenseNode.setAttribute("licenseExpires", "2025-09-03T14:46:43");
|
||||||
licenseNode.setAttribute("email", "foo@bar.com");
|
licenseNode.setAttribute("email", "foo@bar.com");
|
||||||
licenseNode.setAttribute("valid", "true");
|
licenseNode.setAttribute("valid", true);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -1212,8 +1218,8 @@ tracklistToResponseNode(const TrackList::pointer& tracklist, Session&)
|
|||||||
playlistNode.setAttribute("name", tracklist->getName());
|
playlistNode.setAttribute("name", tracklist->getName());
|
||||||
playlistNode.setAttribute("songCount", tracklist->getCount());
|
playlistNode.setAttribute("songCount", tracklist->getCount());
|
||||||
playlistNode.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(tracklist->getDuration()).count());
|
playlistNode.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(tracklist->getDuration()).count());
|
||||||
playlistNode.setAttribute("public", tracklist->isPublic() ? "true" : "false");
|
playlistNode.setAttribute("public", tracklist->isPublic());
|
||||||
playlistNode.setAttribute("created", "");
|
playlistNode.setAttribute("created", reportedDummyDate);
|
||||||
playlistNode.setAttribute("owner", tracklist->getUser()->getLoginName());
|
playlistNode.setAttribute("owner", tracklist->getUser()->getLoginName());
|
||||||
|
|
||||||
return playlistNode;
|
return playlistNode;
|
||||||
@@ -1807,7 +1813,6 @@ static std::unordered_map<std::string, RequestEntryPointInfo> requestEntryPoints
|
|||||||
{"deletePlaylist", {handleDeletePlaylistRequest, false}},
|
{"deletePlaylist", {handleDeletePlaylistRequest, false}},
|
||||||
|
|
||||||
// Media retrieval
|
// Media retrieval
|
||||||
{"download", {handleNotImplemented, false}},
|
|
||||||
{"hls", {handleNotImplemented, false}},
|
{"hls", {handleNotImplemented, false}},
|
||||||
{"getCaptions", {handleNotImplemented, false}},
|
{"getCaptions", {handleNotImplemented, false}},
|
||||||
{"getLyrics", {handleNotImplemented, false}},
|
{"getLyrics", {handleNotImplemented, false}},
|
||||||
@@ -1871,8 +1876,9 @@ using MediaRetrievalHandlerFunc = std::function<void(RequestContext&, const Wt::
|
|||||||
static std::unordered_map<std::string, MediaRetrievalHandlerFunc> mediaRetrievalHandlers
|
static std::unordered_map<std::string, MediaRetrievalHandlerFunc> mediaRetrievalHandlers
|
||||||
{
|
{
|
||||||
// Media retrieval
|
// Media retrieval
|
||||||
|
{"download", Stream::handleDownload},
|
||||||
|
{"stream", Stream::handleStream},
|
||||||
{"getCoverArt", handleGetCoverArt},
|
{"getCoverArt", handleGetCoverArt},
|
||||||
{"stream", Stream::handle},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|||||||
@@ -69,16 +69,10 @@ 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
|
|
||||||
Response::Node::setAttribute(std::string_view key, long long value)
|
|
||||||
{
|
|
||||||
_attributes[std::string {key}] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
Response::Node::addChild(const std::string& key, Node node)
|
Response::Node::addChild(const std::string& key, Node node)
|
||||||
{
|
{
|
||||||
if (_value.valueless_by_exception())
|
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));
|
||||||
@@ -87,7 +81,7 @@ Response::Node::addChild(const std::string& key, Node node)
|
|||||||
void
|
void
|
||||||
Response::Node::addArrayChild(const std::string& key, Node node)
|
Response::Node::addArrayChild(const std::string& key, Node node)
|
||||||
{
|
{
|
||||||
if (_value.valueless_by_exception())
|
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));
|
||||||
@@ -179,16 +173,22 @@ Response::writeXML(std::ostream& os)
|
|||||||
{
|
{
|
||||||
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))
|
||||||
|
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.valueless_by_exception())
|
if (node._value)
|
||||||
{
|
{
|
||||||
if (std::holds_alternative<std::string>(node._value))
|
const auto& value {*node._value};
|
||||||
res.put_value(std::get<std::string>(node._value));
|
|
||||||
else if (std::holds_alternative<long long>(node._value))
|
if (std::holds_alternative<std::string>(value))
|
||||||
res.put_value(std::get<long long>(node._value));
|
res.put_value(std::get<std::string>(value));
|
||||||
|
else if (std::holds_alternative<bool>(value))
|
||||||
|
res.put_value(std::get<bool>(value));
|
||||||
|
else if (std::holds_alternative<long long>(value))
|
||||||
|
res.put_value(std::get<long long>(value));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -200,7 +200,7 @@ Response::writeXML(std::ostream& os)
|
|||||||
|
|
||||||
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));
|
||||||
@@ -217,9 +217,11 @@ Response::writeXML(std::ostream& os)
|
|||||||
unsigned
|
unsigned
|
||||||
Response::getAPIMinorVersion(std::string_view clientName)
|
Response::getAPIMinorVersion(std::string_view clientName)
|
||||||
{
|
{
|
||||||
// Audinaut does not lower its client API version in plain text authentication mode
|
// Some clients do not rely on version to enable the clear text password auth scheme
|
||||||
if (clientName == "Audinaut")
|
if (clientName == "Audinaut")
|
||||||
return 13;
|
return 16;
|
||||||
|
else if (clientName == "Sublime Music")
|
||||||
|
return 16;
|
||||||
else
|
else
|
||||||
return 12;
|
return 12;
|
||||||
}
|
}
|
||||||
@@ -233,20 +235,24 @@ Response::writeJSON(std::ostream& os)
|
|||||||
{
|
{
|
||||||
Json::Object res;
|
Json::Object res;
|
||||||
|
|
||||||
for (auto itAttribute : node._attributes)
|
auto valueToJsonValue {[](const Node::Value& value) -> Json::Value
|
||||||
{
|
{
|
||||||
if (std::holds_alternative<std::string>(itAttribute.second))
|
if (std::holds_alternative<std::string>(value))
|
||||||
res[itAttribute.first] = Json::Value {std::get<std::string>(itAttribute.second)};
|
return Json::Value {std::get<std::string>(value)};
|
||||||
else if (std::holds_alternative<long long>(itAttribute.second))
|
else if (std::holds_alternative<bool>(value))
|
||||||
res[itAttribute.first] = Json::Value {std::get<long long>(itAttribute.second)};
|
return Json::Value {std::get<bool>(value)};
|
||||||
}
|
else if (std::holds_alternative<long long>(value))
|
||||||
|
return Json::Value {std::get<long long>(value)};
|
||||||
|
|
||||||
if (node._value.valueless_by_exception())
|
throw LmsException("Unexpected value type");
|
||||||
|
}};
|
||||||
|
|
||||||
|
for (auto itAttribute : node._attributes)
|
||||||
|
res[itAttribute.first] = valueToJsonValue(itAttribute.second);
|
||||||
|
|
||||||
|
if (node._value)
|
||||||
{
|
{
|
||||||
if (std::holds_alternative<std::string>(node._value))
|
res["value"] = valueToJsonValue(*node._value);
|
||||||
res["value"] = Json::Value {std::get<std::string>(node._value)};
|
|
||||||
else if (std::holds_alternative<long long>(node._value))
|
|
||||||
res["value"] = Json::Value {std::get<long long>(node._value)};
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <variant>
|
#include <variant>
|
||||||
@@ -180,7 +181,15 @@ class Response
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void setAttribute(std::string_view key, std::string_view value);
|
void setAttribute(std::string_view key, std::string_view value);
|
||||||
void setAttribute(std::string_view key, long long value);
|
|
||||||
|
template <typename T, std::enable_if_t<std::is_arithmetic<T>::value>* = nullptr>
|
||||||
|
void setAttribute(std::string_view key, T value)
|
||||||
|
{
|
||||||
|
if constexpr (std::is_same<bool, T>::value)
|
||||||
|
_attributes[std::string {key}] = value;
|
||||||
|
else
|
||||||
|
_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);
|
||||||
@@ -193,8 +202,9 @@ class Response
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
friend class Response;
|
friend class Response;
|
||||||
std::map<std::string, std::variant<std::string, long long>> _attributes;
|
using Value = std::variant<std::string, bool, long long>;
|
||||||
std::variant<std::string, long long> _value;
|
std::map<std::string, Value> _attributes;
|
||||||
|
std::optional<Value> _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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -210,6 +210,8 @@ testSingleRelease(Session& session)
|
|||||||
releases = Release::getAll(session);
|
releases = Release::getAll(session);
|
||||||
CHECK(releases.size() == 1);
|
CHECK(releases.size() == 1);
|
||||||
CHECK(releases.front().id() == release.getId());
|
CHECK(releases.front().id() == release.getId());
|
||||||
|
|
||||||
|
CHECK(release->getDuration() == std::chrono::seconds {0});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -711,6 +713,12 @@ testMultipleTracksMultipleClustersTopRelease(Session& session)
|
|||||||
ScopedUser user {session, "MyUser"};
|
ScopedUser user {session, "MyUser"};
|
||||||
ScopedTrackList trackList {session, "TrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
ScopedTrackList trackList {session, "TrackList", TrackList::Type::Playlist, false, user.lockAndGet()};
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
CHECK(trackList->getDuration() == std::chrono::seconds {0});
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user