/* * 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 #include #include "auth/PasswordService.hpp" #include "av/AvTranscoder.hpp" #include "cover/CoverArtGrabber.hpp" #include "database/Artist.hpp" #include "database/Cluster.hpp" #include "database/Release.hpp" #include "database/Track.hpp" #include "database/TrackList.hpp" #include "database/User.hpp" #include "main/Service.hpp" #include "similarity/SimilaritySearcher.hpp" #include "utils/Logger.hpp" #include "utils/Utils.hpp" #include "SubsonicId.hpp" #include "SubsonicResponse.hpp" using namespace Database; static const std::string genreClusterName {"GENRE"}; // Files are always reported to be in the same format static const std::size_t reportedBitrate {128}; static const std::string reportedFileSuffix {"mp3"}; static const Av::Encoding reportedEncoding {Av::Encoding::MP3}; static const Av::Encoding transcodeEncoding {Av::Encoding::MP3}; static const std::string reportedStarredDate {"2000-01-01T00:00:00"}; template<> boost::optional readAs(const std::string& str) { return API::Subsonic::IdFromString(str); } template<> boost::optional readAs(const std::string& str) { if (str == "true") return true; else if (str == "false") return false; return {}; } namespace API::Subsonic { struct ClientVersion { unsigned major {}; unsigned minor {}; unsigned patch {}; }; } template<> boost::optional readAs(const std::string& str) { // Expects "X.Y.Z" const auto numbers {splitString(str, ".")}; if (numbers.size() < 2 || numbers.size() > 3) return boost::none; API::Subsonic::ClientVersion version; auto number {readAs(numbers[0])}; if (!number) return boost::none; version.major = *number; number = {readAs(numbers[1])}; if (!number) return boost::none; version.minor = *number; if (numbers.size() == 3) { number = {readAs(numbers[2])}; if (!number) return boost::none; version.patch = *number; } return version; } namespace API::Subsonic { struct ClientInfo { std::string name; std::string user; std::string password; ClientVersion version; }; struct RequestContext { const Wt::Http::ParameterMap& parameters; Session& dbSession; std::string userName; }; using SessionMap = std::map>; static std::map dbSessions; static Session& getOrCreateDbSession(Database::Database& db) { static std::mutex mutex; SessionMap* sessionMap {}; { std::unique_lock lock {mutex}; sessionMap = &dbSessions[std::this_thread::get_id()]; } auto it {sessionMap->find(&db)}; if (it != std::end(*sessionMap)) return *it->second; auto res {sessionMap->emplace(&db, db.createSession())}; assert(res.second); LMS_LOG(API_SUBSONIC, DEBUG) << "Created db session"; return *(res.first->second); } static void clearDbSessions() { dbSessions.clear(); } static std::string makeNameFilesystemCompatible(const std::string& name) { return replaceInString(name, "/", "_"); } template std::vector getMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& paramName) { std::vector res; auto it = parameterMap.find(paramName); if (it == parameterMap.end()) return res; for (const std::string& param : it->second) { auto val {readAs(param)}; if (!val) { res.clear(); return res; } res.emplace_back(std::move(*val)); } return res; } template std::vector getMandatoryMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param) { std::vector res {getMultiParametersAs(parameterMap, param)}; if (res.empty()) throw Error {Error::Code::RequiredParameterMissing}; return res; } template boost::optional getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param) { std::vector params {getMultiParametersAs(parameterMap, param)}; if (params.size() != 1) return {}; return T { std::move(params.front()) }; } template T getMandatoryParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param) { auto res {getParameterAs(parameterMap, param)}; if (!res) throw Error {Error::Code::RequiredParameterMissing}; return *res; } static std::string decodePasswordIfNeeded(const std::string& password) { if (password.find("enc:") == 0) { auto decodedPassword {stringFromHex(password.substr(4))}; if (!decodedPassword) throw Error {Error::CustomType::BadPasswordFormat}; return *decodedPassword; } return password; } static ClientInfo getClientInfo(const Wt::Http::ParameterMap& parameters) { ClientInfo res; // Mandatory parameters res.version = getMandatoryParameterAs(parameters, "v"); if (res.version.major > API_VERSION_MAJOR) throw Error {Error::Code::ServerMustUpgrade}; if (res.version.major < API_VERSION_MAJOR) throw Error {Error::Code::ClientMustUpgrade}; if (res.version.minor > API_VERSION_MINOR) throw Error {Error::Code::ServerMustUpgrade}; res.name = getMandatoryParameterAs(parameters, "c"); res.user = getMandatoryParameterAs(parameters, "u"); res.password = decodePasswordIfNeeded(getMandatoryParameterAs(parameters, "p")); return res; } // MediaRetrievals struct MediaRetrievalResult { std::string mimeType; std::vector data; Wt::cpp17::any continuationData; }; SubsonicResource::SubsonicResource(Database::Database& db) : _db {db} { } SubsonicResource::~SubsonicResource() { LMS_LOG(API_SUBSONIC, DEBUG) << "Cleaning db sessions..."; clearDbSessions(); } static std::string parameterMapToDebugString(const Wt::Http::ParameterMap& parameterMap) { auto censorValue = [](const std::string& type, const std::string& value) -> std::string { if (type == "p" || type == "password") return "*SENSIBLE DATA*"; else return value; }; std::string res; for (const auto& params : parameterMap) { res += "{" + params.first + "="; if (params.second.size() == 1) { res += censorValue(params.first, params.second.front()); } else { res += "{"; for (const std::string& param : params.second) res += censorValue(params.first, param) + ","; res += "}"; } res += "}, "; } return res; } static void checkUserIsMySelfOrAdmin(RequestContext& context, const std::string& username) { if (username != context.userName) { User::pointer currentUser {User::getByLoginName(context.dbSession, context.userName)}; if (!currentUser) throw Error {Error::Code::RequestedDataNotFound}; if (!currentUser->isAdmin()) throw Error {Error::Code::UserNotAuthorized}; } } 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 Artist::pointer& artist) { return artist->getName(); }); return joinStrings(names, ", "); } static std::string getTrackPath(const Track::pointer& track) { std::string path; auto release {track->getRelease()}; if (release) { auto artists {release->getReleaseArtists()}; if (artists.empty()) artists = release->getArtists(); if (artists.size() > 1) path = "Various Artists/"; else if (artists.size() == 1) path = makeNameFilesystemCompatible(artists.front()->getName()) + "/"; path += makeNameFilesystemCompatible(track->getRelease()->getName()) + "/"; } if (track->getDiscNumber()) path += std::to_string(*track->getDiscNumber()) + "-"; if (track->getTrackNumber()) path += std::to_string(*track->getTrackNumber()) + "-"; return path + makeNameFilesystemCompatible(track->getName()) + "." + reportedFileSuffix; } static Response::Node trackToResponseNode(const Track::pointer& track, Session& dbSession, const User::pointer& user) { Response::Node trackResponse; trackResponse.setAttribute("id", IdToString({Id::Type::Track, track.id()})); trackResponse.setAttribute("isDir", "false"); trackResponse.setAttribute("title", track->getName()); if (track->getTrackNumber()) trackResponse.setAttribute("track", std::to_string(*track->getTrackNumber())); if (track->getDiscNumber()) trackResponse.setAttribute("discNumber", std::to_string(*track->getDiscNumber())); if (track->getYear()) trackResponse.setAttribute("year", std::to_string(*track->getYear())); trackResponse.setAttribute("size", std::to_string(reportedBitrate * 1000 / 8 * std::chrono::duration_cast(track->getDuration()).count())); trackResponse.setAttribute("coverArt", IdToString({Id::Type::Track, track.id()})); auto artists {track->getArtists()}; if (!artists.empty()) { trackResponse.setAttribute("artist", getArtistNames(artists)); if (artists.size() == 1) trackResponse.setAttribute("artistId", IdToString({Id::Type::Artist, artists.front().id()})); } if (track->getRelease()) { trackResponse.setAttribute("album", track->getRelease()->getName()); trackResponse.setAttribute("albumId", IdToString({Id::Type::Release, track->getRelease().id()})); trackResponse.setAttribute("parent", IdToString({Id::Type::Release, track->getRelease().id()})); } trackResponse.setAttribute("path", getTrackPath(track)); trackResponse.setAttribute("bitRate", std::to_string(reportedBitrate)); trackResponse.setAttribute("duration", std::to_string(std::chrono::duration_cast(track->getDuration()).count())); trackResponse.setAttribute("suffix", reportedFileSuffix); trackResponse.setAttribute("contentType", Av::encodingToMimetype(reportedEncoding)); trackResponse.setAttribute("type", "music"); if (user->hasStarredTrack(track)) trackResponse.setAttribute("starred", reportedStarredDate); // Report the first GENRE for this track ClusterType::pointer clusterType {ClusterType::getByName(dbSession, genreClusterName)}; if (clusterType) { auto clusters {track->getClusterGroups({clusterType}, 1)}; if (!clusters.empty() && !clusters.front().empty()) trackResponse.setAttribute("genre", clusters.front().front()->getName()); } return trackResponse; } static Response::Node releaseToResponseNode(const Release::pointer& release, Session& dbSession, const User::pointer& user, bool id3) { Response::Node albumNode; if (id3) { albumNode.setAttribute("name", release->getName()); albumNode.setAttribute("songCount", std::to_string(release->getTracksCount())); albumNode.setAttribute("duration", std::to_string(std::chrono::duration_cast(release->getDuration()).count())); } else { albumNode.setAttribute("title", release->getName()); albumNode.setAttribute("isDir", "true"); } albumNode.setAttribute("id", IdToString({Id::Type::Release, release.id()})); albumNode.setAttribute("coverArt", IdToString({Id::Type::Release, release.id()})); auto releaseYear {release->getReleaseYear()}; if (releaseYear) albumNode.setAttribute("year", std::to_string(*releaseYear)); auto artists {release->getReleaseArtists()}; if (artists.empty()) artists = release->getArtists(); if (artists.empty() && !id3) { albumNode.setAttribute("parent", IdToString({Id::Type::Root})); } else if (!artists.empty()) { if (artists.size() > 1) { albumNode.setAttribute("artist", "Various Artists"); if (!id3) albumNode.setAttribute("parent", IdToString({Id::Type::Root})); } else { albumNode.setAttribute("artist", artists.front()->getName()); if (id3) albumNode.setAttribute("artistId", IdToString({Id::Type::Artist, artists.front().id()})); else albumNode.setAttribute("parent", IdToString({Id::Type::Artist, artists.front().id()})); } } if (id3) { // Report the first GENRE for this track ClusterType::pointer clusterType {ClusterType::getByName(dbSession, genreClusterName)}; if (clusterType) { auto clusters {release->getClusterGroups({clusterType}, 1)}; if (!clusters.empty() && !clusters.front().empty()) albumNode.setAttribute("genre", clusters.front().front()->getName()); } } if (user->hasStarredRelease(release)) albumNode.setAttribute("starred", reportedStarredDate); return albumNode; } static Response::Node artistToResponseNode(const User::pointer& user, const Artist::pointer& artist, bool id3) { Response::Node artistNode; artistNode.setAttribute("id", IdToString({Id::Type::Artist, artist.id()})); artistNode.setAttribute("name", artist->getName()); if (id3) artistNode.setAttribute("albumCount", std::to_string(artist->getReleaseCount())); if (user->hasStarredArtist(artist)) artistNode.setAttribute("starred", reportedStarredDate); return artistNode; } static Response::Node clusterToResponseNode(const Cluster::pointer& cluster) { Response::Node clusterNode; clusterNode.setValue(cluster->getName()); clusterNode.setAttribute("songCount", std::to_string(cluster->getTracksCount())); clusterNode.setAttribute("albumCount", std::to_string(cluster->getReleasesCount())); return clusterNode; } static Response::Node 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"); Response::Node folder; folder.setValue("0"); userNode.addArrayChild("folder", std::move(folder)); return userNode; } static Response handlePingRequest(RequestContext& context) { return Response::createOkResponse(); } static Response handleChangePassword(RequestContext& context) { std::string username {getMandatoryParameterAs(context.parameters, "username")}; std::string password {decodePasswordIfNeeded(getMandatoryParameterAs(context.parameters, "password"))}; if (!getService()->evaluatePasswordStrength(username, password)) throw Error {Error::CustomType::PasswordTooWeak}; const User::PasswordHash hash {getService()->hashPassword(password)}; auto transaction {context.dbSession.createUniqueTransaction()}; checkUserIsMySelfOrAdmin(context, username); User::pointer user {User::getByLoginName(context.dbSession, username)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; user.modify()->setPasswordHash(hash); user.modify()->clearAuthTokens(); return Response::createOkResponse(); } static Response handleCreatePlaylistRequest(RequestContext& context) { // Optional params auto id {getParameterAs(context.parameters, "playlistId")}; if (id && id->type != Id::Type::Playlist) throw Error {Error::CustomType::BadIdFormat}; auto name {getParameterAs(context.parameters, "name")}; std::vector trackIds {getMultiParametersAs(context.parameters, "songId")}; if (!std::all_of(std::cbegin(trackIds), std::cend(trackIds ), [](const Id& id) { return id.type == Id::Type::Track; })) throw Error {Error::CustomType::BadIdFormat}; if (!name && !id) throw Error {Error::Code::RequiredParameterMissing}; auto transaction {context.dbSession.createUniqueTransaction()}; User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; TrackList::pointer tracklist; if (id) { tracklist = TrackList::getById(context.dbSession, id->value); if (!tracklist || tracklist->getUser() != user || tracklist->getType() != TrackList::Type::Playlist) { throw Error {Error::Code::RequestedDataNotFound}; } if (name) tracklist.modify()->setName(*name); } else { tracklist = TrackList::create(context.dbSession, *name, TrackList::Type::Playlist, false, user); } for (const Id& trackId : trackIds) { Track::pointer track {Track::getById(context.dbSession, trackId.value)}; if (!track) continue; TrackListEntry::create(context.dbSession, track, tracklist ); } return Response::createOkResponse(); } static Response handleCreateUserRequest(RequestContext& context) { std::string username {getMandatoryParameterAs(context.parameters, "username")}; std::string password {decodePasswordIfNeeded(getMandatoryParameterAs(context.parameters, "password"))}; // Just ignore all the other fields as we don't handle them if (!getService()->evaluatePasswordStrength(username, password)) throw Error {Error::CustomType::PasswordTooWeak}; const User::PasswordHash hash {getService()->hashPassword(password)}; auto transaction {context.dbSession.createUniqueTransaction()}; if (User::getByLoginName(context.dbSession, username) != User::pointer{}) throw Error {Error::CustomType::UserAlreadyExists}; User::pointer user {User::create(context.dbSession, username, hash)}; user.modify()->setMaxAudioTranscodeBitrate(128000); return Response::createOkResponse(); } static Response handleDeletePlaylistRequest(RequestContext& context) { Id id {getMandatoryParameterAs(context.parameters, "id")}; if (id.type != Id::Type::Playlist) throw Error {Error::CustomType::BadIdFormat}; auto transaction {context.dbSession.createUniqueTransaction()}; User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; TrackList::pointer tracklist {TrackList::getById(context.dbSession, id.value)}; if (!tracklist || tracklist->getUser() != user || tracklist->getType() != TrackList::Type::Playlist) { throw Error {Error::Code::RequestedDataNotFound}; } tracklist.remove(); return Response::createOkResponse(); } static Response handleDeleteUserRequest(RequestContext& context) { std::string username {getMandatoryParameterAs(context.parameters, "username")}; if (username == context.userName) throw Error {Error::Code::UserNotAuthorized}; auto transaction {context.dbSession.createUniqueTransaction()}; User::pointer user {User::getByLoginName(context.dbSession, username)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; user.remove(); return Response::createOkResponse(); } static Response handleGetLicenseRequest(RequestContext& context) { 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; } static Response handleGetRandomSongsRequest(RequestContext& context) { // Optional params std::size_t size {getParameterAs(context.parameters, "size").get_value_or(50)}; size = std::min(size, std::size_t {500}); auto transaction {context.dbSession.createSharedTransaction()}; User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; auto tracks {Track::getAllRandom(context.dbSession, size)}; Response response {Response::createOkResponse()}; Response::Node& randomSongsNode {response.createNode("randomSongs")}; for (const Track::pointer& track : tracks) randomSongsNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); return response; } static Response handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) { // Mandatory params std::string type {getMandatoryParameterAs(context.parameters, "type")}; // Optional params std::size_t size {getParameterAs(context.parameters, "size").get_value_or(10)}; std::size_t offset {getParameterAs(context.parameters, "offset").get_value_or(0)}; std::vector releases; auto transaction {context.dbSession.createSharedTransaction()}; User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; if (type == "random") { // Random results are paginated, but there is no acceptable way to handle the pagination params without repeating some albums releases = Release::getAllRandom(context.dbSession, size); } else if (type == "newest") { auto after {Wt::WLocalDateTime::currentServerDateTime().toUTC().addMonths(-6)}; releases = Release::getLastAdded(context.dbSession, after, offset, size); } else if (type == "alphabeticalByName") { releases = Release::getAll(context.dbSession, offset, size); } else if (type == "alphabeticalByArtist") { releases = Release::getAllOrderedByArtist(context.dbSession, offset, size); } else if (type == "byYear") { int fromYear {getMandatoryParameterAs(context.parameters, "fromYear")}; int toYear {getMandatoryParameterAs(context.parameters, "toYear")}; releases = Release::getByYear(context.dbSession, fromYear, toYear, offset, size); } else if (type == "starred") { releases = user->getStarredReleases(offset, size); } else if (type == "byGenre") { // Mandatory param std::string genre {getMandatoryParameterAs(context.parameters, "genre")}; ClusterType::pointer clusterType {ClusterType::getByName(context.dbSession, genreClusterName)}; if (clusterType) { Cluster::pointer cluster {clusterType->getCluster(genre)}; if (cluster) { bool more; releases = Release::getByFilter(context.dbSession, {cluster.id()}, {}, offset, size, more); } } } else throw Error {Error::CustomType::NotImplemented}; Response response {Response::createOkResponse()}; Response::Node& albumListNode {response.createNode(id3 ? "albumList2" : "albumList")}; for (const Release::pointer& release : releases) albumListNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3)); return response; } static Response handleGetAlbumListRequest(RequestContext& context) { return handleGetAlbumListRequestCommon(context, false /* no id3 */); } static Response handleGetAlbumList2Request(RequestContext& context) { return handleGetAlbumListRequestCommon(context, true /* id3 */); } static Response handleGetAlbumRequest(RequestContext& context) { // Mandatory params Id id {getMandatoryParameterAs(context.parameters, "id")}; if (id.type != Id::Type::Release) throw Error {Error::CustomType::BadIdFormat}; auto transaction {context.dbSession.createSharedTransaction()}; Release::pointer release {Release::getById(context.dbSession, id.value)}; if (!release) throw Error {Error::Code::RequestedDataNotFound}; User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; Response response {Response::createOkResponse()}; Response::Node releaseNode {releaseToResponseNode(release, context.dbSession, user, true /* id3 */)}; auto tracks {release->getTracks()}; for (const Track::pointer& track : tracks) releaseNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); response.addNode("album", std::move(releaseNode)); return response; } static Response handleGetArtistRequest(RequestContext& context) { // Mandatory params Id id {getMandatoryParameterAs(context.parameters, "id")}; if (id.type != Id::Type::Artist) throw Error {Error::CustomType::BadIdFormat}; auto transaction {context.dbSession.createSharedTransaction()}; Artist::pointer artist {Artist::getById(context.dbSession, id.value)}; User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!artist || !user) throw Error {Error::Code::RequestedDataNotFound}; Response response {Response::createOkResponse()}; Response::Node artistNode {artistToResponseNode(user, artist, true /* id3 */)}; auto releases {artist->getReleases()}; for (const Release::pointer& release : releases) artistNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, true /* id3 */)); response.addNode("artist", std::move(artistNode)); return response; } static Response handleGetArtistInfoRequestCommon(RequestContext& context, bool id3) { // Mandatory params Id id {getMandatoryParameterAs(context.parameters, "id")}; if (id.type != Id::Type::Artist) throw Error {Error::CustomType::BadIdFormat}; // Optional params std::size_t count {getParameterAs(context.parameters, "count").get_value_or(20)}; Response response {Response::createOkResponse()}; Response::Node& artistInfoNode {response.createNode(id3 ? "artistInfo2" : "artistInfo")}; { auto transaction {context.dbSession.createSharedTransaction()}; Artist::pointer artist {Artist::getById(context.dbSession, id.value)}; if (!artist) throw Error {Error::Code::RequestedDataNotFound}; if (!artist->getMBID().empty()) artistInfoNode.createChild("musicBrainzId").setValue(artist->getMBID()); } auto similarArtistsId {getService()->getSimilarArtists(context.dbSession, id.value, count)}; { auto transaction {context.dbSession.createSharedTransaction()}; User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; for ( const auto& similarArtistId : similarArtistsId ) { Artist::pointer similarArtist {Artist::getById(context.dbSession, similarArtistId)}; if (similarArtist) artistInfoNode.addArrayChild("similarArtist", artistToResponseNode(user, similarArtist, id3)); } } return response; } static Response handleGetArtistInfoRequest(RequestContext& context) { return handleGetArtistInfoRequestCommon(context, false /* no id3 */); } static Response handleGetArtistInfo2Request(RequestContext& context) { return handleGetArtistInfoRequestCommon(context, true /* id3 */); } static Response handleGetArtistsRequest(RequestContext& context) { Response response {Response::createOkResponse()}; Response::Node& artistsNode {response.createNode("artists")}; Response::Node& indexNode {artistsNode.createArrayChild("index")}; indexNode.setAttribute("name", "?"); auto transaction {context.dbSession.createSharedTransaction()}; User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; auto artists {Artist::getAll(context.dbSession)}; for (const Artist::pointer& artist : artists) indexNode.addArrayChild("artist", artistToResponseNode(user, artist, true /* id3 */)); return response; } static Response handleGetMusicDirectoryRequest(RequestContext& context) { // Mandatory params Id id {getMandatoryParameterAs(context.parameters, "id")}; Response response {Response::createOkResponse()}; Response::Node& directoryNode {response.createNode("directory")}; directoryNode.setAttribute("id", IdToString(id)); auto transaction {context.dbSession.createSharedTransaction()}; User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; switch (id.type) { case Id::Type::Root: { directoryNode.setAttribute("name", "Music"); auto artists {Artist::getAll(context.dbSession)}; for (const Artist::pointer& artist : artists) directoryNode.addArrayChild("child", artistToResponseNode(user, artist, false /* no id3 */)); break; } case Id::Type::Artist: { auto artist {Artist::getById(context.dbSession, id.value)}; if (!artist) throw Error {Error::Code::RequestedDataNotFound}; directoryNode.setAttribute("name", makeNameFilesystemCompatible(artist->getName())); auto releases {artist->getReleases()}; for (const Release::pointer& release : releases) directoryNode.addArrayChild("child", releaseToResponseNode(release, context.dbSession, user, false /* no id3 */)); break; } case Id::Type::Release: { auto release {Release::getById(context.dbSession, id.value)}; if (!release) throw Error {Error::Code::RequestedDataNotFound}; directoryNode.setAttribute("name", makeNameFilesystemCompatible(release->getName())); auto tracks {release->getTracks()}; for (const Track::pointer& track : tracks) directoryNode.addArrayChild("child", trackToResponseNode(track, context.dbSession, user)); break; } default: throw Error {Error::CustomType::BadIdFormat}; } return response; } static Response handleGetMusicFoldersRequest(RequestContext& context) { Response response {Response::createOkResponse()}; Response::Node& musicFoldersNode {response.createNode("musicFolders")}; Response::Node& musicFolderNode {musicFoldersNode.createArrayChild("musicFolder")}; musicFolderNode.setAttribute("id", "0"); musicFolderNode.setAttribute("name", "Music"); return response; } static Response handleGetGenresRequest(RequestContext& context) { Response response {Response::createOkResponse()}; Response::Node& genresNode {response.createNode("genres")}; auto transaction {context.dbSession.createSharedTransaction()}; const ClusterType::pointer clusterType {ClusterType::getByName(context.dbSession, genreClusterName)}; if (clusterType) { const auto clusters {clusterType->getClusters()}; for (const Cluster::pointer& cluster : clusters) genresNode.addArrayChild("genre", clusterToResponseNode(cluster)); } return response; } static Response handleGetIndexesRequest(RequestContext& context) { Response response {Response::createOkResponse()}; Response::Node& artistsNode {response.createNode("indexes")}; Response::Node& indexNode {artistsNode.createArrayChild("index")}; indexNode.setAttribute("name", "?"); auto transaction {context.dbSession.createSharedTransaction()}; User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; auto artists {Artist::getAll(context.dbSession)}; for (const Artist::pointer& artist : artists) indexNode.addArrayChild("artist", artistToResponseNode(user, artist, false /* no id3 */)); return response; } static Response handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) { // Mandatory params Id id {getMandatoryParameterAs(context.parameters, "id")}; if (id.type != Id::Type::Artist) throw Error {Error::CustomType::BadIdFormat}; // Optional params std::size_t count {getParameterAs(context.parameters, "count").get_value_or(50)}; auto transaction {context.dbSession.createSharedTransaction()}; Artist::pointer artist {Artist::getById(context.dbSession, id.value)}; User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user || !artist) throw Error {Error::Code::RequestedDataNotFound}; // "Returns a random collection of songs from the given artist and similar artists" auto tracks {artist->getRandomTracks(count / 2)}; auto similarArtistsId {getService()->getSimilarArtists(context.dbSession, artist.id(), 5)}; for ( const auto& similarArtistId : similarArtistsId ) { Artist::pointer similarArtist {Artist::getById(context.dbSession, similarArtistId)}; if (!similarArtist) continue; auto similarArtistTracks {similarArtist->getRandomTracks((count / 2) / 5)}; tracks.insert(tracks.end(), std::make_move_iterator(std::begin(similarArtistTracks)), std::make_move_iterator(std::end(similarArtistTracks))); } auto now {std::chrono::system_clock::now()}; std::mt19937 randGenerator {static_cast(std::chrono::duration_cast(now.time_since_epoch()).count())}; std::shuffle(std::begin(tracks), std::end(tracks), randGenerator); Response response {Response::createOkResponse()}; Response::Node& similarSongsNode {response.createNode(id3 ? "similarSongs2" : "similarSongs")}; for (const Track::pointer& track : tracks) similarSongsNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); return response; } static Response handleGetSimilarSongsRequest(RequestContext& context) { return handleGetSimilarSongsRequestCommon(context, false /* no id3 */); } static Response handleGetSimilarSongs2Request(RequestContext& context) { return handleGetSimilarSongsRequestCommon(context, true /* id3 */); } static Response handleGetStarredRequestCommon(RequestContext& context, bool id3) { auto transaction {context.dbSession.createSharedTransaction()}; User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; Response response {Response::createOkResponse()}; Response::Node& starredNode {response.createNode(id3 ? "starred2" : "starred")}; { auto artists {user->getStarredArtists()}; for (const Artist::pointer& artist : artists) starredNode.addArrayChild("artist", artistToResponseNode(user, artist, id3)); } { auto releases {user->getStarredReleases()}; for (const Release::pointer& release : releases) starredNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3)); } { auto tracks {user->getStarredTracks()}; for (const Track::pointer& track : tracks) starredNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); } return response; } static Response handleGetStarredRequest(RequestContext& context) { return handleGetStarredRequestCommon(context, false /* no id3 */); } static Response handleGetStarred2Request(RequestContext& context) { return handleGetStarredRequestCommon(context, true /* id3 */); } static Response::Node tracklistToResponseNode(const TrackList::pointer& tracklist, Session& dbSession) { Response::Node playlistNode; playlistNode.setAttribute("id", IdToString({Id::Type::Playlist, tracklist.id()})); playlistNode.setAttribute("name", tracklist->getName()); playlistNode.setAttribute("songCount", std::to_string(tracklist->getCount())); playlistNode.setAttribute("duration", std::to_string(std::chrono::duration_cast(tracklist->getDuration()).count())); playlistNode.setAttribute("public", tracklist->isPublic() ? "true" : "false"); playlistNode.setAttribute("created", ""); playlistNode.setAttribute("owner", tracklist->getUser()->getLoginName()); return playlistNode; } static Response handleGetPlaylistRequest(RequestContext& context) { // Mandatory params Id id {getMandatoryParameterAs(context.parameters, "id")}; if (id.type != Id::Type::Playlist) throw Error {Error::CustomType::BadIdFormat}; auto transaction {context.dbSession.createSharedTransaction()}; User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; TrackList::pointer tracklist {TrackList::getById(context.dbSession, id.value)}; if (!user || !tracklist) throw Error {Error::Code::RequestedDataNotFound}; Response response {Response::createOkResponse()}; Response::Node playlistNode {tracklistToResponseNode(tracklist, context.dbSession)}; auto entries {tracklist->getEntries()}; for (const TrackListEntry::pointer& entry : entries) playlistNode.addArrayChild("entry", trackToResponseNode(entry->getTrack(), context.dbSession, user)); response.addNode("playlist", playlistNode ); return response; } static Response handleGetPlaylistsRequest(RequestContext& context) { auto transaction {context.dbSession.createSharedTransaction()}; User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; Response response {Response::createOkResponse()}; Response::Node& playlistsNode {response.createNode("playlists")}; auto tracklists {TrackList::getAll(context.dbSession, user, TrackList::Type::Playlist)}; for (const TrackList::pointer& tracklist : tracklists) playlistsNode.addArrayChild("playlist", tracklistToResponseNode(tracklist, context.dbSession)); return response; } static Response handleGetSongsByGenreRequest(RequestContext& context) { // Mandatory params std::string genre {getMandatoryParameterAs(context.parameters, "genre")}; // Optional params std::size_t size {getParameterAs(context.parameters, "count").get_value_or(10)}; size = std::min(size, std::size_t {500}); std::size_t offset {getParameterAs(context.parameters, "offset").get_value_or(0)}; auto transaction {context.dbSession.createSharedTransaction()}; auto clusterType {ClusterType::getByName(context.dbSession, genreClusterName)}; if (!clusterType) throw Error {Error::Code::RequestedDataNotFound}; auto cluster {clusterType->getCluster(genre)}; if (!cluster) throw Error {Error::Code::RequestedDataNotFound}; User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; Response response {Response::createOkResponse()}; Response::Node& songsByGenreNode {response.createNode("songsByGenre")}; bool more; auto tracks {Track::getByFilter(context.dbSession, {cluster.id()}, {}, offset, size, more)}; for (const Track::pointer& track : tracks) songsByGenreNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); return response; } static Response handleGetUserRequest(RequestContext& context) { std::string username {getMandatoryParameterAs(context.parameters, "username")}; auto transaction {context.dbSession.createSharedTransaction()}; checkUserIsMySelfOrAdmin(context, username); const User::pointer user {User::getByLoginName(context.dbSession, username)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; Response response {Response::createOkResponse()}; response.addNode("user", userToResponseNode(user)); return response; } static Response handleGetUsersRequest(RequestContext& context) { auto transaction {context.dbSession.createSharedTransaction()}; Response response {Response::createOkResponse()}; Response::Node& usersNode {response.createNode("users")}; const auto users {User::getAll(context.dbSession)}; for (const User::pointer& user : users) usersNode.addArrayChild("user", userToResponseNode(user)); return response; } static Response handleSearchRequestCommon(RequestContext& context, bool id3) { // Mandatory params std::string query {getMandatoryParameterAs(context.parameters, "query")}; std::vector keywords {splitString(query, " ")}; // Optional params std::size_t artistCount {getParameterAs(context.parameters, "artistCount").get_value_or(20)}; std::size_t artistOffset {getParameterAs(context.parameters, "artistOffset").get_value_or(0)}; std::size_t albumCount {getParameterAs(context.parameters, "albumCount").get_value_or(20)}; std::size_t albumOffset {getParameterAs(context.parameters, "albumOffset").get_value_or(0)}; std::size_t songCount {getParameterAs(context.parameters, "songCount").get_value_or(20)}; std::size_t songOffset {getParameterAs(context.parameters, "songOffset").get_value_or(0)}; auto transaction {context.dbSession.createSharedTransaction()}; User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; Response response {Response::createOkResponse()}; Response::Node& searchResult2Node {response.createNode(id3 ? "searchResult3" : "searchResult2")}; bool more; { auto artists {Artist::getByFilter(context.dbSession, {}, keywords, artistOffset, artistCount, more)}; for (const Artist::pointer& artist : artists) searchResult2Node.addArrayChild("artist", artistToResponseNode(user, artist, id3)); } { auto releases {Release::getByFilter(context.dbSession, {}, keywords, albumOffset, albumCount, more)}; for (const Release::pointer& release : releases) searchResult2Node.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3)); } { auto tracks {Track::getByFilter(context.dbSession, {}, keywords, songOffset, songCount, more)}; for (const Track::pointer& track : tracks) searchResult2Node.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); } return response; } struct StarParameters { std::vector artistIds; std::vector releaseIds; std::vector trackIds; }; static StarParameters getStarParameters(const Wt::Http::ParameterMap& parameters) { StarParameters res; std::vector ids {getMultiParametersAs(parameters, "id")}; res.artistIds = getMultiParametersAs(parameters, "artistId"); res.releaseIds = getMultiParametersAs(parameters, "albumId"); if (!std::all_of(std::cbegin(res.releaseIds ), std::cend(res.releaseIds ), [](const Id& id) { return id.type == Id::Type::Release; })) throw Error {Error::CustomType::BadIdFormat}; if (!std::all_of(std::cbegin(res.artistIds ), std::cend(res.artistIds ), [](const Id& id) { return id.type == Id::Type::Artist; })) throw Error {Error::CustomType::BadIdFormat}; // Redispatch the old "id" parameter in new lists for (const Id& id : ids) { switch (id.type) { case Id::Type::Artist: res.artistIds.emplace_back(id); break; case Id::Type::Release: res.releaseIds.emplace_back(id); break; case Id::Type::Track: res.trackIds.emplace_back(id); break; default: throw Error {Error::CustomType::BadIdFormat}; } } return res; } static Response handleStarRequest(RequestContext& context) { StarParameters params {getStarParameters(context.parameters)}; auto transaction {context.dbSession.createSharedTransaction()}; User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; for (const Id& id : params.artistIds) { Artist::pointer artist {Artist::getById(context.dbSession, id.value)}; if (!artist) continue; user.modify()->starArtist(artist); } for (const Id& id : params.releaseIds) { Release::pointer release {Release::getById(context.dbSession, id.value)}; if (!release) continue; user.modify()->starRelease(release); } for (const Id& id : params.trackIds) { Track::pointer track {Track::getById(context.dbSession, id.value)}; if (!track) continue; user.modify()->starTrack(track); } return Response::createOkResponse(); } static Response handleSearch2Request(RequestContext& context) { return handleSearchRequestCommon(context, false /* no id3 */); } static Response handleSearch3Request(RequestContext& context) { return handleSearchRequestCommon(context, true /* id3 */); } static Response handleUnstarRequest(RequestContext& context) { StarParameters params {getStarParameters(context.parameters)}; auto transaction {context.dbSession.createSharedTransaction()}; User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; for (const Id& id : params.artistIds) { Artist::pointer artist {Artist::getById(context.dbSession, id.value)}; if (!artist) continue; user.modify()->unstarArtist(artist); } for (const Id& id : params.releaseIds) { Release::pointer release {Release::getById(context.dbSession, id.value)}; if (!release) continue; user.modify()->unstarRelease(release); } for (const Id& id : params.trackIds) { Track::pointer track {Track::getById(context.dbSession, id.value)}; if (!track) continue; user.modify()->unstarTrack(track); } return Response::createOkResponse(); } static Response handleUpdateUserRequest(RequestContext& context) { std::string username {getMandatoryParameterAs(context.parameters, "username")}; boost::optional password {getParameterAs(context.parameters, "password")}; boost::optional maxBitRate {getParameterAs(context.parameters, "maxBitRate")}; User::PasswordHash hash; if (password) { *password = decodePasswordIfNeeded(*password); if (!getService()->evaluatePasswordStrength(username, *password)) throw Error {Error::CustomType::PasswordTooWeak}; hash = getService()->hashPassword(*password); } auto transaction {context.dbSession.createUniqueTransaction()}; User::pointer user {User::getByLoginName(context.dbSession, username)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; if (maxBitRate) { if (*maxBitRate == 0) *maxBitRate = 320; user.modify()->setMaxAudioTranscodeBitrate(*maxBitRate * 1000); } if (password) { user.modify()->setPasswordHash(hash); user.modify()->clearAuthTokens(); } return Response::createOkResponse(); } static Response handleUpdatePlaylistRequest(RequestContext& context) { // Mandatory params Id id {getMandatoryParameterAs(context.parameters, "playlistId")}; if (id.type != Id::Type::Playlist) throw Error {Error::CustomType::BadIdFormat}; // Optional parameters auto name {getParameterAs(context.parameters, "name")}; auto isPublic {getParameterAs(context.parameters, "public")}; std::vector trackIdsToAdd {getMultiParametersAs(context.parameters, "songIdToAdd")}; if (!std::all_of(std::cbegin(trackIdsToAdd), std::cend(trackIdsToAdd), [](const Id& id) { return id.type == Id::Type::Track; })) throw Error {Error::CustomType::BadIdFormat}; std::vector trackPositionsToRemove {getMultiParametersAs(context.parameters, "songIndexToRemove")}; auto transaction {context.dbSession.createSharedTransaction()}; User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; TrackList::pointer tracklist {TrackList::getById(context.dbSession, id.value)}; if (!tracklist || tracklist->getUser() != user || tracklist->getType() != TrackList::Type::Playlist) { throw Error {Error::Code::RequestedDataNotFound}; } if (name) tracklist.modify()->setName(*name); if (isPublic) tracklist.modify()->setIsPublic(*isPublic); { // Remove from end to make indexes stable std::sort(std::begin(trackPositionsToRemove), std::end(trackPositionsToRemove), std::greater()); for (std::size_t trackPositionToRemove : trackPositionsToRemove) { auto entry {tracklist->getEntry(trackPositionToRemove)}; if (entry) entry.remove(); } } // Add tracks for (const Id& trackIdToAdd : trackIdsToAdd) { Track::pointer track {Track::getById(context.dbSession, trackIdToAdd.value)}; if (!track) continue; TrackListEntry::create(context.dbSession, track, tracklist ); } return Response::createOkResponse(); } static std::shared_ptr createTranscoder(RequestContext& context) { // Mandatory params Id id {getMandatoryParameterAs(context.parameters, "id")}; // Optional params boost::optional maxBitRate {getParameterAs(context.parameters, "maxBitRate")}; boost::filesystem::path trackPath; { auto transaction {context.dbSession.createSharedTransaction()}; User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; // "If set to zero, no limit is imposed" if (!maxBitRate || *maxBitRate == 0) maxBitRate = user->getAudioTranscodeBitrate() / 1000; *maxBitRate = clamp(*maxBitRate, std::size_t {48}, user->getMaxAudioTranscodeBitrate() / 1000); auto track {Track::getById(context.dbSession, id.value)}; if (!track) throw Error {Error::Code::RequestedDataNotFound}; trackPath = track->getPath(); } Av::TranscodeParameters parameters {}; parameters.stripMetadata = false; // Since it can be cached and some players read the metadata from the downloaded file parameters.bitrate = *maxBitRate * 1000; parameters.encoding = transcodeEncoding; return std::make_shared(trackPath, parameters); } static MediaRetrievalResult handleStream(RequestContext& context, Wt::Http::ResponseContinuation* continuation) { MediaRetrievalResult res; // TODO store only weak ptrs and use a ring container to store shared_ptr? std::shared_ptr transcoder; if (!continuation) { transcoder = createTranscoder(context); transcoder->start(); res.mimeType = transcoder->getOutputMimeType(); LMS_LOG(API_SUBSONIC, DEBUG) << "Mime type set to '" << transcoder->getOutputMimeType() << "'"; } else { transcoder = Wt::cpp17::any_cast>(continuation->data()); } if (!transcoder) throw Error {Error::CustomType::InternalError}; if (!transcoder->isComplete()) { static constexpr std::size_t chunkSize {65536*4}; res.data.reserve(chunkSize); transcoder->process(res.data, chunkSize); } if (!transcoder->isComplete()) res.continuationData = std::move(transcoder); return res; } static MediaRetrievalResult handleGetCoverArt(RequestContext& context, Wt::Http::ResponseContinuation*) { // Mandatory params Id id {getMandatoryParameterAs(context.parameters, "id")}; std::size_t size {getParameterAs(context.parameters, "size").get_value_or(256)}; size = clamp(size, std::size_t {32}, std::size_t {1024}); MediaRetrievalResult res; switch (id.type) { case Id::Type::Track: res.data = getService()->getFromTrack(context.dbSession, id.value, Image::Format::JPEG, size); break; case Id::Type::Release: res.data = getService()->getFromRelease(context.dbSession, id.value, Image::Format::JPEG, size); break; default: throw Error {Error::CustomType::BadIdFormat}; } res.mimeType = Image::format_to_mimeType(Image::Format::JPEG); return res; } using RequestHandlerFunc = std::function; struct RequestEntryPointInfo { RequestHandlerFunc func; bool mustBeAdmin; }; static std::map requestEntryPoints { {"changePassword", {handleChangePassword, false}}, {"createPlaylist", {handleCreatePlaylistRequest, false}}, {"createUser", {handleCreateUserRequest, true}}, {"deletePlaylist", {handleDeletePlaylistRequest, false}}, {"deleteUser", {handleDeleteUserRequest, true}}, {"getAlbumList", {handleGetAlbumListRequest, false}}, {"getAlbumList2", {handleGetAlbumList2Request, false}}, {"getAlbum", {handleGetAlbumRequest, false}}, {"getArtist", {handleGetArtistRequest, false}}, {"getArtistInfo", {handleGetArtistInfoRequest, false}}, {"getArtistInfo2", {handleGetArtistInfo2Request, false}}, {"getArtists", {handleGetArtistsRequest, false}}, {"getGenres", {handleGetGenresRequest, false}}, {"getIndexes", {handleGetIndexesRequest, false}}, {"getLicense", {handleGetLicenseRequest, false}}, {"getMusicDirectory", {handleGetMusicDirectoryRequest, false}}, {"getMusicFolders", {handleGetMusicFoldersRequest, false}}, {"getRandomSongs", {handleGetRandomSongsRequest, false}}, {"getSimilarSongs", {handleGetSimilarSongsRequest, false}}, {"getSimilarSongs2", {handleGetSimilarSongs2Request, false}}, {"getStarred", {handleGetStarredRequest, false}}, {"getStarred2", {handleGetStarred2Request, false}}, {"getPlaylist", {handleGetPlaylistRequest, false}}, {"getPlaylists", {handleGetPlaylistsRequest, false}}, {"getSongsByGenre", {handleGetSongsByGenreRequest, false}}, {"getUser", {handleGetUserRequest, false}}, {"getUsers", {handleGetUsersRequest, true}}, {"ping", {handlePingRequest, false}}, {"search2", {handleSearch2Request, false}}, {"search3", {handleSearch3Request, false}}, {"star", {handleStarRequest, false}}, {"unstar", {handleUnstarRequest, false}}, {"updateUser", {handleUpdateUserRequest, true}}, {"updatePlaylist", {handleUpdatePlaylistRequest, false}}, }; using MediaRetrievalHandlerFunc = std::function; static std::map mediaRetrievalHandlers { {"/rest/stream", handleStream}, {"/rest/getCoverArt", handleGetCoverArt}, }; void SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) { static std::atomic curRequestId {}; const std::size_t requestId {curRequestId++}; LMS_LOG(API_SUBSONIC, DEBUG) << "Handling request " << requestId << " '" << request.pathInfo() << "', continuation = " << (request.continuation() ? "true" : "false") << ", params = " << parameterMapToDebugString(request.getParameterMap()); std::string requestPath {request.pathInfo()}; if (stringEndsWith(requestPath, ".view")) requestPath.resize(requestPath.length() - 5); const Wt::Http::ParameterMap& parameters {request.getParameterMap()}; // Optional parameters ResponseFormat format {getParameterAs(parameters, "f").get_value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml}; try { // Mandatory parameters const ClientInfo clientInfo {getClientInfo(parameters)}; Session& dbSession {getOrCreateDbSession(_db)}; switch (getService()->checkUserPassword(dbSession, boost::asio::ip::address::from_string(request.clientAddress()), clientInfo.user, clientInfo.password)) { case Auth::PasswordService::PasswordCheckResult::Match: break; case Auth::PasswordService::PasswordCheckResult::Mismatch: throw Error {Error::Code::WrongUsernameOrPassword}; case Auth::PasswordService::PasswordCheckResult::Throttled: throw Error {Error::CustomType::LoginThrottled}; } RequestContext requestContext {.parameters = parameters, .dbSession = dbSession, .userName = clientInfo.user}; auto itEntryPoint {requestEntryPoints.find(requestPath)}; if (itEntryPoint != requestEntryPoints.end()) { if (itEntryPoint->second.mustBeAdmin) { auto transaction {dbSession.createSharedTransaction()}; User::pointer user {User::getByLoginName(dbSession, clientInfo.user)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; if (!user->isAdmin()) throw Error {Error::Code::UserNotAuthorized}; } Response resp {(itEntryPoint->second.func)(requestContext)}; resp.write(response.out(), format); response.setMimeType(ResponseFormatToMimeType(format)); LMS_LOG(API_SUBSONIC, DEBUG) << "Request " << requestId << " '" << requestPath << "' handled!"; return; } auto itStreamHandler {mediaRetrievalHandlers.find(requestPath)}; if (itStreamHandler != mediaRetrievalHandlers.end()) { MediaRetrievalResult res {itStreamHandler->second(requestContext, request.continuation())}; if (!res.mimeType.empty()) response.setMimeType(res.mimeType); if (!res.data.empty()) { response.out().write(reinterpret_cast(&res.data[0]), res.data.size()); if (!response.out()) { LMS_LOG(API_SUBSONIC, ERROR) << "Write failed!"; return; } } if (res.continuationData.has_value()) { auto continuation {response.createContinuation()}; continuation->setData(std::move(res.continuationData)); } LMS_LOG(API_SUBSONIC, DEBUG) << "Request " << requestId << " '" << requestPath << "' handled!"; return; } LMS_LOG(API_SUBSONIC, ERROR) << "Unhandled command '" << requestPath << "'"; throw Error {Error::CustomType::NotImplemented}; } catch (const Error& e) { LMS_LOG(API_SUBSONIC, ERROR) << "Error while processing request '" << requestPath << "'" << ", params = [" << parameterMapToDebugString(request.getParameterMap()) << "]" << ", code = " << static_cast(e.getCode()) << ", msg = '" << e.getMessage() << "'"; Response resp {Response::createFailedResponse(e)}; resp.write(response.out(), format); response.setMimeType(ResponseFormatToMimeType(format)); } } } // namespace api::subsonic