diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index a50262ae..5b2e088c 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -496,12 +496,25 @@ std::chrono::milliseconds Release::getDuration() const { assert(self()); - assert(self()->id() != Wt::Dbo::dbo_traits::invalidId() ); + assert(self()->id() != Wt::Dbo::dbo_traits::invalidId()); assert(session()); using milli = std::chrono::duration; - Wt::Dbo::Query query {session()->query("SELECT SUM(duration) FROM track t INNER JOIN release r ON t.release_id = r.id") + Wt::Dbo::Query query {session()->query("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::invalidId()); + assert(session()); + + Wt::Dbo::Query query {session()->query("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(); diff --git a/src/libs/database/impl/TrackList.cpp b/src/libs/database/impl/TrackList.cpp index 03a20d97..981dd9cd 100644 --- a/src/libs/database/impl/TrackList.cpp +++ b/src/libs/database/impl/TrackList.cpp @@ -410,7 +410,7 @@ TrackList::getDuration() const using milli = std::chrono::duration; - Wt::Dbo::Query query {session()->query("SELECT SUM(duration) FROM track t INNER JOIN tracklist_entry p_e ON t.id = p_e.track_id") + Wt::Dbo::Query query {session()->query("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(); diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index d2deceb7..8b411b19 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -90,6 +90,7 @@ class Release : public Wt::Dbo::Dbo std::optional getTotalTrack() const; std::optional getTotalDisc() const; std::chrono::milliseconds getDuration() const; + Wt::WDateTime getLastWritten() const; // Get the artists of this release std::vector > getArtists(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const; diff --git a/src/libs/subsonic/impl/Scan.cpp b/src/libs/subsonic/impl/Scan.cpp index 42716069..70b2deb0 100644 --- a/src/libs/subsonic/impl/Scan.cpp +++ b/src/libs/subsonic/impl/Scan.cpp @@ -34,7 +34,7 @@ namespace API::Subsonic::Scan const IMediaScanner::Status scanStatus {ServiceProvider::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); diff --git a/src/libs/subsonic/impl/Stream.cpp b/src/libs/subsonic/impl/Stream.cpp index cd7645be..62503463 100644 --- a/src/libs/subsonic/impl/Stream.cpp +++ b/src/libs/subsonic/impl/Stream.cpp @@ -57,7 +57,6 @@ struct StreamParameters std::optional 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 resourceHandler; + + Wt::Http::ResponseContinuation *continuation = request.continuation(); + if (!continuation) + { + // Mandatory params + Id id {getMandatoryParameterAs(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>(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 resourceHandler; diff --git a/src/libs/subsonic/impl/Stream.hpp b/src/libs/subsonic/impl/Stream.hpp index bf7fade3..c2de5709 100644 --- a/src/libs/subsonic/impl/Stream.hpp +++ b/src/libs/subsonic/impl/Stream.hpp @@ -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); } diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index a177590d..07e3c2a2 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -19,6 +19,7 @@ #include "subsonic/SubsonicResource.hpp" #include +#include #include #include @@ -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(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 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 mediaRetrievalHandlers { // Media retrieval + {"download", Stream::handleDownload}, + {"stream", Stream::handleStream}, {"getCoverArt", handleGetCoverArt}, - {"stream", Stream::handle}, }; void diff --git a/src/libs/subsonic/impl/SubsonicResponse.cpp b/src/libs/subsonic/impl/SubsonicResponse.cpp index 9572d18e..47c47fcb 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.cpp +++ b/src/libs/subsonic/impl/SubsonicResponse.cpp @@ -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(itAttribute.second)) res.put("." + itAttribute.first, std::get(itAttribute.second)); + else if (std::holds_alternative(itAttribute.second)) + res.put("." + itAttribute.first, std::get(itAttribute.second)); else if (std::holds_alternative(itAttribute.second)) res.put("." + itAttribute.first, std::get(itAttribute.second)); } - if (node._value.valueless_by_exception()) + if (node._value) { - if (std::holds_alternative(node._value)) - res.put_value(std::get(node._value)); - else if (std::holds_alternative(node._value)) - res.put_value(std::get(node._value)); + const auto& value {*node._value}; + + if (std::holds_alternative(value)) + res.put_value(std::get(value)); + else if (std::holds_alternative(value)) + res.put_value(std::get(value)); + else if (std::holds_alternative(value)) + res.put_value(std::get(value)); } else { @@ -200,7 +200,7 @@ Response::writeXML(std::ostream& os) for (auto itChildArrayNode : node._childrenArrays) { - const std::vector& childArrayNodes {itChildArrayNode .second}; + const std::vector& 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(itAttribute.second)) - res[itAttribute.first] = Json::Value {std::get(itAttribute.second)}; - else if (std::holds_alternative(itAttribute.second)) - res[itAttribute.first] = Json::Value {std::get(itAttribute.second)}; - } + if (std::holds_alternative(value)) + return Json::Value {std::get(value)}; + else if (std::holds_alternative(value)) + return Json::Value {std::get(value)}; + else if (std::holds_alternative(value)) + return Json::Value {std::get(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(node._value)) - res["value"] = Json::Value {std::get(node._value)}; - else if (std::holds_alternative(node._value)) - res["value"] = Json::Value {std::get(node._value)}; + res["value"] = valueToJsonValue(*node._value); } else { diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp index 811d22c9..11cc0ae3 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -19,6 +19,7 @@ #pragma once #include +#include #include #include #include @@ -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 ::value>* = nullptr> + void setAttribute(std::string_view key, T value) + { + if constexpr (std::is_same::value) + _attributes[std::string {key}] = value; + else + _attributes[std::string {key}] = static_cast(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> _attributes; - std::variant _value; + using Value = std::variant; + std::map _attributes; + std::optional _value; std::map> _children; std::map> _childrenArrays; }; diff --git a/src/test/database/DatabaseTest.cpp b/src/test/database/DatabaseTest.cpp index 54602e89..c96a625d 100644 --- a/src/test/database/DatabaseTest.cpp +++ b/src/test/database/DatabaseTest.cpp @@ -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()};