Subsonic API: various fixes to make Sublime Music happy
This commit is contained in:
@@ -496,12 +496,25 @@ std::chrono::milliseconds
|
||||
Release::getDuration() const
|
||||
{
|
||||
assert(self());
|
||||
assert(self()->id() != Wt::Dbo::dbo_traits<Artist>::invalidId() );
|
||||
assert(self()->id() != Wt::Dbo::dbo_traits<Artist>::invalidId());
|
||||
assert(session());
|
||||
|
||||
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())};
|
||||
|
||||
return query.resultValue();
|
||||
|
||||
@@ -410,7 +410,7 @@ TrackList::getDuration() const
|
||||
|
||||
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())};
|
||||
|
||||
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> getTotalDisc() const;
|
||||
std::chrono::milliseconds getDuration() const;
|
||||
Wt::WDateTime getLastWritten() const;
|
||||
|
||||
// Get the artists of this release
|
||||
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()};
|
||||
|
||||
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)
|
||||
statusResponse.setAttribute("count", scanStatus.inProgressScanStats->processedFiles);
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ struct StreamParameters
|
||||
std::optional<Av::TranscodeParameters> transcodeParameters;
|
||||
};
|
||||
|
||||
|
||||
static
|
||||
StreamParameters
|
||||
getStreamParameters(RequestContext& context)
|
||||
@@ -110,7 +109,44 @@ getStreamParameters(RequestContext& context)
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
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 <atomic>
|
||||
#include <iomanip>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <Wt/WLocalDateTime.h>
|
||||
@@ -50,8 +51,7 @@ using namespace Database;
|
||||
|
||||
static const std::string genreClusterName {"GENRE"};
|
||||
static const std::string reportedStarredDate {"2000-01-01T00:00:00"};
|
||||
static const std::string reportedCreatedBookmarkDate {"2000-01-01T00:00:00"};
|
||||
static const std::string reportedChangedBookmarkDate {"2000-01-01T00:00:00"};
|
||||
static const std::string reportedDummyDate {"2000-01-01T00:00:00"};
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
@@ -287,7 +287,7 @@ trackToResponseNode(const Track::pointer& track, Session& dbSession, const User:
|
||||
Response::Node trackResponse;
|
||||
|
||||
trackResponse.setAttribute("id", IdToString({Id::Type::Track, track.id()}));
|
||||
trackResponse.setAttribute("isDir", "false");
|
||||
trackResponse.setAttribute("isDir", false);
|
||||
trackResponse.setAttribute("title", track->getName());
|
||||
if (track->getTrackNumber())
|
||||
trackResponse.setAttribute("track", *track->getTrackNumber());
|
||||
@@ -358,8 +358,8 @@ trackBookmarkToResponseNode(const TrackBookmark::pointer& trackBookmark)
|
||||
trackBookmarkNode.setAttribute("position", trackBookmark->getOffset().count());
|
||||
if (!trackBookmark->getComment().empty())
|
||||
trackBookmarkNode.setAttribute("comment", trackBookmark->getComment());
|
||||
trackBookmarkNode.setAttribute("created", reportedCreatedBookmarkDate);
|
||||
trackBookmarkNode.setAttribute("changed", reportedChangedBookmarkDate);
|
||||
trackBookmarkNode.setAttribute("created", reportedDummyDate);
|
||||
trackBookmarkNode.setAttribute("changed", reportedDummyDate);
|
||||
trackBookmarkNode.setAttribute("username", trackBookmark->getUser()->getLoginName());
|
||||
|
||||
return trackBookmarkNode;
|
||||
@@ -380,7 +380,13 @@ releaseToResponseNode(const Release::pointer& release, Session& dbSession, const
|
||||
else
|
||||
{
|
||||
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()}));
|
||||
@@ -471,18 +477,18 @@ userToResponseNode(const User::pointer& user)
|
||||
Response::Node userNode;
|
||||
|
||||
userNode.setAttribute("username", user->getLoginName());
|
||||
userNode.setAttribute("scrobblingEnabled", "false");
|
||||
userNode.setAttribute("adminRole", user->isAdmin() ? "true" : "false");
|
||||
userNode.setAttribute("settingsRole", "true");
|
||||
userNode.setAttribute("downloadRole", "false");
|
||||
userNode.setAttribute("uploadRole", "false");
|
||||
userNode.setAttribute("playlistRole", "true");
|
||||
userNode.setAttribute("coverArtRole", "false");
|
||||
userNode.setAttribute("commentRole", "false");
|
||||
userNode.setAttribute("podcastRole", "false");
|
||||
userNode.setAttribute("streamRole", "true");
|
||||
userNode.setAttribute("jukeboxRole", "false");
|
||||
userNode.setAttribute("shareRole", "false");
|
||||
userNode.setAttribute("scrobblingEnabled", false);
|
||||
userNode.setAttribute("adminRole", user->isAdmin());
|
||||
userNode.setAttribute("settingsRole", true);
|
||||
userNode.setAttribute("downloadRole", true);
|
||||
userNode.setAttribute("uploadRole", false);
|
||||
userNode.setAttribute("playlistRole", true);
|
||||
userNode.setAttribute("coverArtRole", false);
|
||||
userNode.setAttribute("commentRole", false);
|
||||
userNode.setAttribute("podcastRole", false);
|
||||
userNode.setAttribute("streamRole", true);
|
||||
userNode.setAttribute("jukeboxRole", false);
|
||||
userNode.setAttribute("shareRole", false);
|
||||
|
||||
Response::Node folder;
|
||||
folder.setValue("0");
|
||||
@@ -661,7 +667,7 @@ handleGetLicenseRequest(RequestContext& context)
|
||||
Response::Node& licenseNode {response.createNode("license")};
|
||||
licenseNode.setAttribute("licenseExpires", "2025-09-03T14:46:43");
|
||||
licenseNode.setAttribute("email", "foo@bar.com");
|
||||
licenseNode.setAttribute("valid", "true");
|
||||
licenseNode.setAttribute("valid", true);
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -1212,8 +1218,8 @@ tracklistToResponseNode(const TrackList::pointer& tracklist, Session&)
|
||||
playlistNode.setAttribute("name", tracklist->getName());
|
||||
playlistNode.setAttribute("songCount", tracklist->getCount());
|
||||
playlistNode.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(tracklist->getDuration()).count());
|
||||
playlistNode.setAttribute("public", tracklist->isPublic() ? "true" : "false");
|
||||
playlistNode.setAttribute("created", "");
|
||||
playlistNode.setAttribute("public", tracklist->isPublic());
|
||||
playlistNode.setAttribute("created", reportedDummyDate);
|
||||
playlistNode.setAttribute("owner", tracklist->getUser()->getLoginName());
|
||||
|
||||
return playlistNode;
|
||||
@@ -1807,8 +1813,7 @@ static std::unordered_map<std::string, RequestEntryPointInfo> requestEntryPoints
|
||||
{"deletePlaylist", {handleDeletePlaylistRequest, false}},
|
||||
|
||||
// Media retrieval
|
||||
{"download", {handleNotImplemented, false}},
|
||||
{"hls", {handleNotImplemented, false}},
|
||||
{"hls", {handleNotImplemented, false}},
|
||||
{"getCaptions", {handleNotImplemented, false}},
|
||||
{"getLyrics", {handleNotImplemented, false}},
|
||||
{"getAvatar", {handleNotImplemented, false}},
|
||||
@@ -1871,8 +1876,9 @@ using MediaRetrievalHandlerFunc = std::function<void(RequestContext&, const Wt::
|
||||
static std::unordered_map<std::string, MediaRetrievalHandlerFunc> mediaRetrievalHandlers
|
||||
{
|
||||
// Media retrieval
|
||||
{"download", Stream::handleDownload},
|
||||
{"stream", Stream::handleStream},
|
||||
{"getCoverArt", handleGetCoverArt},
|
||||
{"stream", Stream::handle},
|
||||
};
|
||||
|
||||
void
|
||||
|
||||
@@ -69,16 +69,10 @@ Response::Node::setAttribute(std::string_view key, std::string_view 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
|
||||
Response::Node::addChild(const std::string& key, Node node)
|
||||
{
|
||||
if (_value.valueless_by_exception())
|
||||
if (_value)
|
||||
throw LmsException {"Node already has a value"};
|
||||
|
||||
_children[key].emplace_back(std::move(node));
|
||||
@@ -87,7 +81,7 @@ Response::Node::addChild(const std::string& key, Node node)
|
||||
void
|
||||
Response::Node::addArrayChild(const std::string& key, Node node)
|
||||
{
|
||||
if (_value.valueless_by_exception())
|
||||
if (_value)
|
||||
throw LmsException {"Node already has a value"};
|
||||
|
||||
_childrenArrays[key].emplace_back(std::move(node));
|
||||
@@ -179,16 +173,22 @@ Response::writeXML(std::ostream& os)
|
||||
{
|
||||
if (std::holds_alternative<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))
|
||||
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))
|
||||
res.put_value(std::get<std::string>(node._value));
|
||||
else if (std::holds_alternative<long long>(node._value))
|
||||
res.put_value(std::get<long long>(node._value));
|
||||
const auto& value {*node._value};
|
||||
|
||||
if (std::holds_alternative<std::string>(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
|
||||
{
|
||||
@@ -200,7 +200,7 @@ Response::writeXML(std::ostream& os)
|
||||
|
||||
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 )
|
||||
res.add_child(itChildArrayNode.first, nodeToPropertyTree(childNode));
|
||||
@@ -217,9 +217,11 @@ Response::writeXML(std::ostream& os)
|
||||
unsigned
|
||||
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")
|
||||
return 13;
|
||||
return 16;
|
||||
else if (clientName == "Sublime Music")
|
||||
return 16;
|
||||
else
|
||||
return 12;
|
||||
}
|
||||
@@ -233,20 +235,24 @@ Response::writeJSON(std::ostream& os)
|
||||
{
|
||||
Json::Object res;
|
||||
|
||||
for (auto itAttribute : node._attributes)
|
||||
auto valueToJsonValue {[](const Node::Value& value) -> Json::Value
|
||||
{
|
||||
if (std::holds_alternative<std::string>(itAttribute.second))
|
||||
res[itAttribute.first] = Json::Value {std::get<std::string>(itAttribute.second)};
|
||||
else if (std::holds_alternative<long long>(itAttribute.second))
|
||||
res[itAttribute.first] = Json::Value {std::get<long long>(itAttribute.second)};
|
||||
}
|
||||
if (std::holds_alternative<std::string>(value))
|
||||
return Json::Value {std::get<std::string>(value)};
|
||||
else if (std::holds_alternative<bool>(value))
|
||||
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"] = 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)};
|
||||
res["value"] = valueToJsonValue(*node._value);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
@@ -180,7 +181,15 @@ class Response
|
||||
{
|
||||
public:
|
||||
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
|
||||
void setValue(std::string_view value);
|
||||
@@ -193,8 +202,9 @@ class Response
|
||||
|
||||
private:
|
||||
friend class Response;
|
||||
std::map<std::string, std::variant<std::string, long long>> _attributes;
|
||||
std::variant<std::string, long long> _value;
|
||||
using Value = std::variant<std::string, bool, long long>;
|
||||
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>> _childrenArrays;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user