diff --git a/Makefile.am b/Makefile.am
index 954821e3..ba2c63cd 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -7,21 +7,21 @@ lms_docrootdir=$(pkgdatadir)/docroot
lms_approotdir=$(pkgdatadir)/approot
lms_cssdir=$(lms_docrootdir)/css
-lms_imagesdir=$(lms_docrootdir)/images
lms_jsdir=$(lms_docrootdir)/js
+lms_imagesdir=$(lms_approotdir)/images
lms_css_DATA = \
docroot/css/lms.css
-lms_images_DATA = \
- docroot/images/unknown-cover.jpg \
- docroot/images/unknown-artist.jpg
-
lms_js_DATA = \
docroot/js/bootstrap-notify.js \
docroot/js/jquery-1.10.2.min.js \
docroot/js/mediaplayer.js
+lms_images_DATA = \
+ approot/images/unknown-cover.jpg \
+ approot/images/unknown-artist.jpg
+
lms_approot_DATA = \
approot/admin-database.xml \
approot/admin-user.xml \
diff --git a/docroot/images/unknown-artist.jpg b/approot/images/unknown-artist.jpg
similarity index 100%
rename from docroot/images/unknown-artist.jpg
rename to approot/images/unknown-artist.jpg
diff --git a/docroot/images/unknown-cover.jpg b/approot/images/unknown-cover.jpg
similarity index 100%
rename from docroot/images/unknown-cover.jpg
rename to approot/images/unknown-cover.jpg
diff --git a/src/Makefile.am b/src/Makefile.am
index 6eb370e4..1e8f7830 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1,6 +1,9 @@
bin_PROGRAMS = lms
lms_SOURCES = \
+ $(srcdir)/api/subsonic/SubsonicId.cpp \
+ $(srcdir)/api/subsonic/SubsonicResource.cpp \
+ $(srcdir)/api/subsonic/SubsonicResponse.cpp \
$(srcdir)/av/AvInfo.cpp \
$(srcdir)/av/AvTranscoder.cpp \
$(srcdir)/cover/CoverArtGrabber.cpp \
diff --git a/src/api/subsonic/SubsonicId.cpp b/src/api/subsonic/SubsonicId.cpp
new file mode 100644
index 00000000..02e270e7
--- /dev/null
+++ b/src/api/subsonic/SubsonicId.cpp
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2019 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 .
+ */
+
+#include "SubsonicId.hpp"
+
+#include "utils/Logger.hpp"
+#include "utils/Utils.hpp"
+
+namespace API::Subsonic
+{
+
+boost::optional
+IdFromString(const std::string& id)
+{
+ std::vector values {splitString(id, "-")};
+ if (values.size() != 2)
+ {
+ LMS_LOG(API_SUBSONIC, ERROR) << "Bad id format";
+ return {};
+ }
+
+ Id res;
+
+ std::string type {std::move(values[0])};
+ if (type == "artist")
+ res.type = Id::Type::Artist;
+ else if (type == "album")
+ res.type = Id::Type::Release;
+ else if (type == "track")
+ res.type = Id::Type::Track;
+ else
+ {
+ LMS_LOG(API_SUBSONIC, ERROR) << "Bad id format";
+ return {};
+ }
+
+ auto optId {readAs(values[1])};
+ if (!optId)
+ {
+ LMS_LOG(API_SUBSONIC, ERROR) << "Bad id format";
+ return {};
+ }
+
+ res.id = *optId;
+
+ return res;
+}
+
+std::string
+IdToString(const Id& id)
+{
+ std::string res;
+
+ switch (id.type)
+ {
+ case Id::Type::Artist:
+ res = "artist-";
+ break;
+ case Id::Type::Release:
+ res = "album-";
+ break;
+ case Id::Type::Track:
+ res = "track-";
+ break;
+ }
+
+ return res + std::to_string(id.id);
+}
+
+} // namespace API::Subsonic
diff --git a/src/api/subsonic/SubsonicId.hpp b/src/api/subsonic/SubsonicId.hpp
new file mode 100644
index 00000000..60604dbf
--- /dev/null
+++ b/src/api/subsonic/SubsonicId.hpp
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2019 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 .
+ */
+
+#include
+
+#include "database/Types.hpp"
+
+namespace API::Subsonic
+{
+
+struct Id
+{
+ enum class Type
+ {
+ Track,
+ Release,
+ Artist,
+ };
+
+ Type type;
+ Database::IdType id;
+};
+
+boost::optional IdFromString(const std::string& id);
+std::string IdToString(const Id& id);
+
+} // namespace API::Subsonic
diff --git a/src/api/subsonic/SubsonicResource.cpp b/src/api/subsonic/SubsonicResource.cpp
new file mode 100644
index 00000000..91c0bbd2
--- /dev/null
+++ b/src/api/subsonic/SubsonicResource.cpp
@@ -0,0 +1,657 @@
+/*
+ * Copyright (C) 2019 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 .
+ */
+#include "SubsonicResource.hpp"
+
+#include
+#include
+#include
+
+#include
+
+#include "av/AvTranscoder.hpp"
+#include "cover/CoverArtGrabber.hpp"
+#include "database/Artist.hpp"
+#include "database/Release.hpp"
+#include "database/Track.hpp"
+#include "main/Services.hpp"
+#include "utils/Logger.hpp"
+#include "utils/Utils.hpp"
+#include "SubsonicId.hpp"
+#include "SubsonicResponse.hpp"
+
+// Requests
+#define PING_URL "/rest/ping.view"
+#define GET_LICENSE_URL "/rest/getLicense.view"
+#define GET_RANDOM_SONGS_URL "/rest/getRandomSongs.view"
+#define GET_ALBUM_LIST_URL "/rest/getAlbumList.view"
+#define GET_MUSIC_DIRECTORY_URL "/rest/getMusicDirectory.view"
+#define GET_STARRED_URL "/rest/getStarred.view"
+#define GET_MUSIC_FOLDERS_URL "/rest/getMusicFolders.view"
+#define GET_INDEXES_URL "/rest/getIndexes.view"
+#define GET_ARTISTS_URL "/rest/getArtists.view"
+
+// MediaRetrievals
+#define STREAM_URL "/rest/stream.view"
+#define GET_COVER_ART_URL "/rest/getCoverArt.view"
+
+namespace API::Subsonic
+{
+
+// requests
+using RequestHandlerFunc = std::function;
+static Response handlePingRequest(const Wt::Http::ParameterMap& request, Database::Handler& db);
+static Response handleGetLicenseRequest(const Wt::Http::ParameterMap& request, Database::Handler& db);
+static Response handleGetRandomSongsRequest(const Wt::Http::ParameterMap& request, Database::Handler& db);
+static Response handleGetAlbumListRequest(const Wt::Http::ParameterMap& request, Database::Handler& db);
+static Response handleGetMusicDirectoryRequest(const Wt::Http::ParameterMap& request, Database::Handler& db);
+static Response handleGetMusicFoldersRequest(const Wt::Http::ParameterMap& request, Database::Handler& db);
+static Response handleGetIndexesRequest(const Wt::Http::ParameterMap& request, Database::Handler& db);
+static Response handleGetArtistsRequest(const Wt::Http::ParameterMap& request, Database::Handler& db);
+
+// MediaRetrievals
+using MediaRetrivalHandlerFunc = std::function;
+void handleStream(const Wt::Http::Request& request, Database::Handler& db, Wt::Http::Response& response);
+void handleGetCoverArt(const Wt::Http::Request& request, Database::Handler& db, Wt::Http::Response& response);
+
+static std::map requestHandlers
+{
+ {PING_URL, handlePingRequest},
+ {GET_LICENSE_URL, handleGetLicenseRequest},
+ {GET_RANDOM_SONGS_URL, handleGetRandomSongsRequest},
+ {GET_ALBUM_LIST_URL, handleGetAlbumListRequest},
+ {GET_MUSIC_DIRECTORY_URL, handleGetMusicDirectoryRequest},
+ {GET_STARRED_URL, handlePingRequest}, // TODO
+ {GET_MUSIC_FOLDERS_URL, handleGetMusicFoldersRequest},
+ {GET_INDEXES_URL, handleGetIndexesRequest},
+ {GET_ARTISTS_URL, handleGetArtistsRequest},
+};
+
+static std::map mediaRetrievalHandlers
+{
+ {STREAM_URL, handleStream},
+ {GET_COVER_ART_URL, handleGetCoverArt},
+};
+
+template
+boost::optional
+getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
+{
+ boost::optional res;
+
+ auto it = parameterMap.find(param);
+ if (it == parameterMap.end())
+ return res;
+
+ if (it->second.size() != 1)
+ return res;
+
+ return readAs(it->second.front());
+}
+
+
+struct ClientInfo
+{
+ std::string name;
+ ResponseFormat format;
+ std::string user;
+ std::string password;
+};
+
+boost::optional
+getClientInfo(const Wt::Http::ParameterMap& parameters)
+{
+ boost::optional res {ClientInfo {}};
+
+ // Mandatory parameters
+ auto param {getParameterAs(parameters, "c")};
+ if (!param)
+ return {};
+ res->name = *param;
+
+ param = getParameterAs(parameters, "u");
+ if (!param)
+ return {};
+ res->user = *param;
+
+ param = getParameterAs(parameters, "p");
+ if (!param)
+ return {};
+ res->password = *param;
+
+ // Optional parameters
+ param = getParameterAs(parameters, "f");
+ res->format = (param ? ResponseFormat::json : ResponseFormat::xml); // TODO
+
+ return res;
+}
+
+SubsonicResource::SubsonicResource(Wt::Dbo::SqlConnectionPool& connectionPool)
+: _db {connectionPool}
+{
+}
+
+std::vector
+SubsonicResource::getPaths()
+{
+ std::vector paths;
+
+ for (auto it : requestHandlers)
+ paths.emplace_back(it.first);
+
+ for (auto it : mediaRetrievalHandlers)
+ paths.emplace_back(it.first);
+
+ return paths;
+}
+
+void
+SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response)
+{
+ LMS_LOG(API_SUBSONIC, DEBUG) << "REQUEST. Path = '" << request.path() << "', pathInfo = '" << request.pathInfo() << "', queryString = '" << request.queryString() << "'";
+
+ const Wt::Http::ParameterMap parameters {request.getParameterMap()};
+
+ for (const auto it : parameters)
+ {
+ LMS_LOG(API_SUBSONIC, DEBUG) << "Found param '" << it.first << "'";
+ for (const std::string& value : it.second)
+ LMS_LOG(API_SUBSONIC, DEBUG) << "\t'" << value << "'";
+ }
+
+ std::string s{std::istreambuf_iterator(request.in()), {}};
+
+ LMS_LOG(API_SUBSONIC, DEBUG) << "BODY = '" << s << "'";
+
+ auto clientInfo {getClientInfo(parameters)};
+ if (!clientInfo)
+ {
+ LMS_LOG(API_SUBSONIC, ERROR) << "Failed to parse client info";
+ return;
+ }
+
+ try
+ {
+ static std::mutex mutex;
+
+ std::unique_lock lock{mutex}; // For now just handle request s one by one
+
+ auto itHandler {requestHandlers.find(request.path())};
+ if (itHandler != requestHandlers.end())
+ {
+ Response resp {(itHandler->second)(request.getParameterMap(), _db)};
+ responseToStream(resp, clientInfo->format, response.out());
+ response.setMimeType(ResponseFormatToMimeType(clientInfo->format));
+ return;
+ }
+
+ auto itStreamHandler {mediaRetrievalHandlers.find(request.path())};
+ if (itStreamHandler != mediaRetrievalHandlers.end())
+ {
+ itStreamHandler->second(request, _db, response);
+ return;
+ }
+
+ LMS_LOG(API_SUBSONIC, ERROR) << "Unhandled command '" << request.path() << "'";
+
+ }
+ catch (const Error& e)
+ {
+ Response resp {Response::createFailedResponse(e)};
+ responseToStream(resp, clientInfo->format, response.out());
+ response.setMimeType(ResponseFormatToMimeType(clientInfo->format));
+ }
+}
+
+static
+std::string
+getArtistNames(const std::vector artists)
+{
+ if (artists.size() == 1)
+ return artists.front()->getName();
+
+ std::vector names;
+ names.resize(artists.size());
+
+ std::transform(std::cbegin(artists), std::cend(artists), std::begin(names),
+ [](const Database::Artist::pointer& artist)
+ {
+ return artist->getName();
+ });
+
+ return joinStrings(names, ", ");
+}
+
+static
+Response::Node
+trackToResponseNode(const Database::Track::pointer& track)
+{
+ Response::Node trackResponse;
+
+ trackResponse.setAttribute("title", track->getName());
+ trackResponse.setAttribute("id", IdToString({Id::Type::Track, track.id()}));
+ trackResponse.setAttribute("coverArt", IdToString({Id::Type::Track, track.id()}));
+ trackResponse.setAttribute("isDir", "false");
+
+ auto artists {track->getArtists()};
+ if (!artists.empty())
+ trackResponse.setAttribute("artist", getArtistNames(artists));
+
+ trackResponse.setAttribute("path", track->getName() + ".mp3");
+ trackResponse.setAttribute("bitrate", "128");
+ trackResponse.setAttribute("duration", std::to_string(std::chrono::duration_cast(track->getDuration()).count()));
+ trackResponse.setAttribute("suffix", "mp3");
+ trackResponse.setAttribute("contentType", "audio/mpeg");
+ trackResponse.setAttribute("size", std::to_string(128000/8 * std::chrono::duration_cast(track->getDuration()).count()));
+
+ if (track->getYear())
+ trackResponse.setAttribute("year", std::to_string(*track->getYear()));
+
+ if (track->getTrackNumber())
+ trackResponse.setAttribute("track", std::to_string(*track->getTrackNumber()));
+
+ if (track->getRelease())
+ {
+ trackResponse.setAttribute("parent", IdToString({Id::Type::Release, track->getRelease().id()}));
+ trackResponse.setAttribute("album", track->getRelease()->getName());
+ }
+
+ return trackResponse;
+}
+
+static
+Response::Node
+releaseToResponseNode(const Database::Release::pointer& release)
+{
+ Response::Node albumNode;
+
+ albumNode.setAttribute("title", release->getName());
+ albumNode.setAttribute("id", IdToString({Id::Type::Release, release.id()}));
+ albumNode.setAttribute("isDir", "true");
+ albumNode.setAttribute("coverArt", IdToString({Id::Type::Release, release.id()}));
+
+ auto artists {release->getArtists()};
+ if (!artists.empty())
+ {
+ if (artists.size() > 1)
+ albumNode.setAttribute("artist", "Various Artists");
+ else
+ albumNode.setAttribute("artist", artists.front()->getName());
+ }
+
+ return albumNode;
+}
+
+static
+Response::Node
+artistToResponseNode(const Database::Artist::pointer& artist)
+{
+ Response::Node artistResponse;
+
+ artistResponse.setAttribute("id", IdToString({Id::Type::Artist, artist.id()}));
+ artistResponse.setAttribute("name", artist->getName());
+ artistResponse.setAttribute("albumCount", std::to_string(artist->getReleases().size()));
+
+ return artistResponse;
+}
+
+
+// Handlers
+Response
+handlePingRequest(const Wt::Http::ParameterMap ¶meters, Database::Handler& handler)
+{
+ return Response::createOkResponse();
+}
+
+Response
+handleGetLicenseRequest(const Wt::Http::ParameterMap ¶meters, Database::Handler& handler)
+{
+ Response response {Response::createOkResponse()};
+
+ Response::Node& licenseNode {response.createNode("license")};
+ licenseNode.setAttribute("licenseExpires", "2019-09-03T14:46:43");
+ licenseNode.setAttribute("email", "foo@bar.com");
+ licenseNode.setAttribute("valid", "true");
+
+ return response;
+}
+
+Response
+handleGetRandomSongsRequest(const Wt::Http::ParameterMap& parameters, Database::Handler& db)
+{
+ // Optional params
+ auto size {getParameterAs(parameters, "size")};
+ if (!size)
+ size = 50;
+
+ Wt::Dbo::Transaction transaction {db.getSession()};
+
+ auto tracks {Database::Track::getAllRandom(db.getSession(), *size)};
+
+ LMS_LOG(API_SUBSONIC, DEBUG) << "Got " << tracks.size() << " tracks";
+
+ Response response {Response::createOkResponse()};
+
+ Response::Node& randomSongsNode {response.createNode("randomSongs")};
+ for (const Database::Track::pointer& track : tracks)
+ randomSongsNode.addArrayChild("song", trackToResponseNode(track));
+
+ return response;
+}
+
+static
+std::vector getRandomAlbums(Wt::Dbo::Session& session, std::size_t offset, std::size_t size)
+{
+ std::vector res;
+
+ std::size_t nbReleases {Database::Release::getCount(session)};
+ if (offset > nbReleases)
+ return res;
+
+ if (offset + size > nbReleases)
+ size = nbReleases - offset;
+
+ std::vector indexes;
+ indexes.resize(nbReleases);
+ std::iota(std::begin(indexes), std::end(indexes), 1);
+
+ // As random results are paginated, we need to set a seed for it
+ std::random_device r;
+ std::seed_seq seed {1337};
+ std::mt19937 generator{seed};
+
+ std::shuffle(std::begin(indexes), std::end(indexes), generator);
+ std::for_each(std::next(std::begin(indexes), offset), std::next(std::begin(indexes), offset + size),
+ [&](std::size_t offset)
+ {
+ auto release {Database::Release::getAll(session, offset, 1)};
+ if (!release.empty())
+ res.emplace_back(release.front());
+ });
+
+ return res;
+}
+
+
+
+Response
+handleGetAlbumListRequest(const Wt::Http::ParameterMap& request, Database::Handler& db)
+{
+ // Mandatory params
+ auto type {getParameterAs(request, "type")};
+ if (!type)
+ throw Error {Error::Code::RequiredParameterMissing};
+
+ // Optional params
+ auto size {getParameterAs(request, "size")};
+ if (!size)
+ size = 10;
+
+ auto offset {getParameterAs(request, "offset")};
+ if (!offset)
+ offset = 0;
+
+ std::vector releases;
+
+ Wt::Dbo::Transaction transaction {db.getSession()};
+
+ if (*type == "random")
+ {
+ releases = getRandomAlbums(db.getSession(), *offset, *size);
+ }
+ else if (*type == "newest")
+ {
+ auto after {Wt::WLocalDateTime::currentServerDateTime().toUTC().addMonths(-1)};
+ releases = Database::Release::getLastAdded(db.getSession(), after, offset, size);
+ }
+ else
+ throw Error {"Unsupported request"};
+
+ LMS_LOG(API_SUBSONIC, DEBUG) << "Got " << releases.size() << " albums";
+
+ Response response {Response::createOkResponse()};
+ Response::Node& albumListNode {response.createNode("albumList")};
+
+ for (const Database::Release::pointer& release : releases)
+ albumListNode.addArrayChild("album", releaseToResponseNode(release));
+
+ return response;
+}
+
+Response
+handleGetMusicDirectoryRequest(const Wt::Http::ParameterMap& request, Database::Handler& db)
+{
+ // Mandatory params
+ auto idParam {getParameterAs(request, "id")};
+ if (!idParam)
+ throw Error {Error::Code::RequiredParameterMissing};
+
+ auto id {IdFromString(*idParam)};
+ if (!id)
+ throw Error {"Bad id"};
+
+ Wt::Dbo::Transaction transaction {db.getSession()};
+
+ Response response {Response::createOkResponse()};
+ Response::Node& directoryNode {response.createNode("directory")};
+
+ switch (id->type)
+ {
+ case Id::Type::Artist:
+ {
+ auto artist {Database::Artist::getById(db.getSession(), id->id)};
+ if (!artist)
+ throw Error {Error::Code::RequestedDataNotFound};
+
+ auto releases {artist->getReleases()};
+ for (const Database::Release::pointer& release : releases)
+ directoryNode.addArrayChild("child", releaseToResponseNode(release));
+
+ break;
+ }
+
+ case Id::Type::Release:
+ {
+ auto release {Database::Release::getById(db.getSession(), id->id)};
+ if (!release)
+ throw Error {Error::Code::RequestedDataNotFound};
+
+ auto tracks {release->getTracks()};
+ for (const Database::Track::pointer& track : tracks)
+ directoryNode.addArrayChild("child", trackToResponseNode(track));
+
+ break;
+ }
+
+ default:
+ throw Error {"Bad id"};
+ }
+
+ return response;
+}
+
+Response
+handleGetMusicFoldersRequest(const Wt::Http::ParameterMap& request, Database::Handler& db)
+{
+ Response response {Response::createOkResponse()};
+ Response::Node& musicFoldersNode {response.createNode("musicFolders")};
+
+ Response::Node& musicFolderNode {musicFoldersNode.createArrayChild("musicFolder")};
+ musicFolderNode.setAttribute("id", "1");
+ musicFolderNode.setAttribute("name", "Music");
+
+ return response;
+}
+
+Response
+handleGetIndexesRequest(const Wt::Http::ParameterMap& request, Database::Handler& db)
+{
+ Wt::Dbo::Transaction transaction {db.getSession()};
+
+ Response response {Response::createOkResponse()};
+ Response::Node& artistsNode {response.createNode("indexes")};
+
+ Response::Node& indexNode {artistsNode.createArrayChild("index")};
+
+ auto artists {Database::Artist::getAll(db.getSession())};
+ for (const Database::Artist::pointer& artist : artists)
+ indexNode.addArrayChild("artist", artistToResponseNode(artist));
+
+ return response;
+}
+
+
+Response
+handleGetArtistsRequest(const Wt::Http::ParameterMap& request, Database::Handler& db)
+{
+ Wt::Dbo::Transaction transaction {db.getSession()};
+
+ // TODO factorize with handleGetIndexesRequest
+ Response response {Response::createOkResponse()};
+ Response::Node& artistsNode {response.createNode("artists")};
+
+ auto artists {Database::Artist::getAll(db.getSession())};
+ for (const Database::Artist::pointer& artist : artists)
+ artistsNode.addArrayChild("artist", artistToResponseNode(artist));
+
+ return response;
+}
+
+static
+std::shared_ptr
+createTranscoder(const Wt::Http::ParameterMap& request, Database::Handler& db)
+{
+ // Mandatory params
+ auto idParam {getParameterAs(request, "id")};
+ if (!idParam)
+ throw Error {Error::Code::RequiredParameterMissing};
+
+ auto id {IdFromString(*idParam)};
+ if (!id || id->type != Id::Type::Track)
+ throw Error {"bad id format"};
+
+ boost::filesystem::path trackPath;
+ {
+ Wt::Dbo::Transaction transaction {db.getSession()};
+
+ auto track {Database::Track::getById(db.getSession(), id->id)};
+ if (!track)
+ {
+ LMS_LOG(API_SUBSONIC, ERROR) << "Bad track id";
+ throw Error {Error::Code::RequestedDataNotFound};
+ }
+
+ trackPath = track->getPath();
+ }
+
+ Av::TranscodeParameters parameters {};
+
+ parameters.bitrate = 128000;
+ parameters.encoding = Av::Encoding::MP3;
+
+ return std::make_shared(trackPath, parameters);
+}
+
+void
+handleStream(const Wt::Http::Request& request, Database::Handler& db, Wt::Http::Response& response)
+{
+ LMS_LOG(API_SUBSONIC, DEBUG) << "STREAM";
+
+ std::shared_ptr transcoder;
+
+ Wt::Http::ResponseContinuation* continuation {request.continuation()};
+ if (!continuation)
+ {
+ transcoder = createTranscoder(request.getParameterMap(), db);
+ response.setMimeType(Av::encodingToMimetype(Av::Encoding::MP3));
+ transcoder->start();
+ }
+ else
+ {
+ LMS_LOG(UI, DEBUG) << "Continuation! ";
+ transcoder = Wt::cpp17::any_cast>(continuation->data());
+ }
+
+ if (!transcoder)
+ throw Error {"transcoding failed"};
+
+ if (!transcoder->isComplete())
+ {
+ static constexpr std::size_t chunkSize {65536*4};
+
+ std::vector data;
+ data.reserve(chunkSize);
+
+ transcoder->process(data, chunkSize);
+
+ LMS_LOG(API_SUBSONIC, DEBUG) << "Writing " << data.size() << " bytes...";
+ response.out().write(reinterpret_cast(&data[0]), data.size());
+
+ if (!response.out())
+ {
+ LMS_LOG(UI, ERROR) << "Write failed!";
+ return;
+ }
+ }
+
+ if (!transcoder->isComplete())
+ {
+ continuation = response.createContinuation();
+ continuation->setData(transcoder);
+ }
+ else
+ LMS_LOG(API_SUBSONIC, DEBUG) << "No more data!";
+}
+
+void
+handleGetCoverArt(const Wt::Http::Request& request, Database::Handler& db, Wt::Http::Response& response)
+{
+ LMS_LOG(API_SUBSONIC, DEBUG) << "STREAM";
+
+ // Mandatory params
+ auto idParam {getParameterAs(request.getParameterMap(), "id")};
+ if (!idParam)
+ throw Error {Error::Code::RequiredParameterMissing};
+
+ auto id {IdFromString(*idParam)};
+ if (!id)
+ throw Error {"bad id format"};
+
+ auto size {getParameterAs(request.getParameterMap(), "size")};
+ if (!size || *size == 0)
+ size = 256;
+
+ std::vector cover;
+ switch (id->type)
+ {
+ case Id::Type::Track:
+ cover = getServices().coverArtGrabber->getFromTrack(db.getSession(), id->id, Image::Format::JPEG, *size);
+ break;
+ case Id::Type::Release:
+ cover = getServices().coverArtGrabber->getFromRelease(db.getSession(), id->id, Image::Format::JPEG, *size);
+ break;
+ default:
+ throw Error {"bad id format"};
+ }
+
+ response.setMimeType( Image::format_to_mimeType(Image::Format::JPEG) );
+ response.out().write(reinterpret_cast(&cover[0]), cover.size());
+}
+
+} // namespace api::subsonic
+
diff --git a/src/api/subsonic/SubsonicResource.hpp b/src/api/subsonic/SubsonicResource.hpp
new file mode 100644
index 00000000..ca0ac427
--- /dev/null
+++ b/src/api/subsonic/SubsonicResource.hpp
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2019 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
+
+#include
+#include
+
+#include "database/DatabaseHandler.hpp"
+
+namespace API::Subsonic
+{
+
+class SubsonicResource final : public Wt::WResource
+{
+ public:
+ SubsonicResource(Wt::Dbo::SqlConnectionPool& connectionPool);
+
+ static std::vector getPaths();
+ private:
+
+
+ void handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) override;
+
+ Database::Handler _db;
+};
+
+} // namespace
diff --git a/src/api/subsonic/SubsonicResponse.cpp b/src/api/subsonic/SubsonicResponse.cpp
new file mode 100644
index 00000000..fbc830e6
--- /dev/null
+++ b/src/api/subsonic/SubsonicResponse.cpp
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2019 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 .
+ */
+
+#include "SubsonicResponse.hpp"
+
+#include
+#include
+
+#define API_VERSION "1.12.0"
+
+namespace API::Subsonic
+{
+
+std::string
+ResponseFormatToMimeType(ResponseFormat format)
+{
+ switch (format)
+ {
+ case ResponseFormat::xml: return "text/xml";
+ case ResponseFormat::json: return "application/json";
+ }
+
+ return "";
+}
+
+static
+const char*
+ErrorCodeToString(Error::Code error)
+{
+ switch (error)
+ {
+ case Error::Code::RequiredParameterMissing:
+ return "Required parameter is missing.";
+ case Error::Code::ClientMustUpgrade:
+ return "Incompatible Subsonic REST protocol version. Client must upgrade.";
+ case Error::Code::ServerMustUpgrade:
+ return "Incompatible Subsonic REST protocol version. Server must upgrade.";
+ case Error::Code::WrongUsernameOrPassword:
+ return "Wrong username or password.";
+ case Error::Code::UserNotAuthorized:
+ return "User is not authorized for the given operation.";
+ case Error::Code::RequestedDataNotFound:
+ return "The requested data was not found.";
+ default:
+ return "Unknown error";
+ }
+}
+
+Error::Error(Code code)
+: _code {code},
+_message {ErrorCodeToString(code)}
+{
+}
+
+Error::Error(const std::string& message)
+: _code {Code::Generic},
+_message {message}
+{
+}
+
+void
+Response::Node::setAttribute(const std::string& key, const std::string& value)
+{
+ attributes[key] = value;
+}
+
+void
+Response::Node::addChild(const std::string& key, Node node)
+{
+ children[key].emplace_back(std::move(node));
+}
+
+void
+Response::Node::addArrayChild(const std::string& key, Node node)
+{
+ childrenArrays[key].emplace_back(std::move(node));
+}
+
+
+Response::Node&
+Response::Node::createChild(const std::string& key)
+{
+ children[key].emplace_back();
+ return children[key].back();
+}
+
+Response::Node&
+Response::Node::createArrayChild(const std::string& key)
+{
+ childrenArrays[key].emplace_back();
+ return childrenArrays[key].back();
+}
+
+Response
+Response::createOkResponse()
+{
+ Response response;
+ Node& responseNode {response._root.createChild("subsonic-response")};
+
+ responseNode.setAttribute("status", "ok");
+ responseNode.setAttribute("version", API_VERSION);
+
+ return response;
+}
+
+Response
+Response::createFailedResponse(const Error& error)
+{
+ Response response;
+ Node& responseNode {response._root.createChild("subsonic-response")};
+
+ responseNode.setAttribute("status", "failed");
+ responseNode.setAttribute("version", API_VERSION);
+
+ Node& errorNode {responseNode.createChild("error")};
+ errorNode.setAttribute("code", std::to_string(static_cast(error.getCode())));
+ errorNode.setAttribute("message", error.getMessage());
+
+ return response;
+}
+
+Response::Node&
+Response::createNode(const std::string& key)
+{
+ return _root.children["subsonic-response"].front().createChild(key);
+}
+
+boost::property_tree::ptree
+NodeToPropertyTree(const Response::Node& node, ResponseFormat format)
+{
+ boost::property_tree::ptree res;
+
+ for (auto itChildNode : node.children)
+ {
+ for (const Response::Node& childNode : itChildNode.second)
+ res.add_child(itChildNode.first, NodeToPropertyTree(childNode, format));
+ }
+
+ for (auto itChildArrayNode : node.childrenArrays)
+ {
+ const std::vector& childArrayNodes {itChildArrayNode .second};
+
+ if (format == ResponseFormat::json)
+ {
+ boost::property_tree::ptree array;
+
+ for (const Response::Node& childNode : childArrayNodes )
+ array.push_back(std::make_pair("", NodeToPropertyTree(childNode, format)));
+
+ res.add_child(itChildArrayNode.first, array);
+ }
+ else
+ {
+ for (const Response::Node& childNode : childArrayNodes )
+ res.add_child(itChildArrayNode.first, NodeToPropertyTree(childNode, format));
+ }
+ }
+
+ for (auto itAttribute : node.attributes)
+ {
+ std::string key {format == ResponseFormat::xml ? "." : ""};
+
+ key += itAttribute.first;
+
+ res.put(key, itAttribute.second);
+ }
+
+ return res;
+}
+
+void responseToStream(const Response& response, ResponseFormat format, std::ostream& os)
+{
+ boost::property_tree::ptree root {NodeToPropertyTree(response._root, format)};
+
+ switch (format)
+ {
+ case ResponseFormat::xml:
+ boost::property_tree::write_xml(os, root);
+ break;
+ case ResponseFormat::json:
+ boost::property_tree::write_json(os, root);
+ break;
+
+ }
+}
+
+} // namespace
+
diff --git a/src/api/subsonic/SubsonicResponse.hpp b/src/api/subsonic/SubsonicResponse.hpp
new file mode 100644
index 00000000..7154e4a1
--- /dev/null
+++ b/src/api/subsonic/SubsonicResponse.hpp
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2019 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 .
+ */
+
+#include