API Subsonic: first working version, still WIP (missing commands, no authentication)
This commit is contained in:
+5
-5
@@ -7,21 +7,21 @@ lms_docrootdir=$(pkgdatadir)/docroot
|
||||
lms_approotdir=$(pkgdatadir)/approot
|
||||
|
||||
lms_cssdir=$(lms_docrootdir)/css
|
||||
lms_imagesdir=$(lms_docrootdir)/images
|
||||
lms_jsdir=$(lms_docrootdir)/js
|
||||
lms_imagesdir=$(lms_approotdir)/images
|
||||
|
||||
lms_css_DATA = \
|
||||
docroot/css/lms.css
|
||||
|
||||
lms_images_DATA = \
|
||||
docroot/images/unknown-cover.jpg \
|
||||
docroot/images/unknown-artist.jpg
|
||||
|
||||
lms_js_DATA = \
|
||||
docroot/js/bootstrap-notify.js \
|
||||
docroot/js/jquery-1.10.2.min.js \
|
||||
docroot/js/mediaplayer.js
|
||||
|
||||
lms_images_DATA = \
|
||||
approot/images/unknown-cover.jpg \
|
||||
approot/images/unknown-artist.jpg
|
||||
|
||||
lms_approot_DATA = \
|
||||
approot/admin-database.xml \
|
||||
approot/admin-user.xml \
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@@ -1,6 +1,9 @@
|
||||
bin_PROGRAMS = lms
|
||||
|
||||
lms_SOURCES = \
|
||||
$(srcdir)/api/subsonic/SubsonicId.cpp \
|
||||
$(srcdir)/api/subsonic/SubsonicResource.cpp \
|
||||
$(srcdir)/api/subsonic/SubsonicResponse.cpp \
|
||||
$(srcdir)/av/AvInfo.cpp \
|
||||
$(srcdir)/av/AvTranscoder.cpp \
|
||||
$(srcdir)/cover/CoverArtGrabber.cpp \
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "SubsonicId.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
|
||||
boost::optional<Id>
|
||||
IdFromString(const std::string& id)
|
||||
{
|
||||
std::vector<std::string> values {splitString(id, "-")};
|
||||
if (values.size() != 2)
|
||||
{
|
||||
LMS_LOG(API_SUBSONIC, ERROR) << "Bad id format";
|
||||
return {};
|
||||
}
|
||||
|
||||
Id res;
|
||||
|
||||
std::string type {std::move(values[0])};
|
||||
if (type == "artist")
|
||||
res.type = Id::Type::Artist;
|
||||
else if (type == "album")
|
||||
res.type = Id::Type::Release;
|
||||
else if (type == "track")
|
||||
res.type = Id::Type::Track;
|
||||
else
|
||||
{
|
||||
LMS_LOG(API_SUBSONIC, ERROR) << "Bad id format";
|
||||
return {};
|
||||
}
|
||||
|
||||
auto optId {readAs<Database::IdType>(values[1])};
|
||||
if (!optId)
|
||||
{
|
||||
LMS_LOG(API_SUBSONIC, ERROR) << "Bad id format";
|
||||
return {};
|
||||
}
|
||||
|
||||
res.id = *optId;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string
|
||||
IdToString(const Id& id)
|
||||
{
|
||||
std::string res;
|
||||
|
||||
switch (id.type)
|
||||
{
|
||||
case Id::Type::Artist:
|
||||
res = "artist-";
|
||||
break;
|
||||
case Id::Type::Release:
|
||||
res = "album-";
|
||||
break;
|
||||
case Id::Type::Track:
|
||||
res = "track-";
|
||||
break;
|
||||
}
|
||||
|
||||
return res + std::to_string(id.id);
|
||||
}
|
||||
|
||||
} // namespace API::Subsonic
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <boost/optional.hpp>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
|
||||
struct Id
|
||||
{
|
||||
enum class Type
|
||||
{
|
||||
Track,
|
||||
Release,
|
||||
Artist,
|
||||
};
|
||||
|
||||
Type type;
|
||||
Database::IdType id;
|
||||
};
|
||||
|
||||
boost::optional<Id> IdFromString(const std::string& id);
|
||||
std::string IdToString(const Id& id);
|
||||
|
||||
} // namespace API::Subsonic
|
||||
@@ -0,0 +1,657 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "SubsonicResource.hpp"
|
||||
|
||||
#include <mutex>
|
||||
#include <numeric>
|
||||
#include <random>
|
||||
|
||||
#include <Wt/WLocalDateTime.h>
|
||||
|
||||
#include "av/AvTranscoder.hpp"
|
||||
#include "cover/CoverArtGrabber.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "main/Services.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
#include "SubsonicId.hpp"
|
||||
#include "SubsonicResponse.hpp"
|
||||
|
||||
// Requests
|
||||
#define PING_URL "/rest/ping.view"
|
||||
#define GET_LICENSE_URL "/rest/getLicense.view"
|
||||
#define GET_RANDOM_SONGS_URL "/rest/getRandomSongs.view"
|
||||
#define GET_ALBUM_LIST_URL "/rest/getAlbumList.view"
|
||||
#define GET_MUSIC_DIRECTORY_URL "/rest/getMusicDirectory.view"
|
||||
#define GET_STARRED_URL "/rest/getStarred.view"
|
||||
#define GET_MUSIC_FOLDERS_URL "/rest/getMusicFolders.view"
|
||||
#define GET_INDEXES_URL "/rest/getIndexes.view"
|
||||
#define GET_ARTISTS_URL "/rest/getArtists.view"
|
||||
|
||||
// MediaRetrievals
|
||||
#define STREAM_URL "/rest/stream.view"
|
||||
#define GET_COVER_ART_URL "/rest/getCoverArt.view"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
|
||||
// requests
|
||||
using RequestHandlerFunc = std::function<Response(const Wt::Http::ParameterMap&, Database::Handler&)>;
|
||||
static Response handlePingRequest(const Wt::Http::ParameterMap& request, Database::Handler& db);
|
||||
static Response handleGetLicenseRequest(const Wt::Http::ParameterMap& request, Database::Handler& db);
|
||||
static Response handleGetRandomSongsRequest(const Wt::Http::ParameterMap& request, Database::Handler& db);
|
||||
static Response handleGetAlbumListRequest(const Wt::Http::ParameterMap& request, Database::Handler& db);
|
||||
static Response handleGetMusicDirectoryRequest(const Wt::Http::ParameterMap& request, Database::Handler& db);
|
||||
static Response handleGetMusicFoldersRequest(const Wt::Http::ParameterMap& request, Database::Handler& db);
|
||||
static Response handleGetIndexesRequest(const Wt::Http::ParameterMap& request, Database::Handler& db);
|
||||
static Response handleGetArtistsRequest(const Wt::Http::ParameterMap& request, Database::Handler& db);
|
||||
|
||||
// MediaRetrievals
|
||||
using MediaRetrivalHandlerFunc = std::function<void(const Wt::Http::Request&, Database::Handler&, Wt::Http::Response& response)>;
|
||||
void handleStream(const Wt::Http::Request& request, Database::Handler& db, Wt::Http::Response& response);
|
||||
void handleGetCoverArt(const Wt::Http::Request& request, Database::Handler& db, Wt::Http::Response& response);
|
||||
|
||||
static std::map<std::string, RequestHandlerFunc> requestHandlers
|
||||
{
|
||||
{PING_URL, handlePingRequest},
|
||||
{GET_LICENSE_URL, handleGetLicenseRequest},
|
||||
{GET_RANDOM_SONGS_URL, handleGetRandomSongsRequest},
|
||||
{GET_ALBUM_LIST_URL, handleGetAlbumListRequest},
|
||||
{GET_MUSIC_DIRECTORY_URL, handleGetMusicDirectoryRequest},
|
||||
{GET_STARRED_URL, handlePingRequest}, // TODO
|
||||
{GET_MUSIC_FOLDERS_URL, handleGetMusicFoldersRequest},
|
||||
{GET_INDEXES_URL, handleGetIndexesRequest},
|
||||
{GET_ARTISTS_URL, handleGetArtistsRequest},
|
||||
};
|
||||
|
||||
static std::map<std::string, MediaRetrivalHandlerFunc> mediaRetrievalHandlers
|
||||
{
|
||||
{STREAM_URL, handleStream},
|
||||
{GET_COVER_ART_URL, handleGetCoverArt},
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
boost::optional<T>
|
||||
getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
{
|
||||
boost::optional<T> res;
|
||||
|
||||
auto it = parameterMap.find(param);
|
||||
if (it == parameterMap.end())
|
||||
return res;
|
||||
|
||||
if (it->second.size() != 1)
|
||||
return res;
|
||||
|
||||
return readAs<T>(it->second.front());
|
||||
}
|
||||
|
||||
|
||||
struct ClientInfo
|
||||
{
|
||||
std::string name;
|
||||
ResponseFormat format;
|
||||
std::string user;
|
||||
std::string password;
|
||||
};
|
||||
|
||||
boost::optional<ClientInfo>
|
||||
getClientInfo(const Wt::Http::ParameterMap& parameters)
|
||||
{
|
||||
boost::optional<ClientInfo> res {ClientInfo {}};
|
||||
|
||||
// Mandatory parameters
|
||||
auto param {getParameterAs<std::string>(parameters, "c")};
|
||||
if (!param)
|
||||
return {};
|
||||
res->name = *param;
|
||||
|
||||
param = getParameterAs<std::string>(parameters, "u");
|
||||
if (!param)
|
||||
return {};
|
||||
res->user = *param;
|
||||
|
||||
param = getParameterAs<std::string>(parameters, "p");
|
||||
if (!param)
|
||||
return {};
|
||||
res->password = *param;
|
||||
|
||||
// Optional parameters
|
||||
param = getParameterAs<std::string>(parameters, "f");
|
||||
res->format = (param ? ResponseFormat::json : ResponseFormat::xml); // TODO
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void
|
||||
SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response)
|
||||
{
|
||||
LMS_LOG(API_SUBSONIC, DEBUG) << "REQUEST. Path = '" << request.path() << "', pathInfo = '" << request.pathInfo() << "', queryString = '" << request.queryString() << "'";
|
||||
|
||||
const Wt::Http::ParameterMap parameters {request.getParameterMap()};
|
||||
|
||||
for (const auto it : parameters)
|
||||
{
|
||||
LMS_LOG(API_SUBSONIC, DEBUG) << "Found param '" << it.first << "'";
|
||||
for (const std::string& value : it.second)
|
||||
LMS_LOG(API_SUBSONIC, DEBUG) << "\t'" << value << "'";
|
||||
}
|
||||
|
||||
std::string s{std::istreambuf_iterator<char>(request.in()), {}};
|
||||
|
||||
LMS_LOG(API_SUBSONIC, DEBUG) << "BODY = '" << s << "'";
|
||||
|
||||
auto clientInfo {getClientInfo(parameters)};
|
||||
if (!clientInfo)
|
||||
{
|
||||
LMS_LOG(API_SUBSONIC, ERROR) << "Failed to parse client info";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
static std::mutex mutex;
|
||||
|
||||
std::unique_lock<std::mutex> lock{mutex}; // For now just handle request s one by one
|
||||
|
||||
auto itHandler {requestHandlers.find(request.path())};
|
||||
if (itHandler != requestHandlers.end())
|
||||
{
|
||||
Response resp {(itHandler->second)(request.getParameterMap(), _db)};
|
||||
responseToStream(resp, clientInfo->format, response.out());
|
||||
response.setMimeType(ResponseFormatToMimeType(clientInfo->format));
|
||||
return;
|
||||
}
|
||||
|
||||
auto itStreamHandler {mediaRetrievalHandlers.find(request.path())};
|
||||
if (itStreamHandler != mediaRetrievalHandlers.end())
|
||||
{
|
||||
itStreamHandler->second(request, _db, response);
|
||||
return;
|
||||
}
|
||||
|
||||
LMS_LOG(API_SUBSONIC, ERROR) << "Unhandled command '" << request.path() << "'";
|
||||
|
||||
}
|
||||
catch (const Error& e)
|
||||
{
|
||||
Response resp {Response::createFailedResponse(e)};
|
||||
responseToStream(resp, clientInfo->format, response.out());
|
||||
response.setMimeType(ResponseFormatToMimeType(clientInfo->format));
|
||||
}
|
||||
}
|
||||
|
||||
static
|
||||
std::string
|
||||
getArtistNames(const std::vector<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
|
||||
Response::Node
|
||||
trackToResponseNode(const Database::Track::pointer& track)
|
||||
{
|
||||
Response::Node trackResponse;
|
||||
|
||||
trackResponse.setAttribute("title", track->getName());
|
||||
trackResponse.setAttribute("id", IdToString({Id::Type::Track, track.id()}));
|
||||
trackResponse.setAttribute("coverArt", IdToString({Id::Type::Track, track.id()}));
|
||||
trackResponse.setAttribute("isDir", "false");
|
||||
|
||||
auto artists {track->getArtists()};
|
||||
if (!artists.empty())
|
||||
trackResponse.setAttribute("artist", getArtistNames(artists));
|
||||
|
||||
trackResponse.setAttribute("path", track->getName() + ".mp3");
|
||||
trackResponse.setAttribute("bitrate", "128");
|
||||
trackResponse.setAttribute("duration", std::to_string(std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count()));
|
||||
trackResponse.setAttribute("suffix", "mp3");
|
||||
trackResponse.setAttribute("contentType", "audio/mpeg");
|
||||
trackResponse.setAttribute("size", std::to_string(128000/8 * std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count()));
|
||||
|
||||
if (track->getYear())
|
||||
trackResponse.setAttribute("year", std::to_string(*track->getYear()));
|
||||
|
||||
if (track->getTrackNumber())
|
||||
trackResponse.setAttribute("track", std::to_string(*track->getTrackNumber()));
|
||||
|
||||
if (track->getRelease())
|
||||
{
|
||||
trackResponse.setAttribute("parent", IdToString({Id::Type::Release, track->getRelease().id()}));
|
||||
trackResponse.setAttribute("album", track->getRelease()->getName());
|
||||
}
|
||||
|
||||
return trackResponse;
|
||||
}
|
||||
|
||||
static
|
||||
Response::Node
|
||||
releaseToResponseNode(const Database::Release::pointer& release)
|
||||
{
|
||||
Response::Node albumNode;
|
||||
|
||||
albumNode.setAttribute("title", release->getName());
|
||||
albumNode.setAttribute("id", IdToString({Id::Type::Release, release.id()}));
|
||||
albumNode.setAttribute("isDir", "true");
|
||||
albumNode.setAttribute("coverArt", IdToString({Id::Type::Release, release.id()}));
|
||||
|
||||
auto artists {release->getArtists()};
|
||||
if (!artists.empty())
|
||||
{
|
||||
if (artists.size() > 1)
|
||||
albumNode.setAttribute("artist", "Various Artists");
|
||||
else
|
||||
albumNode.setAttribute("artist", artists.front()->getName());
|
||||
}
|
||||
|
||||
return albumNode;
|
||||
}
|
||||
|
||||
static
|
||||
Response::Node
|
||||
artistToResponseNode(const Database::Artist::pointer& artist)
|
||||
{
|
||||
Response::Node artistResponse;
|
||||
|
||||
artistResponse.setAttribute("id", IdToString({Id::Type::Artist, artist.id()}));
|
||||
artistResponse.setAttribute("name", artist->getName());
|
||||
artistResponse.setAttribute("albumCount", std::to_string(artist->getReleases().size()));
|
||||
|
||||
return artistResponse;
|
||||
}
|
||||
|
||||
|
||||
// Handlers
|
||||
Response
|
||||
handlePingRequest(const Wt::Http::ParameterMap ¶meters, Database::Handler& handler)
|
||||
{
|
||||
return Response::createOkResponse();
|
||||
}
|
||||
|
||||
Response
|
||||
handleGetLicenseRequest(const Wt::Http::ParameterMap ¶meters, Database::Handler& handler)
|
||||
{
|
||||
Response response {Response::createOkResponse()};
|
||||
|
||||
Response::Node& licenseNode {response.createNode("license")};
|
||||
licenseNode.setAttribute("licenseExpires", "2019-09-03T14:46:43");
|
||||
licenseNode.setAttribute("email", "foo@bar.com");
|
||||
licenseNode.setAttribute("valid", "true");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response
|
||||
handleGetRandomSongsRequest(const Wt::Http::ParameterMap& parameters, Database::Handler& db)
|
||||
{
|
||||
// Optional params
|
||||
auto size {getParameterAs<std::size_t>(parameters, "size")};
|
||||
if (!size)
|
||||
size = 50;
|
||||
|
||||
Wt::Dbo::Transaction transaction {db.getSession()};
|
||||
|
||||
auto tracks {Database::Track::getAllRandom(db.getSession(), *size)};
|
||||
|
||||
LMS_LOG(API_SUBSONIC, DEBUG) << "Got " << tracks.size() << " tracks";
|
||||
|
||||
Response response {Response::createOkResponse()};
|
||||
|
||||
Response::Node& randomSongsNode {response.createNode("randomSongs")};
|
||||
for (const Database::Track::pointer& track : tracks)
|
||||
randomSongsNode.addArrayChild("song", trackToResponseNode(track));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static
|
||||
std::vector<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::random_device r;
|
||||
std::seed_seq seed {1337};
|
||||
std::mt19937 generator{seed};
|
||||
|
||||
std::shuffle(std::begin(indexes), std::end(indexes), generator);
|
||||
std::for_each(std::next(std::begin(indexes), offset), std::next(std::begin(indexes), offset + size),
|
||||
[&](std::size_t offset)
|
||||
{
|
||||
auto release {Database::Release::getAll(session, offset, 1)};
|
||||
if (!release.empty())
|
||||
res.emplace_back(release.front());
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Response
|
||||
handleGetAlbumListRequest(const Wt::Http::ParameterMap& request, Database::Handler& db)
|
||||
{
|
||||
// Mandatory params
|
||||
auto type {getParameterAs<std::string>(request, "type")};
|
||||
if (!type)
|
||||
throw Error {Error::Code::RequiredParameterMissing};
|
||||
|
||||
// Optional params
|
||||
auto size {getParameterAs<std::size_t>(request, "size")};
|
||||
if (!size)
|
||||
size = 10;
|
||||
|
||||
auto offset {getParameterAs<std::size_t>(request, "offset")};
|
||||
if (!offset)
|
||||
offset = 0;
|
||||
|
||||
std::vector<Database::Release::pointer> releases;
|
||||
|
||||
Wt::Dbo::Transaction transaction {db.getSession()};
|
||||
|
||||
if (*type == "random")
|
||||
{
|
||||
releases = getRandomAlbums(db.getSession(), *offset, *size);
|
||||
}
|
||||
else if (*type == "newest")
|
||||
{
|
||||
auto after {Wt::WLocalDateTime::currentServerDateTime().toUTC().addMonths(-1)};
|
||||
releases = Database::Release::getLastAdded(db.getSession(), after, offset, size);
|
||||
}
|
||||
else
|
||||
throw Error {"Unsupported request"};
|
||||
|
||||
LMS_LOG(API_SUBSONIC, DEBUG) << "Got " << releases.size() << " albums";
|
||||
|
||||
Response response {Response::createOkResponse()};
|
||||
Response::Node& albumListNode {response.createNode("albumList")};
|
||||
|
||||
for (const Database::Release::pointer& release : releases)
|
||||
albumListNode.addArrayChild("album", releaseToResponseNode(release));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response
|
||||
handleGetMusicDirectoryRequest(const Wt::Http::ParameterMap& request, Database::Handler& db)
|
||||
{
|
||||
// Mandatory params
|
||||
auto idParam {getParameterAs<std::string>(request, "id")};
|
||||
if (!idParam)
|
||||
throw Error {Error::Code::RequiredParameterMissing};
|
||||
|
||||
auto id {IdFromString(*idParam)};
|
||||
if (!id)
|
||||
throw Error {"Bad id"};
|
||||
|
||||
Wt::Dbo::Transaction transaction {db.getSession()};
|
||||
|
||||
Response response {Response::createOkResponse()};
|
||||
Response::Node& directoryNode {response.createNode("directory")};
|
||||
|
||||
switch (id->type)
|
||||
{
|
||||
case Id::Type::Artist:
|
||||
{
|
||||
auto artist {Database::Artist::getById(db.getSession(), id->id)};
|
||||
if (!artist)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
auto releases {artist->getReleases()};
|
||||
for (const Database::Release::pointer& release : releases)
|
||||
directoryNode.addArrayChild("child", releaseToResponseNode(release));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case Id::Type::Release:
|
||||
{
|
||||
auto release {Database::Release::getById(db.getSession(), id->id)};
|
||||
if (!release)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
auto tracks {release->getTracks()};
|
||||
for (const Database::Track::pointer& track : tracks)
|
||||
directoryNode.addArrayChild("child", trackToResponseNode(track));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw Error {"Bad id"};
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response
|
||||
handleGetMusicFoldersRequest(const Wt::Http::ParameterMap& request, Database::Handler& db)
|
||||
{
|
||||
Response response {Response::createOkResponse()};
|
||||
Response::Node& musicFoldersNode {response.createNode("musicFolders")};
|
||||
|
||||
Response::Node& musicFolderNode {musicFoldersNode.createArrayChild("musicFolder")};
|
||||
musicFolderNode.setAttribute("id", "1");
|
||||
musicFolderNode.setAttribute("name", "Music");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response
|
||||
handleGetIndexesRequest(const Wt::Http::ParameterMap& request, Database::Handler& db)
|
||||
{
|
||||
Wt::Dbo::Transaction transaction {db.getSession()};
|
||||
|
||||
Response response {Response::createOkResponse()};
|
||||
Response::Node& artistsNode {response.createNode("indexes")};
|
||||
|
||||
Response::Node& indexNode {artistsNode.createArrayChild("index")};
|
||||
|
||||
auto artists {Database::Artist::getAll(db.getSession())};
|
||||
for (const Database::Artist::pointer& artist : artists)
|
||||
indexNode.addArrayChild("artist", artistToResponseNode(artist));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
Response
|
||||
handleGetArtistsRequest(const Wt::Http::ParameterMap& request, Database::Handler& db)
|
||||
{
|
||||
Wt::Dbo::Transaction transaction {db.getSession()};
|
||||
|
||||
// TODO factorize with handleGetIndexesRequest
|
||||
Response response {Response::createOkResponse()};
|
||||
Response::Node& artistsNode {response.createNode("artists")};
|
||||
|
||||
auto artists {Database::Artist::getAll(db.getSession())};
|
||||
for (const Database::Artist::pointer& artist : artists)
|
||||
artistsNode.addArrayChild("artist", artistToResponseNode(artist));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static
|
||||
std::shared_ptr<Av::Transcoder>
|
||||
createTranscoder(const Wt::Http::ParameterMap& request, Database::Handler& db)
|
||||
{
|
||||
// Mandatory params
|
||||
auto idParam {getParameterAs<std::string>(request, "id")};
|
||||
if (!idParam)
|
||||
throw Error {Error::Code::RequiredParameterMissing};
|
||||
|
||||
auto id {IdFromString(*idParam)};
|
||||
if (!id || id->type != Id::Type::Track)
|
||||
throw Error {"bad id format"};
|
||||
|
||||
boost::filesystem::path trackPath;
|
||||
{
|
||||
Wt::Dbo::Transaction transaction {db.getSession()};
|
||||
|
||||
auto track {Database::Track::getById(db.getSession(), id->id)};
|
||||
if (!track)
|
||||
{
|
||||
LMS_LOG(API_SUBSONIC, ERROR) << "Bad track id";
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
}
|
||||
|
||||
trackPath = track->getPath();
|
||||
}
|
||||
|
||||
Av::TranscodeParameters parameters {};
|
||||
|
||||
parameters.bitrate = 128000;
|
||||
parameters.encoding = Av::Encoding::MP3;
|
||||
|
||||
return std::make_shared<Av::Transcoder>(trackPath, parameters);
|
||||
}
|
||||
|
||||
void
|
||||
handleStream(const Wt::Http::Request& request, Database::Handler& db, Wt::Http::Response& response)
|
||||
{
|
||||
LMS_LOG(API_SUBSONIC, DEBUG) << "STREAM";
|
||||
|
||||
std::shared_ptr<Av::Transcoder> transcoder;
|
||||
|
||||
Wt::Http::ResponseContinuation* continuation {request.continuation()};
|
||||
if (!continuation)
|
||||
{
|
||||
transcoder = createTranscoder(request.getParameterMap(), db);
|
||||
response.setMimeType(Av::encodingToMimetype(Av::Encoding::MP3));
|
||||
transcoder->start();
|
||||
}
|
||||
else
|
||||
{
|
||||
LMS_LOG(UI, DEBUG) << "Continuation! ";
|
||||
transcoder = Wt::cpp17::any_cast<std::shared_ptr<Av::Transcoder>>(continuation->data());
|
||||
}
|
||||
|
||||
if (!transcoder)
|
||||
throw Error {"transcoding failed"};
|
||||
|
||||
if (!transcoder->isComplete())
|
||||
{
|
||||
static constexpr std::size_t chunkSize {65536*4};
|
||||
|
||||
std::vector<unsigned char> data;
|
||||
data.reserve(chunkSize);
|
||||
|
||||
transcoder->process(data, chunkSize);
|
||||
|
||||
LMS_LOG(API_SUBSONIC, DEBUG) << "Writing " << data.size() << " bytes...";
|
||||
response.out().write(reinterpret_cast<char*>(&data[0]), data.size());
|
||||
|
||||
if (!response.out())
|
||||
{
|
||||
LMS_LOG(UI, ERROR) << "Write failed!";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!transcoder->isComplete())
|
||||
{
|
||||
continuation = response.createContinuation();
|
||||
continuation->setData(transcoder);
|
||||
}
|
||||
else
|
||||
LMS_LOG(API_SUBSONIC, DEBUG) << "No more data!";
|
||||
}
|
||||
|
||||
void
|
||||
handleGetCoverArt(const Wt::Http::Request& request, Database::Handler& db, Wt::Http::Response& response)
|
||||
{
|
||||
LMS_LOG(API_SUBSONIC, DEBUG) << "STREAM";
|
||||
|
||||
// Mandatory params
|
||||
auto idParam {getParameterAs<std::string>(request.getParameterMap(), "id")};
|
||||
if (!idParam)
|
||||
throw Error {Error::Code::RequiredParameterMissing};
|
||||
|
||||
auto id {IdFromString(*idParam)};
|
||||
if (!id)
|
||||
throw Error {"bad id format"};
|
||||
|
||||
auto size {getParameterAs<std::size_t>(request.getParameterMap(), "size")};
|
||||
if (!size || *size == 0)
|
||||
size = 256;
|
||||
|
||||
std::vector<uint8_t> cover;
|
||||
switch (id->type)
|
||||
{
|
||||
case Id::Type::Track:
|
||||
cover = getServices().coverArtGrabber->getFromTrack(db.getSession(), id->id, Image::Format::JPEG, *size);
|
||||
break;
|
||||
case Id::Type::Release:
|
||||
cover = getServices().coverArtGrabber->getFromRelease(db.getSession(), id->id, Image::Format::JPEG, *size);
|
||||
break;
|
||||
default:
|
||||
throw Error {"bad id format"};
|
||||
}
|
||||
|
||||
response.setMimeType( Image::format_to_mimeType(Image::Format::JPEG) );
|
||||
response.out().write(reinterpret_cast<const char *>(&cover[0]), cover.size());
|
||||
}
|
||||
|
||||
} // namespace api::subsonic
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <boost/optional.hpp>
|
||||
|
||||
#include <Wt/WResource.h>
|
||||
#include <Wt/Http/Response.h>
|
||||
|
||||
#include "database/DatabaseHandler.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
|
||||
class SubsonicResource final : public Wt::WResource
|
||||
{
|
||||
public:
|
||||
SubsonicResource(Wt::Dbo::SqlConnectionPool& connectionPool);
|
||||
|
||||
static std::vector<std::string> getPaths();
|
||||
private:
|
||||
|
||||
|
||||
void handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) override;
|
||||
|
||||
Database::Handler _db;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "SubsonicResponse.hpp"
|
||||
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include <boost/property_tree/xml_parser.hpp>
|
||||
|
||||
#define API_VERSION "1.12.0"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
|
||||
std::string
|
||||
ResponseFormatToMimeType(ResponseFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case ResponseFormat::xml: return "text/xml";
|
||||
case ResponseFormat::json: return "application/json";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
static
|
||||
const char*
|
||||
ErrorCodeToString(Error::Code error)
|
||||
{
|
||||
switch (error)
|
||||
{
|
||||
case Error::Code::RequiredParameterMissing:
|
||||
return "Required parameter is missing.";
|
||||
case Error::Code::ClientMustUpgrade:
|
||||
return "Incompatible Subsonic REST protocol version. Client must upgrade.";
|
||||
case Error::Code::ServerMustUpgrade:
|
||||
return "Incompatible Subsonic REST protocol version. Server must upgrade.";
|
||||
case Error::Code::WrongUsernameOrPassword:
|
||||
return "Wrong username or password.";
|
||||
case Error::Code::UserNotAuthorized:
|
||||
return "User is not authorized for the given operation.";
|
||||
case Error::Code::RequestedDataNotFound:
|
||||
return "The requested data was not found.";
|
||||
default:
|
||||
return "Unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
Error::Error(Code code)
|
||||
: _code {code},
|
||||
_message {ErrorCodeToString(code)}
|
||||
{
|
||||
}
|
||||
|
||||
Error::Error(const std::string& message)
|
||||
: _code {Code::Generic},
|
||||
_message {message}
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
Response::Node::setAttribute(const std::string& key, const std::string& value)
|
||||
{
|
||||
attributes[key] = value;
|
||||
}
|
||||
|
||||
void
|
||||
Response::Node::addChild(const std::string& key, Node node)
|
||||
{
|
||||
children[key].emplace_back(std::move(node));
|
||||
}
|
||||
|
||||
void
|
||||
Response::Node::addArrayChild(const std::string& key, Node node)
|
||||
{
|
||||
childrenArrays[key].emplace_back(std::move(node));
|
||||
}
|
||||
|
||||
|
||||
Response::Node&
|
||||
Response::Node::createChild(const std::string& key)
|
||||
{
|
||||
children[key].emplace_back();
|
||||
return children[key].back();
|
||||
}
|
||||
|
||||
Response::Node&
|
||||
Response::Node::createArrayChild(const std::string& key)
|
||||
{
|
||||
childrenArrays[key].emplace_back();
|
||||
return childrenArrays[key].back();
|
||||
}
|
||||
|
||||
Response
|
||||
Response::createOkResponse()
|
||||
{
|
||||
Response response;
|
||||
Node& responseNode {response._root.createChild("subsonic-response")};
|
||||
|
||||
responseNode.setAttribute("status", "ok");
|
||||
responseNode.setAttribute("version", API_VERSION);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response
|
||||
Response::createFailedResponse(const Error& error)
|
||||
{
|
||||
Response response;
|
||||
Node& responseNode {response._root.createChild("subsonic-response")};
|
||||
|
||||
responseNode.setAttribute("status", "failed");
|
||||
responseNode.setAttribute("version", API_VERSION);
|
||||
|
||||
Node& errorNode {responseNode.createChild("error")};
|
||||
errorNode.setAttribute("code", std::to_string(static_cast<int>(error.getCode())));
|
||||
errorNode.setAttribute("message", error.getMessage());
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response::Node&
|
||||
Response::createNode(const std::string& key)
|
||||
{
|
||||
return _root.children["subsonic-response"].front().createChild(key);
|
||||
}
|
||||
|
||||
boost::property_tree::ptree
|
||||
NodeToPropertyTree(const Response::Node& node, ResponseFormat format)
|
||||
{
|
||||
boost::property_tree::ptree res;
|
||||
|
||||
for (auto itChildNode : node.children)
|
||||
{
|
||||
for (const Response::Node& childNode : itChildNode.second)
|
||||
res.add_child(itChildNode.first, NodeToPropertyTree(childNode, format));
|
||||
}
|
||||
|
||||
for (auto itChildArrayNode : node.childrenArrays)
|
||||
{
|
||||
const std::vector<Response::Node>& childArrayNodes {itChildArrayNode .second};
|
||||
|
||||
if (format == ResponseFormat::json)
|
||||
{
|
||||
boost::property_tree::ptree array;
|
||||
|
||||
for (const Response::Node& childNode : childArrayNodes )
|
||||
array.push_back(std::make_pair("", NodeToPropertyTree(childNode, format)));
|
||||
|
||||
res.add_child(itChildArrayNode.first, array);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const Response::Node& childNode : childArrayNodes )
|
||||
res.add_child(itChildArrayNode.first, NodeToPropertyTree(childNode, format));
|
||||
}
|
||||
}
|
||||
|
||||
for (auto itAttribute : node.attributes)
|
||||
{
|
||||
std::string key {format == ResponseFormat::xml ? "<xmlattr>." : ""};
|
||||
|
||||
key += itAttribute.first;
|
||||
|
||||
res.put(key, itAttribute.second);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void responseToStream(const Response& response, ResponseFormat format, std::ostream& os)
|
||||
{
|
||||
boost::property_tree::ptree root {NodeToPropertyTree(response._root, format)};
|
||||
|
||||
switch (format)
|
||||
{
|
||||
case ResponseFormat::xml:
|
||||
boost::property_tree::write_xml(os, root);
|
||||
break;
|
||||
case ResponseFormat::json:
|
||||
boost::property_tree::write_json(os, root);
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
|
||||
enum class ResponseFormat
|
||||
{
|
||||
xml,
|
||||
json,
|
||||
};
|
||||
|
||||
std::string ResponseFormatToMimeType(ResponseFormat format);
|
||||
|
||||
class Error
|
||||
{
|
||||
public:
|
||||
enum class Code
|
||||
{
|
||||
Generic = 0,
|
||||
RequiredParameterMissing = 10,
|
||||
ClientMustUpgrade = 20,
|
||||
ServerMustUpgrade = 30,
|
||||
WrongUsernameOrPassword = 40,
|
||||
UserNotAuthorized = 50,
|
||||
RequestedDataNotFound = 70,
|
||||
};
|
||||
|
||||
Error(Code code);
|
||||
Error(const std::string& message);
|
||||
|
||||
Code getCode() const { return _code; }
|
||||
const std::string& getMessage() const { return _message; }
|
||||
|
||||
private:
|
||||
Code _code;
|
||||
std::string _message;
|
||||
};
|
||||
|
||||
class Response
|
||||
{
|
||||
public:
|
||||
struct Node
|
||||
{
|
||||
std::map<std::string, std::string> attributes;
|
||||
std::map<std::string, std::vector<Node>> children;
|
||||
std::map<std::string, std::vector<Node>> childrenArrays;
|
||||
|
||||
// Helpers
|
||||
void setAttribute(const std::string& key, const std::string& value);
|
||||
|
||||
Node& createChild(const std::string& key);
|
||||
Node& createArrayChild(const std::string& key);
|
||||
|
||||
void addChild(const std::string& key, Node node);
|
||||
void addArrayChild(const std::string& key, Node node);
|
||||
};
|
||||
|
||||
static Response createOkResponse();
|
||||
static Response createFailedResponse(const Error& error);
|
||||
|
||||
virtual ~Response() {}
|
||||
Response(const Response&) = delete;
|
||||
Response& operator=(const Response&) = delete;
|
||||
Response(Response&&) = default;
|
||||
Response& operator=(Response&&) = default;
|
||||
|
||||
Node& createNode(const std::string& key);
|
||||
|
||||
private:
|
||||
friend void responseToStream(const Response& response, ResponseFormat format, std::ostream& os);
|
||||
|
||||
Response() = default;
|
||||
Node _root;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -62,6 +62,7 @@ Grabber::setDefaultCover(boost::filesystem::path p)
|
||||
Image::Image
|
||||
Grabber::getDefaultCover(std::size_t size)
|
||||
{
|
||||
LMS_LOG(COVER, DEBUG) << "Getting a default cover using size = " << size;
|
||||
std::unique_lock<std::mutex> lock(_mutex);
|
||||
|
||||
auto it = _defaultCovers.find(size);
|
||||
@@ -69,7 +70,11 @@ Grabber::getDefaultCover(std::size_t size)
|
||||
{
|
||||
Image::Image cover = _defaultCover;
|
||||
|
||||
cover.scale(size);
|
||||
LMS_LOG(COVER, DEBUG) << "default cover size = " << cover.getSize().width << " x " << cover.getSize().height;
|
||||
|
||||
LMS_LOG(COVER, DEBUG) << "Scaling cover to size = " << size;
|
||||
cover.scale(Image::Geometry{size, size});
|
||||
LMS_LOG(COVER, DEBUG) << "Scaling DONE";
|
||||
auto res = _defaultCovers.insert(std::make_pair(size, cover));
|
||||
assert(res.second);
|
||||
it = res.first;
|
||||
@@ -192,7 +197,7 @@ Grabber::getFromTrack(Wt::Dbo::Session& session, Database::IdType trackId, std::
|
||||
if (!cover)
|
||||
cover = getDefaultCover(size);
|
||||
else
|
||||
cover->scale(size);
|
||||
cover->scale(Image::Geometry {size, size});
|
||||
|
||||
return *cover;
|
||||
}
|
||||
@@ -225,7 +230,7 @@ Grabber::getFromRelease(Wt::Dbo::Session& session, Database::IdType releaseId, s
|
||||
if (!cover)
|
||||
cover = getDefaultCover(size);
|
||||
else
|
||||
cover->scale(size);
|
||||
cover->scale(Image::Geometry {size, size});
|
||||
|
||||
return *cover;
|
||||
}
|
||||
|
||||
@@ -61,10 +61,30 @@ Release::create(Wt::Dbo::Session& session, const std::string& name, const std::s
|
||||
return session.add(std::make_unique<Release>(name, MBID));
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getAll(Wt::Dbo::Session& session, int offset, int size)
|
||||
std::size_t
|
||||
Release::getCount(Wt::Dbo::Session& session)
|
||||
{
|
||||
Wt::Dbo::collection<pointer> res = session.find<Release>().offset(offset).limit(size);
|
||||
Wt::Dbo::collection<pointer> releases {session.find<Release>()};
|
||||
return releases.size();
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getAll(Wt::Dbo::Session& session, boost::optional<std::size_t> offset, boost::optional<std::size_t> size)
|
||||
{
|
||||
Wt::Dbo::collection<pointer> res = session.find<Release>()
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.limit(size ? static_cast<int>(*size) : - 1);
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getAllRandom(Wt::Dbo::Session& session, boost::optional<std::size_t> size)
|
||||
{
|
||||
Wt::Dbo::collection<pointer> res = session.find<Release>()
|
||||
.limit(size ? static_cast<int>(*size) : - 1)
|
||||
.orderBy("RANDOM()");
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
@@ -77,13 +97,14 @@ Release::getAllOrphans(Wt::Dbo::Session& session)
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, int limit)
|
||||
Release::getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, boost::optional<std::size_t> offset, boost::optional<std::size_t> limit)
|
||||
{
|
||||
Wt::Dbo::collection<Release::pointer> res = session.query<Release::pointer>("SELECT r from release r INNER JOIN track t ON r.id = t.release_id")
|
||||
.where("t.file_added > ?").bind(after)
|
||||
.groupBy("r.id")
|
||||
.orderBy("t.file_added DESC")
|
||||
.limit(limit);
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
@@ -44,12 +44,14 @@ class Release : public Wt::Dbo::Dbo<Release>
|
||||
Release(const std::string& name, const std::string& MBID = "");
|
||||
|
||||
// Accessors
|
||||
static std::size_t getCount(Wt::Dbo::Session& session);
|
||||
static pointer getByMBID(Wt::Dbo::Session& session, const std::string& MBID);
|
||||
static std::vector<pointer> getByName(Wt::Dbo::Session& session, const std::string& name);
|
||||
static pointer getById(Wt::Dbo::Session& session, IdType id);
|
||||
static std::vector<pointer> getAllOrphans(Wt::Dbo::Session& session); // no track related
|
||||
static std::vector<pointer> getAll(Wt::Dbo::Session& session, int offset, int size);
|
||||
static std::vector<pointer> getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, int size = 1);
|
||||
static std::vector<pointer> getAll(Wt::Dbo::Session& session, boost::optional<std::size_t> offset = {}, boost::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getAllRandom(Wt::Dbo::Session& session, boost::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, boost::optional<std::size_t> offset = {}, boost::optional<std::size_t> size = {});
|
||||
|
||||
static std::vector<pointer> getByFilter(Wt::Dbo::Session& session,
|
||||
const std::set<IdType>& clusters, // at least one track that belongs to these clusters
|
||||
|
||||
+14
-2
@@ -38,9 +38,21 @@ _filePath( p.string() )
|
||||
}
|
||||
|
||||
Wt::Dbo::collection< Track::pointer >
|
||||
Track::getAll(Wt::Dbo::Session& session)
|
||||
Track::getAll(Wt::Dbo::Session& session, boost::optional<std::size_t> limit)
|
||||
{
|
||||
return session.find<Track>();
|
||||
int size {limit ? static_cast<int>(*limit) : -1};
|
||||
|
||||
return session.find<Track>().limit(size);
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getAllRandom(Wt::Dbo::Session& session, boost::optional<std::size_t> limit)
|
||||
{
|
||||
Wt::Dbo::collection<Track::pointer> res {session.find<Track>()
|
||||
.limit(limit ? static_cast<int>(*limit) : -1)
|
||||
.orderBy("RANDOM()")};
|
||||
|
||||
return std::vector<Track::pointer>(std::cbegin(res), std::cend(res));
|
||||
}
|
||||
|
||||
std::vector<IdType>
|
||||
|
||||
@@ -63,7 +63,8 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
int size,
|
||||
bool& moreExpected);
|
||||
|
||||
static Wt::Dbo::collection< pointer > getAll(Wt::Dbo::Session& session);
|
||||
static Wt::Dbo::collection< pointer > getAll(Wt::Dbo::Session& session, boost::optional<std::size_t> limit = {});
|
||||
static std::vector<pointer> getAllRandom(Wt::Dbo::Session& session, boost::optional<std::size_t> limit = {});
|
||||
static std::vector<IdType> getAllIds(Wt::Dbo::Session& session); // nested transaction
|
||||
static std::vector<boost::filesystem::path> getAllPaths(Wt::Dbo::Session& session); // nested transaction
|
||||
static std::vector<pointer> getMBIDDuplicates(Wt::Dbo::Session& session);
|
||||
|
||||
+28
-10
@@ -83,15 +83,22 @@ Image::load(boost::filesystem::path p)
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
Image::scale(std::size_t size)
|
||||
Geometry
|
||||
Image::getSize() const
|
||||
{
|
||||
if (!size)
|
||||
Magick::Geometry geometry {_image.size()};
|
||||
return {geometry.width(), geometry.height()};
|
||||
}
|
||||
|
||||
bool
|
||||
Image::scale(Geometry geometry)
|
||||
{
|
||||
if (geometry.width == 0 || geometry.height == 0)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
_image.resize( Magick::Geometry(size, size ) );
|
||||
_image.resize( Magick::Geometry(geometry.width, geometry.height ) );
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -105,16 +112,27 @@ Image::scale(std::size_t size)
|
||||
std::vector<uint8_t>
|
||||
Image::save(Format format) const
|
||||
{
|
||||
Magick::Image outputImage(_image);
|
||||
std::vector<uint8_t> res;
|
||||
|
||||
outputImage.magick( format_to_magick(format));
|
||||
try
|
||||
{
|
||||
Magick::Image outputImage(_image);
|
||||
|
||||
Magick::Blob blob;
|
||||
outputImage.write(&blob);
|
||||
outputImage.magick( format_to_magick(format));
|
||||
|
||||
auto begin = static_cast<const uint8_t*>(blob.data());
|
||||
Magick::Blob blob;
|
||||
outputImage.write(&blob);
|
||||
|
||||
return std::vector<uint8_t>(begin, begin + blob.length());
|
||||
auto begin = static_cast<const uint8_t*>(blob.data());
|
||||
std::copy(begin, begin + blob.length(), std::back_inserter(res));
|
||||
return res;
|
||||
}
|
||||
catch (Magick::Exception& e)
|
||||
{
|
||||
LMS_LOG(COVER, ERROR) << "Caught Magick exception during save:" << e.what();
|
||||
res.clear();
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Image
|
||||
|
||||
+9
-1
@@ -37,6 +37,12 @@ std::string format_to_mimeType(Format format);
|
||||
|
||||
void init(const char *path);
|
||||
|
||||
struct Geometry
|
||||
{
|
||||
std::size_t width;
|
||||
std::size_t height;
|
||||
};
|
||||
|
||||
class Image
|
||||
{
|
||||
public:
|
||||
@@ -45,8 +51,10 @@ class Image
|
||||
bool load(const std::vector<unsigned char>& rawData);
|
||||
bool load(boost::filesystem::path p);
|
||||
|
||||
Geometry getSize() const;
|
||||
|
||||
// Operations
|
||||
bool scale(std::size_t size);
|
||||
bool scale(Geometry geometry);
|
||||
|
||||
// output
|
||||
std::vector<uint8_t> save(Format format) const;
|
||||
|
||||
+10
-1
@@ -23,6 +23,7 @@
|
||||
#include <Wt/WServer.h>
|
||||
#include <Wt/WApplication.h>
|
||||
|
||||
#include "api/subsonic/SubsonicResource.hpp"
|
||||
#include "av/AvInfo.hpp"
|
||||
#include "av/AvTranscoder.hpp"
|
||||
#include "cover/CoverArtGrabber.hpp"
|
||||
@@ -132,9 +133,17 @@ int main(int argc, char* argv[])
|
||||
|
||||
getServices().mediaScanner->setAddon(similarityFeaturesScannerAddon);
|
||||
getServices().coverArtGrabber = std::make_unique<CoverArt::Grabber>();
|
||||
getServices().coverArtGrabber->setDefaultCover(server.appRoot() + "/images/unknown-cover.jpg");
|
||||
|
||||
getServices().similaritySearcher = std::make_unique<Similarity::Searcher>(similarityFeaturesScannerAddon);
|
||||
|
||||
// bind entry point
|
||||
API::Subsonic::SubsonicResource subsonicResource {*connectionPool};
|
||||
|
||||
// bind API resources
|
||||
for (const std::string& path : API::Subsonic::SubsonicResource::getPaths())
|
||||
server.addResource(&subsonicResource, path);
|
||||
|
||||
// bind UI entry point
|
||||
server.addEntryPoint(Wt::EntryPointType::Application,
|
||||
std::bind(UserInterface::LmsApplication::create,
|
||||
std::placeholders::_1, std::ref(*connectionPool), std::ref(appGroups)));
|
||||
|
||||
@@ -109,9 +109,6 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
|
||||
messageResourceBundle().use(appRoot() + "tracks");
|
||||
messageResourceBundle().use(appRoot() + "tracksinfo");
|
||||
|
||||
// hack since Server does not expose the docRoot
|
||||
getServices().coverArtGrabber->setDefaultCover(Wt::WApplication::instance()->docRoot() + "/images/unknown-cover.jpg");
|
||||
|
||||
// Require js here to avoid async problems
|
||||
requireJQuery("/js/jquery-1.10.2.min.js");
|
||||
require("/js/mediaplayer.js");
|
||||
|
||||
@@ -60,7 +60,7 @@ ReleasesInfo::refreshRecentlyAdded()
|
||||
|
||||
Wt::Dbo::Transaction transaction(LmsApp->getDboSession());
|
||||
|
||||
auto releases = Release::getLastAdded(LmsApp->getDboSession(), after, 5);
|
||||
auto releases {Release::getLastAdded(LmsApp->getDboSession(), after, 0, 5)};
|
||||
|
||||
_recentlyAddedContainer->clear();
|
||||
for (auto release : releases)
|
||||
|
||||
@@ -23,6 +23,7 @@ std::string getModuleName(Module mod)
|
||||
{
|
||||
switch (mod)
|
||||
{
|
||||
case Module::API_SUBSONIC: return "API_SUBSONIC";
|
||||
case Module::AV: return "AV";
|
||||
case Module::COVER: return "COVER";
|
||||
case Module::DB: return "DB";
|
||||
|
||||
@@ -35,6 +35,7 @@ enum class Severity
|
||||
|
||||
enum class Module
|
||||
{
|
||||
API_SUBSONIC,
|
||||
AV,
|
||||
COVER,
|
||||
DB,
|
||||
|
||||
Reference in New Issue
Block a user