Files
lms/src/api/subsonic/SubsonicResource.cpp
T

1704 lines
52 KiB
C++

/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "SubsonicResource.hpp"
#include <atomic>
#include <mutex>
#include <numeric>
#include <random>
#include <Wt/Auth/Identity.h>
#include <Wt/WLocalDateTime.h>
#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 "main/Service.hpp"
#include "similarity/SimilaritySearcher.hpp"
#include "utils/Logger.hpp"
#include "utils/Utils.hpp"
#include "SubsonicId.hpp"
#include "SubsonicResponse.hpp"
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"};
// Requests
#define PING_URL "/rest/ping.view"
#define CREATE_PLAYLIST_URL "/rest/createPlaylist.view"
#define DELETE_PLAYLIST_URL "/rest/deletePlaylist.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_ALBUM_LIST2_URL "/rest/getAlbumList2.view"
#define GET_ALBUM_URL "/rest/getAlbum.view"
#define GET_ARTIST_URL "/rest/getArtist.view"
#define GET_ARTIST_INFO_URL "/rest/getArtistInfo.view"
#define GET_ARTIST_INFO2_URL "/rest/getArtistInfo2.view"
#define GET_ARTISTS_URL "/rest/getArtists.view"
#define GET_MUSIC_DIRECTORY_URL "/rest/getMusicDirectory.view"
#define GET_MUSIC_FOLDERS_URL "/rest/getMusicFolders.view"
#define GET_GENRES_URL "/rest/getGenres.view"
#define GET_INDEXES_URL "/rest/getIndexes.view"
#define GET_SIMILAR_SONGS_URL "/rest/getSimilarSongs.view"
#define GET_SIMILAR_SONGS2_URL "/rest/getSimilarSongs2.view"
#define GET_STARRED_URL "/rest/getStarred.view"
#define GET_STARRED2_URL "/rest/getStarred2.view"
#define GET_PLAYLIST_URL "/rest/getPlaylist.view"
#define GET_PLAYLISTS_URL "/rest/getPlaylists.view"
#define GET_SONGS_BY_GENRE_URL "/rest/getSongsByGenre.view"
#define SEARCH2_URL "/rest/search2.view"
#define SEARCH3_URL "/rest/search3.view"
#define STAR_URL "/rest/star.view"
#define UNSTAR_URL "/rest/unstar.view"
#define UPDATE_PLAYLIST_URL "/rest/updatePlaylist.view"
// MediaRetrievals
#define STREAM_URL "/rest/stream.view"
#define GET_COVER_ART_URL "/rest/getCoverArt.view"
template<>
boost::optional<API::Subsonic::Id>
readAs(const std::string& str)
{
return API::Subsonic::IdFromString(str);
}
template<>
boost::optional<bool>
readAs(const std::string& str)
{
if (str == "true")
return true;
else if (str == "false")
return false;
return {};
}
namespace API::Subsonic
{
struct ClientInfo
{
std::string name;
std::string user;
std::string password;
};
struct RequestContext
{
const Wt::Http::ParameterMap& parameters;
Database::Handler& db;
std::string userName;
};
// requests
using RequestHandlerFunc = std::function<Response(RequestContext& context)>;
static Response handlePingRequest(RequestContext& context);
static Response handleCreatePlaylistRequest(RequestContext& context);
static Response handleDeletePlaylistRequest(RequestContext& context);
static Response handleGetLicenseRequest(RequestContext& context);
static Response handleGetRandomSongsRequest(RequestContext& context);
static Response handleGetAlbumListRequest(RequestContext& context);
static Response handleGetAlbumList2Request(RequestContext& context);
static Response handleGetAlbumRequest(RequestContext& context);
static Response handleGetArtistRequest(RequestContext& context);
static Response handleGetArtistInfoRequest(RequestContext& context);
static Response handleGetArtistInfo2Request(RequestContext& context);
static Response handleGetArtistsRequest(RequestContext& context);
static Response handleGetMusicDirectoryRequest(RequestContext& context);
static Response handleGetMusicFoldersRequest(RequestContext& context);
static Response handleGetGenresRequest(RequestContext& context);
static Response handleGetIndexesRequest(RequestContext& context);
static Response handleGetSimilarSongsRequest(RequestContext& context);
static Response handleGetSimilarSongs2Request(RequestContext& context);
static Response handleGetStarredRequest(RequestContext& context);
static Response handleGetStarred2Request(RequestContext& context);
static Response handleGetPlaylistRequest(RequestContext& context);
static Response handleGetPlaylistsRequest(RequestContext& context);
static Response handleGetSongsByGenreRequest(RequestContext& context);
static Response handleSearch2Request(RequestContext& context);
static Response handleSearch3Request(RequestContext& context);
static Response handleStarRequest(RequestContext& context);
static Response handleUnstarRequest(RequestContext& context);
static Response handleUpdatePlaylistRequest(RequestContext& context);
// MediaRetrievals
struct MediaRetrievalResult
{
std::string mimeType;
std::vector<uint8_t> data;
Wt::cpp17::any continuationData;
};
using MediaRetrievalHandlerFunc = std::function<MediaRetrievalResult(RequestContext&, Wt::Http::ResponseContinuation*)>;
MediaRetrievalResult handleStream(RequestContext& context, Wt::Http::ResponseContinuation* continuation);
MediaRetrievalResult handleGetCoverArt(RequestContext& context, Wt::Http::ResponseContinuation* continuation);
static std::map<std::string, RequestHandlerFunc> requestHandlers
{
{PING_URL, handlePingRequest},
{CREATE_PLAYLIST_URL, handleCreatePlaylistRequest},
{DELETE_PLAYLIST_URL, handleDeletePlaylistRequest},
{GET_LICENSE_URL, handleGetLicenseRequest},
{GET_RANDOM_SONGS_URL, handleGetRandomSongsRequest},
{GET_ALBUM_LIST_URL, handleGetAlbumListRequest},
{GET_ALBUM_LIST2_URL, handleGetAlbumList2Request},
{GET_ALBUM_URL, handleGetAlbumRequest},
{GET_ARTIST_URL, handleGetArtistRequest},
{GET_ARTIST_INFO_URL, handleGetArtistInfoRequest},
{GET_ARTIST_INFO2_URL, handleGetArtistInfo2Request},
{GET_ARTISTS_URL, handleGetArtistsRequest},
{GET_MUSIC_DIRECTORY_URL, handleGetMusicDirectoryRequest},
{GET_MUSIC_FOLDERS_URL, handleGetMusicFoldersRequest},
{GET_GENRES_URL, handleGetGenresRequest},
{GET_INDEXES_URL, handleGetIndexesRequest},
{GET_SIMILAR_SONGS_URL, handleGetSimilarSongsRequest},
{GET_SIMILAR_SONGS2_URL, handleGetSimilarSongs2Request},
{GET_STARRED_URL, handleGetStarredRequest},
{GET_STARRED2_URL, handleGetStarred2Request},
{GET_PLAYLIST_URL, handleGetPlaylistRequest},
{GET_PLAYLISTS_URL, handleGetPlaylistsRequest},
{GET_SONGS_BY_GENRE_URL, handleGetSongsByGenreRequest},
{STAR_URL, handleStarRequest},
{SEARCH2_URL, handleSearch2Request},
{SEARCH3_URL, handleSearch3Request},
{UNSTAR_URL, handleUnstarRequest},
{UPDATE_PLAYLIST_URL, handleUpdatePlaylistRequest},
};
static std::map<std::string, MediaRetrievalHandlerFunc> mediaRetrievalHandlers
{
{STREAM_URL, handleStream},
{GET_COVER_ART_URL, handleGetCoverArt},
};
static
std::string
makeNameFilesystemCompatible(const std::string& name)
{
return replaceInString(name, "/", "_");
}
template<typename T>
std::vector<T>
getMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
{
std::vector<T> res;
auto it = parameterMap.find(param);
if (it == parameterMap.end())
return res;
for (const std::string& param : it->second)
{
auto val {readAs<T>(param)};
if (!val)
{
res.clear();
return res;
}
res.emplace_back(std::move(*val));
}
return res;
}
template<typename T>
std::vector<T>
getMandatoryMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
{
std::vector<T> res {getMultiParametersAs<T>(parameterMap, param)};
if (res.empty())
throw Error {Error::Code::RequiredParameterMissing};
return res;
}
template<typename T>
boost::optional<T>
getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
{
std::vector<T> params {getMultiParametersAs<T>(parameterMap, param)};
if (params.size() != 1)
return {};
return T { std::move(params.front()) };
}
template<typename T>
T
getMandatoryParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
{
auto res {getParameterAs<T>(parameterMap, param)};
if (!res)
throw Error {Error::Code::RequiredParameterMissing};
return *res;
}
ClientInfo
getClientInfo(const Wt::Http::ParameterMap& parameters)
{
ClientInfo res;
// Mandatory parameters
res.name = getMandatoryParameterAs<std::string>(parameters, "c");
res.user = getMandatoryParameterAs<std::string>(parameters, "u");
{
std::string password {getMandatoryParameterAs<std::string>(parameters, "p")};
if (password.find("enc:") == 0)
{
auto decodedPassword {stringFromHex(password.substr(4))};
if (!decodedPassword)
throw Error {Error::Code::WrongUsernameOrPassword};
res.password = *decodedPassword;
}
else
res.password = password;
}
return res;
}
static
bool
checkPassword(Database::Handler& db, const ClientInfo& clientInfo)
{
auto authUser {db.getUserDatabase().findWithIdentity(Wt::Auth::Identity::LoginName, clientInfo.user)};
if (!authUser.isValid())
{
LMS_LOG(API_SUBSONIC, ERROR) << "Cannot find user '" << clientInfo.user << "'";
return false;
}
return db.getPasswordService().verifyPassword(authUser, clientInfo.password) == Wt::Auth::PasswordResult::PasswordValid;
}
SubsonicResource::SubsonicResource(Wt::Dbo::SqlConnectionPool& connectionPool)
: _db {connectionPool}
{
}
std::vector<std::string>
SubsonicResource::getPaths()
{
std::vector<std::string> paths;
for (auto it : requestHandlers)
paths.emplace_back(it.first);
for (auto it : mediaRetrievalHandlers)
paths.emplace_back(it.first);
return paths;
}
static
std::string parameterMapToDebugString(const Wt::Http::ParameterMap& parameterMap)
{
auto censorValue = [](const std::string& type, const std::string& value)
{
if (type == "p")
return std::string {"CENSORED"};
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;
}
void
SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response)
{
static std::atomic<std::size_t> curRequestId{};
const std::size_t requestId {curRequestId++};
LMS_LOG(API_SUBSONIC, DEBUG) << "Handling request " << requestId << " '" << request.path() << "', params = " << parameterMapToDebugString(request.getParameterMap());
const Wt::Http::ParameterMap& parameters {request.getParameterMap()};
// Optional parameters
ResponseFormat format {getParameterAs<std::string>(parameters, "f").get_value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml};
try
{
static std::mutex mutex;
ClientInfo clientInfo {getClientInfo(parameters)};
std::unique_lock<std::mutex> lock{mutex}; // For now just handle request s one by one
if (!checkPassword(_db, clientInfo))
throw Error {Error::Code::WrongUsernameOrPassword};
RequestContext requestContext {.parameters = parameters, .db = _db, .userName = clientInfo.user};
auto itHandler {requestHandlers.find(request.path())};
if (itHandler != requestHandlers.end())
{
Response resp {(itHandler->second)(requestContext)};
lock.unlock();
resp.write(response.out(), format);
response.setMimeType(ResponseFormatToMimeType(format));
LMS_LOG(API_SUBSONIC, DEBUG) << "Request " << requestId << " '" << request.path() << "' handled!";
return;
}
auto itStreamHandler {mediaRetrievalHandlers.find(request.path())};
if (itStreamHandler != mediaRetrievalHandlers.end())
{
MediaRetrievalResult res {itStreamHandler->second(requestContext, request.continuation())};
lock.unlock();
if (!res.mimeType.empty())
response.setMimeType(res.mimeType);
if (!res.data.empty())
{
response.out().write(reinterpret_cast<const char *>(&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 << " '" << request.path() << "' handled!";
return;
}
LMS_LOG(API_SUBSONIC, ERROR) << "Unhandled command '" << request.path() << "'";
}
catch (const Error& e)
{
LMS_LOG(API_SUBSONIC, ERROR) << "Error while processing request '" << request.path() << "'"
<< ", params = [" << parameterMapToDebugString(request.getParameterMap()) << "]"
<< ", code = " << static_cast<int>(e.getCode()) << ", msg = '" << e.getMessage() << "'";
Response resp {Response::createFailedResponse(e)};
resp.write(response.out(), format);
response.setMimeType(ResponseFormatToMimeType(format));
}
}
static
std::string
getArtistNames(const std::vector<Database::Artist::pointer>& artists)
{
if (artists.size() == 1)
return artists.front()->getName();
std::vector<std::string> 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
std::string
getTrackPath(const Database::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 Database::User::pointer& user, const Database::Track::pointer& track)
{
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<std::chrono::seconds>(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<std::chrono::seconds>(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
Database::ClusterType::pointer clusterType {Database::ClusterType::getByName(*track.session(), 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 Database::User::pointer& user, const Database::Release::pointer& release, 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<std::chrono::seconds>(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
Database::ClusterType::pointer clusterType {Database::ClusterType::getByName(*release.session(), 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 Database::User::pointer& user, const Database::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 Database::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;
}
// Handlers
Response
handlePingRequest(RequestContext& context)
{
return Response::createOkResponse();
}
Response
handleCreatePlaylistRequest(RequestContext& context)
{
// Optional params
auto id {getParameterAs<Id>(context.parameters, "playlistId")};
if (id && id->type != Id::Type::Playlist)
throw Error {Error::CustomType::BadId};
auto name {getParameterAs<std::string>(context.parameters, "name")};
std::vector<Id> trackIds {getMultiParametersAs<Id>(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::BadId};
if (!name && !id)
throw Error {Error::Code::RequiredParameterMissing};
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
Database::TrackList::pointer tracklist;
if (id)
{
tracklist = Database::TrackList::getById(context.db.getSession(), id->value);
if (!tracklist
|| tracklist->getUser() != user
|| tracklist->getType() != Database::TrackList::Type::Playlist)
{
throw Error {Error::Code::RequestedDataNotFound};
}
if (name)
tracklist.modify()->setName(*name);
}
else
{
tracklist = Database::TrackList::create(context.db.getSession(), *name, Database::TrackList::Type::Playlist, false, user);
}
for (const Id& trackId : trackIds)
{
Database::Track::pointer track {Database::Track::getById(context.db.getSession(), trackId.value)};
if (!track)
continue;
Database::TrackListEntry::create(context.db.getSession(), track, tracklist );
}
return Response::createOkResponse();
}
Response
handleDeletePlaylistRequest(RequestContext& context)
{
// Optional params
Id id {getMandatoryParameterAs<Id>(context.parameters, "id")};
if (id.type != Id::Type::Playlist)
throw Error {Error::CustomType::BadId};
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
Database::TrackList::pointer tracklist {Database::TrackList::getById(context.db.getSession(), id.value)};
if (!tracklist
|| tracklist->getUser() != user
|| tracklist->getType() != Database::TrackList::Type::Playlist)
{
throw Error {Error::Code::RequestedDataNotFound};
}
tracklist.remove();
return Response::createOkResponse();
}
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;
}
Response
handleGetRandomSongsRequest(RequestContext& context)
{
// Optional params
std::size_t size {getParameterAs<std::size_t>(context.parameters, "size").get_value_or(50)};
size = std::min(size, std::size_t {500});
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
auto tracks {Database::Track::getAllRandom(context.db.getSession(), size)};
Response response {Response::createOkResponse()};
Response::Node& randomSongsNode {response.createNode("randomSongs")};
for (const Database::Track::pointer& track : tracks)
randomSongsNode.addArrayChild("song", trackToResponseNode(user, track));
return response;
}
static
std::vector<Database::Release::pointer> getRandomAlbums(Wt::Dbo::Session& session, std::size_t offset, std::size_t size)
{
std::vector<Database::Release::pointer> res;
std::size_t nbReleases {Database::Release::getCount(session)};
if (offset > nbReleases)
return res;
if (offset + size > nbReleases)
size = nbReleases - offset;
std::vector<size_t> 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::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;
}
static
Response
handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
{
// Mandatory params
std::string type {getMandatoryParameterAs<std::string>(context.parameters, "type")};
// Optional params
std::size_t size {getParameterAs<std::size_t>(context.parameters, "size").get_value_or(10)};
std::size_t offset {getParameterAs<std::size_t>(context.parameters, "offset").get_value_or(0)};
std::vector<Database::Release::pointer> releases;
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
if (type == "random")
{
releases = getRandomAlbums(context.db.getSession(), offset, size);
}
else if (type == "newest")
{
auto after {Wt::WLocalDateTime::currentServerDateTime().toUTC().addMonths(-6)};
releases = Database::Release::getLastAdded(context.db.getSession(), after, offset, size);
}
else if (type == "alphabeticalByName")
{
releases = Database::Release::getAll(context.db.getSession(), offset, size);
}
else if (type == "byGenre")
{
// Mandatory param
std::string genre {getMandatoryParameterAs<std::string>(context.parameters, "genre")};
Database::ClusterType::pointer clusterType {Database::ClusterType::getByName(context.db.getSession(), genreClusterName)};
if (clusterType)
{
Database::Cluster::pointer cluster {clusterType->getCluster(genre)};
if (cluster)
{
bool more;
releases = Database::Release::getByFilter(context.db.getSession(), {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 Database::Release::pointer& release : releases)
albumListNode.addArrayChild("album", releaseToResponseNode(user, release, id3));
return response;
}
Response
handleGetAlbumListRequest(RequestContext& context)
{
return handleGetAlbumListRequestCommon(context, false /* no id3 */);
}
Response
handleGetAlbumList2Request(RequestContext& context)
{
return handleGetAlbumListRequestCommon(context, true /* id3 */);
}
Response
handleGetAlbumRequest(RequestContext& context)
{
// Mandatory params
Id id {getMandatoryParameterAs<Id>(context.parameters, "id")};
if (id.type != Id::Type::Release)
throw Error {Error::CustomType::BadId};
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::Release::pointer release {Database::Release::getById(context.db.getSession(), id.value)};
if (!release)
throw Error {Error::Code::RequestedDataNotFound};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
Response response {Response::createOkResponse()};
Response::Node releaseNode {releaseToResponseNode(user, release, true /* id3 */)};
auto tracks {release->getTracks()};
for (const Database::Track::pointer& track : tracks)
releaseNode.addArrayChild("song", trackToResponseNode(user, track));
response.addNode("album", std::move(releaseNode));
return response;
}
Response
handleGetArtistRequest(RequestContext& context)
{
// Mandatory params
Id id {getMandatoryParameterAs<Id>(context.parameters, "id")};
if (id.type != Id::Type::Artist)
throw Error {Error::CustomType::BadId};
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::Artist::pointer artist {Database::Artist::getById(context.db.getSession(), id.value)};
Database::User::pointer user {context.db.getUser(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 Database::Release::pointer& release : releases)
artistNode.addArrayChild("album", releaseToResponseNode(user, release, true /* id3 */));
response.addNode("artist", std::move(artistNode));
return response;
}
static
Response handleGetArtistInfoRequestCommon(RequestContext& context, bool id3)
{
// Mandatory params
Id id {getMandatoryParameterAs<Id>(context.parameters, "id")};
if (id.type != Id::Type::Artist)
throw Error {Error::CustomType::BadId};
// Optional params
std::size_t count {getParameterAs<std::size_t>(context.parameters, "count").get_value_or(10)};
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::Artist::pointer artist {Database::Artist::getById(context.db.getSession(), id.value)};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!artist || !user)
throw Error {Error::Code::RequestedDataNotFound};
Response response {Response::createOkResponse()};
Response::Node& artistInfoNode {response.createNode(id3 ? "artistInfo2" : "artistInfo")};
if (!artist->getMBID().empty())
artistInfoNode.createChild("musicBrainzId").setValue(artist->getMBID());
auto similarArtistsId {getService<Similarity::Searcher>()->getSimilarArtists(context.db.getSession(), artist.id(), count)};
for ( const auto& similarArtistId : similarArtistsId )
{
Database::Artist::pointer similarArtist {Database::Artist::getById(context.db.getSession(), similarArtistId)};
if (similarArtist)
artistInfoNode.addArrayChild("similarArtist", artistToResponseNode(user, similarArtist, id3));
}
return response;
}
Response handleGetArtistInfoRequest(RequestContext& context)
{
return handleGetArtistInfoRequestCommon(context, false /* no id3 */);
}
static Response handleGetArtistInfo2Request(RequestContext& context)
{
return handleGetArtistInfoRequestCommon(context, true /* id3 */);
}
Response
handleGetArtistsRequest(RequestContext& context)
{
Response response {Response::createOkResponse()};
Response::Node& artistsNode {response.createNode("artists")};
Response::Node& indexNode {artistsNode.createArrayChild("index")};
indexNode.setAttribute("name", "?");
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
auto artists {Database::Artist::getAll(context.db.getSession())};
for (const Database::Artist::pointer& artist : artists)
indexNode.addArrayChild("artist", artistToResponseNode(user, artist, true /* id3 */));
return response;
}
Response
handleGetMusicDirectoryRequest(RequestContext& context)
{
// Mandatory params
Id id {getMandatoryParameterAs<Id>(context.parameters, "id")};
Response response {Response::createOkResponse()};
Response::Node& directoryNode {response.createNode("directory")};
directoryNode.setAttribute("id", IdToString(id));
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
switch (id.type)
{
case Id::Type::Root:
{
directoryNode.setAttribute("name", "Music");
auto artists {Database::Artist::getAll(context.db.getSession())};
for (const Database::Artist::pointer& artist : artists)
directoryNode.addArrayChild("child", artistToResponseNode(user, artist, false /* no id3 */));
break;
}
case Id::Type::Artist:
{
auto artist {Database::Artist::getById(context.db.getSession(), id.value)};
if (!artist)
throw Error {Error::Code::RequestedDataNotFound};
directoryNode.setAttribute("name", makeNameFilesystemCompatible(artist->getName()));
auto releases {artist->getReleases()};
for (const Database::Release::pointer& release : releases)
directoryNode.addArrayChild("child", releaseToResponseNode(user, release, false /* no id3 */));
break;
}
case Id::Type::Release:
{
auto release {Database::Release::getById(context.db.getSession(), id.value)};
if (!release)
throw Error {Error::Code::RequestedDataNotFound};
directoryNode.setAttribute("name", makeNameFilesystemCompatible(release->getName()));
auto tracks {release->getTracks()};
for (const Database::Track::pointer& track : tracks)
directoryNode.addArrayChild("child", trackToResponseNode(user, track));
break;
}
default:
throw Error {Error::CustomType::BadId};
}
return response;
}
Response
handleGetMusicFoldersRequest(RequestContext& context)
{
Response response {Response::createOkResponse()};
Response::Node& musicFoldersNode {response.createNode("musicFolders")};
Response::Node& musicFolderNode {musicFoldersNode.createArrayChild("musicFolder")};
musicFolderNode.setAttribute("id", IdToString({Id::Type::Root}));
musicFolderNode.setAttribute("name", "Music");
return response;
}
Response
handleGetGenresRequest(RequestContext& context)
{
Response response {Response::createOkResponse()};
Response::Node& genresNode {response.createNode("genres")};
Wt::Dbo::Transaction transaction {context.db.getSession()};
auto clusterType {Database::ClusterType::getByName(context.db.getSession(), genreClusterName)};
if (clusterType)
{
auto clusters {clusterType->getClusters()};
for (const Database::Cluster::pointer& cluster : clusters)
genresNode.addArrayChild("genre", clusterToResponseNode(cluster));
}
return response;
}
Response
handleGetIndexesRequest(RequestContext& context)
{
Response response {Response::createOkResponse()};
Response::Node& artistsNode {response.createNode("indexes")};
Response::Node& indexNode {artistsNode.createArrayChild("index")};
indexNode.setAttribute("name", "?");
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
auto artists {Database::Artist::getAll(context.db.getSession())};
for (const Database::Artist::pointer& artist : artists)
indexNode.addArrayChild("artist", artistToResponseNode(user, artist, false /* no id3 */));
return response;
}
Response
handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
{
// Mandatory params
Id id {getMandatoryParameterAs<Id>(context.parameters, "id")};
if (id.type != Id::Type::Artist)
throw Error {Error::CustomType::BadId};
// Optional params
std::size_t count {getParameterAs<std::size_t>(context.parameters, "count").get_value_or(50)};
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::Artist::pointer artist {Database::Artist::getById(context.db.getSession(), id.value)};
Database::User::pointer user {context.db.getUser(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<Similarity::Searcher>()->getSimilarArtists(context.db.getSession(), artist.id(), 5)};
for ( const auto& similarArtistId : similarArtistsId )
{
Database::Artist::pointer similarArtist {Database::Artist::getById(context.db.getSession(), 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::mt19937::result_type>(std::chrono::duration_cast<std::chrono::milliseconds>(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 Database::Track::pointer& track : tracks)
similarSongsNode.addArrayChild("song", trackToResponseNode(user, track));
return response;
}
Response
handleGetSimilarSongsRequest(RequestContext& context)
{
return handleGetSimilarSongsRequestCommon(context, false /* no id3 */);
}
Response
handleGetSimilarSongs2Request(RequestContext& context)
{
return handleGetSimilarSongsRequestCommon(context, true /* id3 */);
}
static
Response
handleGetStarredRequestCommon(RequestContext& context, bool id3)
{
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(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 Database::Artist::pointer& artist : artists)
starredNode.addArrayChild("artist", artistToResponseNode(user, artist, id3));
}
{
auto releases {user->getStarredReleases()};
for (const Database::Release::pointer& release : releases)
starredNode.addArrayChild("album", releaseToResponseNode(user, release, id3));
}
{
auto tracks {user->getStarredTracks()};
for (const Database::Track::pointer& track : tracks)
starredNode.addArrayChild("song", trackToResponseNode(user, track));
}
return response;
}
Response
handleGetStarredRequest(RequestContext& context)
{
return handleGetStarredRequestCommon(context, false /* no id3 */);
}
Response
handleGetStarred2Request(RequestContext& context)
{
return handleGetStarredRequestCommon(context, true /* id3 */);
}
Response::Node
tracklistToResponseNode(const Database::TrackList::pointer& tracklist, Database::Handler& db)
{
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<std::chrono::seconds>(tracklist->getDuration()).count()));
playlistNode.setAttribute("public", tracklist->isPublic() ? "true" : "false");
playlistNode.setAttribute("created", "");
{
std::string userId {std::to_string(tracklist->getUser().id())};
Wt::Auth::User authUser { db.getUserDatabase().findWithId(userId)};
if (!authUser.isValid())
throw Error {Error::CustomType::InternalError};
playlistNode.setAttribute("owner", authUser.identity(Wt::Auth::Identity::LoginName).toUTF8());
}
return playlistNode;
}
Response
handleGetPlaylistRequest(RequestContext& context)
{
// Mandatory params
Id id {getMandatoryParameterAs<Id>(context.parameters, "id")};
if (id.type != Id::Type::Playlist)
throw Error {Error::CustomType::BadId};
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
Database::TrackList::pointer tracklist {Database::TrackList::getById(context.db.getSession(), id.value)};
if (!user || !tracklist)
throw Error {Error::Code::RequestedDataNotFound};
Response response {Response::createOkResponse()};
Response::Node playlistNode {tracklistToResponseNode(tracklist, context.db)};
auto entries {tracklist->getEntries()};
for (const Database::TrackListEntry::pointer& entry : entries)
playlistNode.addArrayChild("entry", trackToResponseNode(user, entry->getTrack()));
response.addNode("playlist", playlistNode );
return response;
}
Response
handleGetPlaylistsRequest(RequestContext& context)
{
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
Response response {Response::createOkResponse()};
Response::Node& playlistsNode {response.createNode("playlists")};
auto tracklists {Database::TrackList::getAll(context.db.getSession(), user, Database::TrackList::Type::Playlist)};
for (const Database::TrackList::pointer& tracklist : tracklists)
playlistsNode.addArrayChild("playlist", tracklistToResponseNode(tracklist, context.db));
return response;
}
Response
handleGetSongsByGenreRequest(RequestContext& context)
{
// Mandatory params
std::string genre {getMandatoryParameterAs<std::string>(context.parameters, "genre")};
// Optional params
std::size_t size {getParameterAs<std::size_t>(context.parameters, "count").get_value_or(10)};
size = std::min(size, std::size_t {500});
std::size_t offset {getParameterAs<std::size_t>(context.parameters, "offset").get_value_or(0)};
Wt::Dbo::Transaction transaction {context.db.getSession()};
auto clusterType {Database::ClusterType::getByName(context.db.getSession(), genreClusterName)};
if (!clusterType)
throw Error {Error::Code::RequestedDataNotFound};
auto cluster {clusterType->getCluster(genre)};
if (!cluster)
throw Error {Error::Code::RequestedDataNotFound};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
Response response {Response::createOkResponse()};
Response::Node& songsByGenreNode {response.createNode("songsByGenre")};
bool more;
auto tracks {Database::Track::getByFilter(context.db.getSession(), {cluster.id()}, {}, offset, size, more)};
for (const Database::Track::pointer& track : tracks)
songsByGenreNode.addArrayChild("song", trackToResponseNode(user, track));
return response;
}
static
Response
handleSearchRequestCommon(RequestContext& context, bool id3)
{
// Mandatory params
std::string query {getMandatoryParameterAs<std::string>(context.parameters, "query")};
std::vector<std::string> keywords {splitString(query, " ")};
// Optional params
std::size_t artistCount {getParameterAs<std::size_t>(context.parameters, "artistCount").get_value_or(20)};
std::size_t artistOffset {getParameterAs<std::size_t>(context.parameters, "artistOffset").get_value_or(0)};
std::size_t albumCount {getParameterAs<std::size_t>(context.parameters, "albumCount").get_value_or(20)};
std::size_t albumOffset {getParameterAs<std::size_t>(context.parameters, "albumOffset").get_value_or(0)};
std::size_t songCount {getParameterAs<std::size_t>(context.parameters, "songCount").get_value_or(20)};
std::size_t songOffset {getParameterAs<std::size_t>(context.parameters, "songOffset").get_value_or(0)};
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(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 {Database::Artist::getByFilter(context.db.getSession(), {}, keywords, artistOffset, artistCount, more)};
for (const Database::Artist::pointer& artist : artists)
searchResult2Node.addArrayChild("artist", artistToResponseNode(user, artist, id3));
}
{
auto releases {Database::Release::getByFilter(context.db.getSession(), {}, keywords, albumOffset, albumCount, more)};
for (const Database::Release::pointer& release : releases)
searchResult2Node.addArrayChild("album", releaseToResponseNode(user, release, id3));
}
{
auto tracks {Database::Track::getByFilter(context.db.getSession(), {}, keywords, songOffset, songCount, more)};
for (const Database::Track::pointer& track : tracks)
searchResult2Node.addArrayChild("song", trackToResponseNode(user, track));
}
return response;
}
struct StarParameters
{
std::vector<Id> artistIds;
std::vector<Id> releaseIds;
std::vector<Id> trackIds;
};
static
StarParameters
getStarParameters(const Wt::Http::ParameterMap& parameters)
{
StarParameters res;
std::vector<Id> ids {getMultiParametersAs<Id>(parameters, "id")};
res.artistIds = getMultiParametersAs<Id>(parameters, "artistId");
res.releaseIds = getMultiParametersAs<Id>(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::BadId};
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::BadId};
// 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::BadId};
}
}
return res;
}
Response
handleStarRequest(RequestContext& context)
{
StarParameters params {getStarParameters(context.parameters)};
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
for (const Id& id : params.artistIds)
{
Database::Artist::pointer artist {Database::Artist::getById(context.db.getSession(), id.value)};
if (!artist)
continue;
user.modify()->starArtist(artist);
}
for (const Id& id : params.releaseIds)
{
Database::Release::pointer release {Database::Release::getById(context.db.getSession(), id.value)};
if (!release)
continue;
user.modify()->starRelease(release);
}
for (const Id& id : params.trackIds)
{
Database::Track::pointer track {Database::Track::getById(context.db.getSession(), id.value)};
if (!track)
continue;
user.modify()->starTrack(track);
}
return Response::createOkResponse();
}
Response
handleSearch2Request(RequestContext& context)
{
return handleSearchRequestCommon(context, false /* no id3 */);
}
Response
handleSearch3Request(RequestContext& context)
{
return handleSearchRequestCommon(context, true /* id3 */);
}
Response
handleUnstarRequest(RequestContext& context)
{
StarParameters params {getStarParameters(context.parameters)};
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
for (const Id& id : params.artistIds)
{
Database::Artist::pointer artist {Database::Artist::getById(context.db.getSession(), id.value)};
if (!artist)
continue;
user.modify()->unstarArtist(artist);
}
for (const Id& id : params.releaseIds)
{
Database::Release::pointer release {Database::Release::getById(context.db.getSession(), id.value)};
if (!release)
continue;
user.modify()->unstarRelease(release);
}
for (const Id& id : params.trackIds)
{
Database::Track::pointer track {Database::Track::getById(context.db.getSession(), id.value)};
if (!track)
continue;
user.modify()->unstarTrack(track);
}
return Response::createOkResponse();
}
Response
handleUpdatePlaylistRequest(RequestContext& context)
{
// Mandatory params
Id id {getMandatoryParameterAs<Id>(context.parameters, "playlistId")};
if (id.type != Id::Type::Playlist)
throw Error {Error::CustomType::BadId};
// Optional parameters
auto name {getParameterAs<std::string>(context.parameters, "name")};
auto isPublic {getParameterAs<bool>(context.parameters, "public")};
std::vector<Id> trackIdsToAdd {getMultiParametersAs<Id>(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::BadId};
std::vector<std::size_t> trackPositionsToRemove {getMultiParametersAs<std::size_t>(context.parameters, "songIndexToRemove")};
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(context.userName)};
if (!user)
throw Error {Error::Code::RequestedDataNotFound};
Database::TrackList::pointer tracklist {Database::TrackList::getById(context.db.getSession(), id.value)};
if (!tracklist
|| tracklist->getUser() != user
|| tracklist->getType() != Database::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<std::size_t>());
for (std::size_t trackPositionToRemove : trackPositionsToRemove)
{
auto entry {tracklist->getEntry(trackPositionToRemove)};
if (entry)
entry.remove();
}
}
// Add tracks
for (const Id& trackIdToAdd : trackIdsToAdd)
{
Database::Track::pointer track {Database::Track::getById(context.db.getSession(), trackIdToAdd.value)};
if (!track)
continue;
Database::TrackListEntry::create(context.db.getSession(), track, tracklist );
}
return Response::createOkResponse();
}
static
std::shared_ptr<Av::Transcoder>
createTranscoder(RequestContext& context)
{
// Mandatory params
Id id {getMandatoryParameterAs<Id>(context.parameters, "id")};
// Optional params
boost::optional<std::size_t> maxBitRate {getParameterAs<std::size_t>(context.parameters, "maxBitRate")};
boost::filesystem::path trackPath;
{
Wt::Dbo::Transaction transaction {context.db.getSession()};
Database::User::pointer user {context.db.getUser(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 {Database::Track::getById(context.db.getSession(), 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<Av::Transcoder>(trackPath, parameters);
}
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<Av::Transcoder> 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<std::shared_ptr<Av::Transcoder>>(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;
}
MediaRetrievalResult
handleGetCoverArt(RequestContext& context, Wt::Http::ResponseContinuation*)
{
// Mandatory params
Id id {getMandatoryParameterAs<Id>(context.parameters, "id")};
std::size_t size {getParameterAs<std::size_t>(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<CoverArt::Grabber>()->getFromTrack(context.db.getSession(), id.value, Image::Format::JPEG, size);
break;
case Id::Type::Release:
res.data = getService<CoverArt::Grabber>()->getFromRelease(context.db.getSession(), id.value, Image::Format::JPEG, size);
break;
default:
throw Error {Error::CustomType::BadId};
}
res.mimeType = Image::format_to_mimeType(Image::Format::JPEG);
return res;
}
} // namespace api::subsonic