diff --git a/README.md b/README.md index 0629073f..1a104d1e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ A [demo instance](http://lms-demo.poupon.dev) is available. Note the administrat * Synchronizing 'love' feedbacks * ReplayGain support * User management, with several [authentication backends](INSTALL.md#authentication-backend) -* [Subsonic API](http://www.subsonic.org/pages/api.jsp) / [OpenSubsonic API](https://opensubsonic.netlify.app/docs/opensubsonic-api/) +* [Subsonic API](http://www.subsonic.org/pages/api.jsp) / [OpenSubsonic API](https://opensubsonic.netlify.app/docs/opensubsonic-api/) support ## Music discovery _LMS_ provides several ways to help you find the music you like: diff --git a/SUBSONIC.md b/SUBSONIC.md index 77dd6051..36e2ba6f 100644 --- a/SUBSONIC.md +++ b/SUBSONIC.md @@ -6,6 +6,8 @@ The Subsonic API is enabled by default. __Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method. You may need to check your client to make sure to use the __password__ authentication method. # OpenSubsonic API +OpenSubsonic is an initiative to patch and extend the legacy Subsonic API. You'll find more details in the [official documentation](https://opensubsonic.netlify.app/) + ## Extra fields The following extra fields are implemented: * `Album` response: diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index 3756e8ea..d4aa3f0e 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -8,6 +8,7 @@ add_library(lmssubsonic SHARED impl/entrypoints/MediaRetrieval.cpp impl/entrypoints/Playlists.cpp impl/entrypoints/Searching.cpp + impl/entrypoints/System.cpp impl/entrypoints/UserManagement.cpp impl/responses/Album.cpp impl/responses/Artist.cpp diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index b41d1aad..fe432a81 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -42,6 +42,7 @@ #include "entrypoints/MediaRetrieval.hpp" #include "entrypoints/Playlists.hpp" #include "entrypoints/Searching.hpp" +#include "entrypoints/System.hpp" #include "entrypoints/UserManagement.hpp" #include "ParameterParsing.hpp" #include "ProtocolVersion.hpp" @@ -119,29 +120,11 @@ namespace API::Subsonic throw UserNotAuthorizedError{}; } - Response handlePingRequest(RequestContext& context) - { - return Response::createOkResponse(context.serverProtocolVersion); - } - - Response handleGetLicenseRequest(RequestContext& context) - { - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; - - Response::Node& licenseNode{ response.createNode("license") }; - licenseNode.setAttribute("licenseExpires", "2025-09-03T14:46:43"); - licenseNode.setAttribute("email", "foo@bar.com"); - licenseNode.setAttribute("valid", true); - - return response; - } - Response handleNotImplemented(RequestContext&) { throw NotImplementedGenericError{}; } - using RequestHandlerFunc = std::function; using CheckImplementedFunc = std::function; struct RequestEntryPointInfo @@ -154,8 +137,9 @@ namespace API::Subsonic static const std::unordered_map requestEntryPoints { // System - {"/ping", {handlePingRequest}}, - {"/getLicense", {handleGetLicenseRequest}}, + {"/ping", {handlePingRequest}}, + {"/getLicense", {handleGetLicenseRequest}}, + {"/getOpenSubsonicExtensions", {handleGetOpenSubsonicExtensions}}, // Browsing {"/getMusicFolders", {handleGetMusicFoldersRequest}}, diff --git a/src/libs/subsonic/impl/SubsonicResponse.cpp b/src/libs/subsonic/impl/SubsonicResponse.cpp index fd52f6a6..95952e2e 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.cpp +++ b/src/libs/subsonic/impl/SubsonicResponse.cpp @@ -19,6 +19,7 @@ #include "SubsonicResponse.hpp" +#include #include #include #include @@ -46,17 +47,13 @@ namespace API::Subsonic void Response::Node::setValue(std::string_view value) { - if (!_children.empty() || !_childrenArrays.empty() || !_childrenValues.empty()) - throw LmsException{ "Node already has children" }; - + assert(_children.empty() && _childrenArrays.empty() && _childrenValues.empty()); _value = std::string{ value }; } void Response::Node::setValue(long long value) { - if (!_children.empty() || !_childrenArrays.empty() || !_childrenValues.empty()) - throw LmsException{ "Node already has children" }; - + assert(_children.empty() && _childrenArrays.empty() && _childrenValues.empty()); _value = value; } @@ -67,42 +64,42 @@ namespace API::Subsonic void Response::Node::addChild(const std::string& key, Node node) { - if (_value) - throw LmsException{ "Node already has a value" }; - + assert(!_value); _children[key].emplace_back(std::move(node)); } void Response::Node::createEmptyArrayChild(std::string_view key) { - if (_value) - throw LmsException{ "Node already has a value" }; - + assert(!_value); _childrenArrays.emplace(key, std::vector{}); } void Response::Node::addArrayChild(std::string_view key, Node node) { - if (_value) - throw LmsException{ "Node already has a value" }; - + assert(!_value); _childrenArrays[std::string{ key }].emplace_back(std::move(node)); } void Response::Node::createEmptyArrayValue(std::string_view key) { - if (_value) - throw LmsException{ "Node already has a value" }; - - _childrenValues.emplace(key, std::vector{}); + assert (!_value); + _childrenValues.emplace(key, ValuesType{}); } void Response::Node::addArrayValue(std::string_view key, std::string_view value) { - if (_value) - throw LmsException{ "Node already has a value" }; + assert(!_value); + auto& values{ _childrenValues[std::string{ key }] }; + values.push_back(std::string{ value }); + assert(std::all_of(std::cbegin(values) + 1, std::cend(values), [&](const ValueType& value) {return value.index() == values.front().index();})); + } - _childrenValues[std::string{ key }].push_back(std::string{ value }); + void Response::Node::addArrayValue(std::string_view key, long long value) + { + assert(!_value); + auto& values {_childrenValues[std::string{ key }]}; + values.push_back(value); + assert(std::all_of(std::cbegin(values) + 1, std::cend(values), [&](const ValueType& value) {return value.index() == values.front().index();})); } Response::Node& Response::Node::createChild(const std::string& key) @@ -184,7 +181,7 @@ namespace API::Subsonic void Response::writeXML(std::ostream& os) { - std::function nodeToPropertyTree = [&](const Response::Node& node) + std::function nodeToPropertyTree = [&](const Node& node) { boost::property_tree::ptree res; @@ -200,37 +197,39 @@ namespace API::Subsonic res.put("." + itAttribute.first, std::get(itAttribute.second)); } + auto valueToPropertyTree = [](const Node::ValueType& value) + { + boost::property_tree::ptree res; + std::visit([&](const auto& rawValue) + { + res.put_value(rawValue); + }, value); + + return res; + }; + if (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 if (std::holds_alternative(value)) - res.put_value(std::get(value)); + res = valueToPropertyTree(*node._value); } else { for (const auto& [key, childNodes] : node._children) { - for (const Response::Node& childNode : childNodes) + for (const Node& childNode : childNodes) res.add_child(key, nodeToPropertyTree(childNode)); } for (const auto& [key, childArrayNodes] : node._childrenArrays) { - for (const Response::Node& childNode : childArrayNodes) + for (const Node& childNode : childArrayNodes) res.add_child(key, nodeToPropertyTree(childNode)); } for (const auto& [key, childArrayValues] : node._childrenValues) { - for (const std::string& value : childArrayValues) - res.add_child(key, boost::property_tree::ptree{ value }); + for (const Response::Node::ValueType& value : childArrayValues) + res.add_child(key, valueToPropertyTree(value)); } } @@ -251,16 +250,12 @@ namespace API::Subsonic auto valueToJsonValue{ [](const Node::ValueType& value) -> Json::Value { - 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) }; - else if (std::holds_alternative(value)) - return Json::Value {std::get(value)}; - - throw LmsException("Unexpected value type"); + Json::Value res; + std::visit([&](const auto& rawValue) + { + res = Json::Value{ rawValue }; + }, value); + return res; } }; for (auto itAttribute : node._attributes) @@ -290,8 +285,8 @@ namespace API::Subsonic for (const auto& [key, childValues] : node._childrenValues) { Json::Array array; - for (const std::string& childValue : childValues) - array.emplace_back(Json::Value{ childValue }); + for (const Node::ValueType& childValue : childValues) + array.emplace_back(valueToJsonValue(childValue)); res[key] = std::move(array); } diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp index 395df598..1d04fd6e 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -217,6 +217,7 @@ namespace API::Subsonic void addArrayChild(std::string_view key, Node node); void createEmptyArrayValue(std::string_view key); void addArrayValue(std::string_view key, std::string_view value); + void addArrayValue(std::string_view key, long long value); private: void setVersionAttribute(ProtocolVersion version); @@ -227,7 +228,9 @@ namespace API::Subsonic std::optional _value; std::map> _children; std::map> _childrenArrays; - std::map> _childrenValues; + + using ValuesType = std::vector; + std::map _childrenValues; }; static Response createOkResponse(ProtocolVersion protocolVersion); diff --git a/src/libs/subsonic/impl/entrypoints/System.cpp b/src/libs/subsonic/impl/entrypoints/System.cpp new file mode 100644 index 00000000..6af37fd7 --- /dev/null +++ b/src/libs/subsonic/impl/entrypoints/System.cpp @@ -0,0 +1,34 @@ +#include "entrypoints/System.hpp" + +namespace API::Subsonic +{ + Response handlePingRequest(RequestContext& context) + { + return Response::createOkResponse(context.serverProtocolVersion); + } + + Response handleGetLicenseRequest(RequestContext& context) + { + Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + + Response::Node& licenseNode{ response.createNode("license") }; + licenseNode.setAttribute("licenseExpires", "2025-09-03T14:46:43"); + licenseNode.setAttribute("email", "foo@bar.com"); + licenseNode.setAttribute("valid", true); + + return response; + } + + Response handleGetOpenSubsonicExtensions(RequestContext& context) + { + Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + + { + Response::Node& transcodeOffsetNode{ response.createNode("openSubsonicExtensions") }; + transcodeOffsetNode.setAttribute("name", "transcodeOffset"); + transcodeOffsetNode.addArrayValue("versions", 1); + } + + return response; + }; +} diff --git a/src/libs/subsonic/impl/entrypoints/System.hpp b/src/libs/subsonic/impl/entrypoints/System.hpp new file mode 100644 index 00000000..81a394c8 --- /dev/null +++ b/src/libs/subsonic/impl/entrypoints/System.hpp @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "RequestContext.hpp" +#include "SubsonicResponse.hpp" + +namespace API::Subsonic +{ + Response handlePingRequest(RequestContext& context); + Response handleGetLicenseRequest(RequestContext& context); + Response handleGetOpenSubsonicExtensions(RequestContext& context); +}