Subsonic API: various fixes to make Sublime Music happy

This commit is contained in:
emeric
2020-07-14 11:38:06 +02:00
parent 100d0f55a5
commit d3dcee35a3
10 changed files with 142 additions and 61 deletions
+14 -1
View File
@@ -501,7 +501,20 @@ Release::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 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();
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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);
+38 -2
View File
@@ -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;
+2 -1
View File
@@ -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);
}
+29 -23
View File
@@ -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,7 +1813,6 @@ static std::unordered_map<std::string, RequestEntryPointInfo> requestEntryPoints
{"deletePlaylist", {handleDeletePlaylistRequest, false}},
// Media retrieval
{"download", {handleNotImplemented, false}},
{"hls", {handleNotImplemented, false}},
{"getCaptions", {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
{
// Media retrieval
{"download", Stream::handleDownload},
{"stream", Stream::handleStream},
{"getCoverArt", handleGetCoverArt},
{"stream", Stream::handle},
};
void
+32 -26
View File
@@ -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
{
@@ -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
{
+13 -3
View File
@@ -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;
};
+8
View File
@@ -210,6 +210,8 @@ testSingleRelease(Session& session)
releases = Release::getAll(session);
CHECK(releases.size() == 1);
CHECK(releases.front().id() == release.getId());
CHECK(release->getDuration() == std::chrono::seconds {0});
}
}
@@ -711,6 +713,12 @@ testMultipleTracksMultipleClustersTopRelease(Session& session)
ScopedUser user {session, "MyUser"};
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()};