Subsonic API: do not use ffmpeg to remux the files when transcoding is off. fixes #36

This commit is contained in:
emeric
2020-03-08 17:03:49 +01:00
parent e1755342a4
commit 7d5654cb7d
18 changed files with 643 additions and 312 deletions
+2
View File
@@ -2,6 +2,7 @@
add_library(lmsav SHARED
impl/AvInfo.cpp
impl/AvTranscoder.cpp
impl/AvTranscodeResourceHandler.cpp
impl/AvTypes.cpp
)
@@ -18,6 +19,7 @@ target_link_libraries(lmsav PUBLIC
lmsutils
avformat
avutil
wt
)
install(TARGETS lmsav DESTINATION lib)
@@ -0,0 +1,59 @@
/*
* Copyright (C) 2020 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 "AvTranscodeResourceHandler.hpp"
namespace Av
{
std::unique_ptr<IResourceHandler>
createTranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters)
{
return std::make_unique<TranscodeResourceHandler>(trackPath, parameters);
}
// TODO set some nice HTTP return code
TranscodeResourceHandler::TranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters)
: _transcoder {trackPath, parameters}
{
_transcoder.start();
}
void
TranscodeResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
{
response.setMimeType(_transcoder.getOutputMimeType());
if (!_transcoder.isComplete())
{
std::vector<unsigned char> buffer;
_transcoder.process(buffer, _chunkSize);
response.out().write(reinterpret_cast<const char *>(&buffer[0]), buffer.size());
}
}
bool
TranscodeResourceHandler::isFinished() const
{
return _transcoder.isComplete();
}
}
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2020 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 <filesystem>
#include "av/AvTranscoder.hpp"
#include "utils/IResourceHandler.hpp"
namespace Av
{
class TranscodeResourceHandler final : public IResourceHandler
{
public:
TranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters);
private:
void processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override;
bool isFinished() const override;
static constexpr std::size_t _chunkSize {262144};
const std::filesystem::path _trackPath;
Transcoder _transcoder;
};
}
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2020 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 <filesystem>
#include <memory>
#include "utils/IResourceHandler.hpp"
namespace Av
{
struct TranscodeParameters;
std::unique_ptr<IResourceHandler> createTranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters);
}
+2
View File
@@ -1,5 +1,7 @@
add_library(lmssubsonic SHARED
impl/ParameterParsing.cpp
impl/Stream.cpp
impl/SubsonicId.cpp
impl/SubsonicResource.cpp
impl/SubsonicResponse.cpp
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2020 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 "ParameterParsing.hpp"
namespace StringUtils
{
template<>
std::optional<API::Subsonic::Id>
StringUtils::readAs(const std::string& str)
{
return API::Subsonic::IdFromString(str);
}
template<>
std::optional<bool>
StringUtils::readAs(const std::string& str)
{
if (str == "true")
return true;
else if (str == "false")
return false;
return {};
}
}
@@ -0,0 +1,99 @@
/*
* Copyright (C) 2020 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 <Wt/Http/Request.h>
#include "utils/String.hpp"
#include "SubsonicId.hpp"
#include "SubsonicResponse.hpp"
namespace API::Subsonic
{
template<typename T>
std::vector<T>
getMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& paramName)
{
std::vector<T> res;
auto it = parameterMap.find(paramName);
if (it == parameterMap.end())
return res;
for (const std::string& param : it->second)
{
auto value {StringUtils::readAs<T>(param)};
if (!value)
throw BadParameterFormatGenericError {paramName};
res.emplace_back(std::move(*value));
}
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 RequiredParameterMissingError {};
return res;
}
template<typename T>
std::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 RequiredParameterMissingError {};
return *res;
}
}
namespace StringUtils
{
template<>
std::optional<API::Subsonic::Id>
StringUtils::readAs(const std::string& str);
template<>
std::optional<bool>
StringUtils::readAs(const std::string& str);
}
+44
View File
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2020 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 <string>
#include <Wt/Http/Request.h>
#include <Wt/Http/ResponseContinuation.h>
namespace Database
{
class Session;
}
namespace API::Subsonic
{
struct RequestContext
{
const Wt::Http::ParameterMap& parameters;
Database::Session& dbSession;
std::string userName;
};
}
+139
View File
@@ -0,0 +1,139 @@
/*
* Copyright (C) 2020 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 "Stream.hpp"
#include "av/AvTranscoder.hpp"
#include "av/AvTranscodeResourceHandlerCreator.hpp"
#include "av/AvTypes.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/User.hpp"
#include "utils/IResourceHandler.hpp"
#include "utils/FileResourceHandlerCreator.hpp"
#include "utils/Utils.hpp"
#include "ParameterParsing.hpp"
#include "SubsonicId.hpp"
using namespace Database;
namespace API::Subsonic::Stream
{
static
Av::Encoding
userTranscodeFormatToAvEncoding(AudioFormat format)
{
switch (format)
{
case AudioFormat::MP3: return Av::Encoding::MP3;
case AudioFormat::OGG_OPUS: return Av::Encoding::OGG_OPUS;
case AudioFormat::MATROSKA_OPUS: return Av::Encoding::MATROSKA_OPUS;
case AudioFormat::OGG_VORBIS: return Av::Encoding::OGG_VORBIS;
case AudioFormat::WEBM_VORBIS: return Av::Encoding::WEBM_VORBIS;
default: return Av::Encoding::OGG_OPUS;
}
}
struct StreamParameters
{
std::filesystem::path trackPath;
std::optional<Av::TranscodeParameters> transcodeParameters;
};
static
StreamParameters
getStreamParameters(RequestContext& context)
{
// Mandatory params
Id id {getMandatoryParameterAs<Id>(context.parameters, "id")};
// Optional params
std::optional<std::size_t> maxBitRate {getParameterAs<std::size_t>(context.parameters, "maxBitRate")};
std::optional<std::string> format {getParameterAs<std::string>(context.parameters, "format")};
StreamParameters parameters;
auto transaction {context.dbSession.createSharedTransaction()};
{
auto track {Track::getById(context.dbSession, id.value)};
if (!track)
throw RequestedDataNotFoundError {};
parameters.trackPath = track->getPath();
}
{
const User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
if (!user)
throw UserNotAuthorizedError {};
// format = "raw" => no transcode. Other format values will be ignored
const bool transcode {(!format || (*format != "raw")) && user->getAudioTranscodeEnable()};
if (transcode)
{
// "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);
Av::TranscodeParameters transcodeParameters;
transcodeParameters.bitrate = *maxBitRate * 1000;
transcodeParameters.encoding = userTranscodeFormatToAvEncoding(user->getAudioTranscodeFormat());
parameters.transcodeParameters = std::move(transcodeParameters);
}
}
return parameters;
}
void
handle(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response)
{
std::shared_ptr<IResourceHandler> resourceHandler;
Wt::Http::ResponseContinuation *continuation = request.continuation();
if (!continuation)
{
StreamParameters streamParameters {getStreamParameters(context)};
if (streamParameters.transcodeParameters)
resourceHandler = Av::createTranscodeResourceHandler(streamParameters.trackPath, *streamParameters.transcodeParameters);
else
resourceHandler = createFileResourceHandler(streamParameters.trackPath);
}
else
{
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
}
resourceHandler->processRequest(request, response);
if (!resourceHandler->isFinished())
{
Wt::Http::ResponseContinuation *continuation = response.createContinuation();
continuation->setData(resourceHandler);
}
}
}
@@ -19,22 +19,13 @@
#pragma once
#include <filesystem>
#include <Wt/Http/Request.h>
#include <Wt/Http/Response.h>
#include "RequestContext.hpp"
// Helper used to deliver file contents from a WResource
namespace FileResourceHandler
namespace API::Subsonic::Stream
{
struct ContinuationData
{
std::filesystem::path path;
::uint64_t beyondLastByte;
::uint64_t offset;
};
std::optional<ContinuationData> handleInitialRequest(const Wt::Http::Request& request, Wt::Http::Response& response, const std::filesystem::path& path);
std::optional<ContinuationData> handleContinuationRequest(const Wt::Http::Request& request, Wt::Http::Response& response, const ContinuationData& continuationData);
void handle(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response);
}
+2
View File
@@ -17,6 +17,8 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include "database/Types.hpp"
+23 -241
View File
@@ -19,15 +19,10 @@
#include "subsonic/SubsonicResource.hpp"
#include <atomic>
#include <mutex>
#include <numeric>
#include <random>
#include <thread>
#include <Wt/WLocalDateTime.h>
#include "auth/IPasswordService.hpp"
#include "av/AvTranscoder.hpp"
#include "cover/ICoverArtGrabber.hpp"
#include "database/Artist.hpp"
#include "database/Cluster.hpp"
@@ -40,10 +35,13 @@
#include "database/User.hpp"
#include "recommendation/IEngine.hpp"
#include "utils/Logger.hpp"
#include "utils/Random.hpp"
#include "utils/Service.hpp"
#include "utils/String.hpp"
#include "utils/Utils.hpp"
#include "SubsonicId.hpp"
#include "ParameterParsing.hpp"
#include "RequestContext.hpp"
#include "Stream.hpp"
#include "SubsonicResponse.hpp"
using namespace Database;
@@ -65,25 +63,6 @@ namespace API::Subsonic
namespace StringUtils
{
template<>
std::optional<API::Subsonic::Id>
StringUtils::readAs(const std::string& str)
{
return API::Subsonic::IdFromString(str);
}
template<>
std::optional<bool>
StringUtils::readAs(const std::string& str)
{
if (str == "true")
return true;
else if (str == "false")
return false;
return {};
}
template<>
std::optional<API::Subsonic::ClientVersion>
StringUtils::readAs(const std::string& str)
@@ -122,21 +101,6 @@ namespace StringUtils
namespace API::Subsonic
{
struct ClientInfo
{
std::string name;
std::string user;
std::string password;
ClientVersion version;
};
struct RequestContext
{
const Wt::Http::ParameterMap& parameters;
Session& dbSession;
std::string userName;
};
static
std::string
makeNameFilesystemCompatible(const std::string& name)
@@ -144,62 +108,6 @@ makeNameFilesystemCompatible(const std::string& name)
return StringUtils::replaceInString(name, "/", "_");
}
template<typename T>
std::vector<T>
getMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& paramName)
{
std::vector<T> res;
auto it = parameterMap.find(paramName);
if (it == parameterMap.end())
return res;
for (const std::string& param : it->second)
{
auto value {StringUtils::readAs<T>(param)};
if (!value)
throw BadParameterFormatGenericError {paramName};
res.emplace_back(std::move(*value));
}
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 RequiredParameterMissingError {};
return res;
}
template<typename T>
std::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 RequiredParameterMissingError {};
return *res;
}
static
std::string
decodePasswordIfNeeded(const std::string& password)
@@ -216,6 +124,14 @@ decodePasswordIfNeeded(const std::string& password)
return password;
}
struct ClientInfo
{
std::string name;
std::string user;
std::string password;
ClientVersion version;
};
static
ClientInfo
getClientInfo(const Wt::Http::ParameterMap& parameters)
@@ -238,14 +154,6 @@ getClientInfo(const Wt::Http::ParameterMap& parameters)
return res;
}
// MediaRetrievals
struct MediaRetrievalResult
{
std::string mimeType;
std::vector<uint8_t> data;
Wt::cpp17::any continuationData;
};
SubsonicResource::SubsonicResource(Db& db)
: _sessionPool {db}
{
@@ -1165,9 +1073,7 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
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);
Random::shuffleContainer(tracks);
Response response {Response::createOkResponse()};
Response::Node& similarSongsNode {response.createNode(id3 ? "similarSongs2" : "similarSongs")};
@@ -1775,111 +1681,8 @@ handleNotImplemented(RequestContext&)
}
static
Av::Encoding
userTranscodeFormatToAvEncoding(AudioFormat format)
{
switch (format)
{
case AudioFormat::MP3: return Av::Encoding::MP3;
case AudioFormat::OGG_OPUS: return Av::Encoding::OGG_OPUS;
case AudioFormat::MATROSKA_OPUS: return Av::Encoding::MATROSKA_OPUS;
case AudioFormat::OGG_VORBIS: return Av::Encoding::OGG_VORBIS;
case AudioFormat::WEBM_VORBIS: return Av::Encoding::WEBM_VORBIS;
default: return Av::Encoding::OGG_OPUS;
}
}
static
std::shared_ptr<Av::Transcoder>
createTranscoder(RequestContext& context)
{
// Mandatory params
Id id {getMandatoryParameterAs<Id>(context.parameters, "id")};
// Optional params
std::optional<std::size_t> maxBitRate {getParameterAs<std::size_t>(context.parameters, "maxBitRate")};
std::optional<std::string> format {getParameterAs<std::string>(context.parameters, "format")};
Av::TranscodeParameters parameters {};
parameters.stripMetadata = false; // Since it can be cached and some players read the metadata from the downloaded file
std::filesystem::path trackPath;
{
auto transaction {context.dbSession.createSharedTransaction()};
{
auto track {Track::getById(context.dbSession, id.value)};
if (!track)
throw RequestedDataNotFoundError {};
trackPath = track->getPath();
}
{
const User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
if (!user)
throw UserNotAuthorizedError {};
// format = "raw" => no transcode. Other format values will be ignored
const bool transcode {(!format || (format && *format != "raw")) && user->getAudioTranscodeEnable()};
if (transcode)
{
// "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);
parameters.bitrate = *maxBitRate * 1000;
parameters.encoding = userTranscodeFormatToAvEncoding(user->getAudioTranscodeFormat());
}
}
}
return std::make_shared<Av::Transcoder>(trackPath, parameters);
}
static
MediaRetrievalResult
handleStream(RequestContext& context, Wt::Http::ResponseContinuation* continuation)
{
MediaRetrievalResult res;
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 InternalErrorGenericError {"Cannot create transcoder"};
if (!transcoder->isComplete())
{
static constexpr std::size_t chunkSize {65536*4};
res.data.reserve(chunkSize);
transcoder->process(res.data, chunkSize);
}
if (!transcoder->isComplete())
res.continuationData = std::move(transcoder);
return res;
}
static
MediaRetrievalResult
handleGetCoverArt(RequestContext& context, Wt::Http::ResponseContinuation*)
void
handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
{
// Mandatory params
Id id {getMandatoryParameterAs<Id>(context.parameters, "id")};
@@ -1887,23 +1690,21 @@ handleGetCoverArt(RequestContext& context, Wt::Http::ResponseContinuation*)
std::size_t size {getParameterAs<std::size_t>(context.parameters, "size").value_or(256)};
size = clamp(size, std::size_t {32}, std::size_t {1024});
MediaRetrievalResult res;
std::vector<unsigned char> data;
switch (id.type)
{
case Id::Type::Track:
res.data = ServiceProvider<CoverArt::IGrabber>::get()->getFromTrack(context.dbSession, id.value, CoverArt::Format::JPEG, size);
data = ServiceProvider<CoverArt::IGrabber>::get()->getFromTrack(context.dbSession, id.value, CoverArt::Format::JPEG, size);
break;
case Id::Type::Release:
res.data = ServiceProvider<CoverArt::IGrabber>::get()->getFromRelease(context.dbSession, id.value, CoverArt::Format::JPEG, size);
data = ServiceProvider<CoverArt::IGrabber>::get()->getFromRelease(context.dbSession, id.value, CoverArt::Format::JPEG, size);
break;
default:
throw BadParameterGenericError {"id"};
}
res.mimeType = CoverArt::formatToMimeType(CoverArt::Format::JPEG);
return res;
response.out().write(reinterpret_cast<const char*>(&data[0]), data.size());
response.setMimeType(CoverArt::formatToMimeType(CoverArt::Format::JPEG));
}
using RequestHandlerFunc = std::function<Response(RequestContext& context)>;
@@ -2019,12 +1820,12 @@ static std::unordered_map<std::string, RequestEntryPointInfo> requestEntryPoints
{"startScan", {handleNotImplemented, true}},
};
using MediaRetrievalHandlerFunc = std::function<MediaRetrievalResult(RequestContext&, Wt::Http::ResponseContinuation*)>;
using MediaRetrievalHandlerFunc = std::function<void(RequestContext&, const Wt::Http::Request&, Wt::Http::Response&)>;
static std::unordered_map<std::string, MediaRetrievalHandlerFunc> mediaRetrievalHandlers
{
// Media retrieval
{"getCoverArt", handleGetCoverArt},
{"stream", handleStream},
{"stream", Stream::handle},
};
void
@@ -2091,26 +1892,7 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp
auto itStreamHandler {mediaRetrievalHandlers.find(requestPath)};
if (itStreamHandler != mediaRetrievalHandlers.end())
{
MediaRetrievalResult res {itStreamHandler->second(requestContext, request.continuation())};
if (!res.mimeType.empty())
response.setMimeType(res.mimeType);
if (!res.data.empty())
{
response.out().write(reinterpret_cast<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));
}
itStreamHandler->second(requestContext, request, response);
LMS_LOG(API_SUBSONIC, DEBUG) << "Request " << requestId << " '" << requestPath << "' handled!";
return;
}
@@ -16,6 +16,7 @@
* 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 <map>
#include <string>
+32 -51
View File
@@ -17,25 +17,30 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "utils/FileResourceHandler.hpp"
#include "FileResourceHandler.hpp"
#include <fstream>
#include "utils/Logger.hpp"
namespace FileResourceHandler
std::unique_ptr<IResourceHandler>
createFileResourceHandler(const std::filesystem::path& path)
{
return std::make_unique<FileResourceHandler>(path);
}
static constexpr std::size_t _chunkSize {262144};
static
std::optional<ContinuationData>
handleRequestPiecewise(const Wt::Http::Request& request,
Wt::Http::Response& response,
ContinuationData continuationData)
FileResourceHandler::FileResourceHandler(const std::filesystem::path& path)
: _path {path}
{
::uint64_t startByte {continuationData.offset};
std::ifstream ifs {continuationData.path.string().c_str(), std::ios::in | std::ios::binary};
}
void
FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
{
::uint64_t startByte {_offset};
std::ifstream ifs {_path.string().c_str(), std::ios::in | std::ios::binary};
LMS_LOG(UTILS, DEBUG) << "startByte = " << startByte;
@@ -43,9 +48,10 @@ handleRequestPiecewise(const Wt::Http::Request& request,
{
if (!ifs)
{
LMS_LOG(UTILS, ERROR) << "Cannot open file stream for '" << continuationData.path.string() << "'";
LMS_LOG(UTILS, ERROR) << "Cannot open file stream for '" << _path.string() << "'";
response.setStatus(404);
return std::nullopt;
_isFinished = true;
return;
}
else
{
@@ -67,7 +73,8 @@ handleRequestPiecewise(const Wt::Http::Request& request,
response.addHeader("Content-Range", contentRange.str());
LMS_LOG(UTILS, DEBUG) << "Range not satisfiable";
return std::nullopt;
_isFinished = true;
return;
}
if (ranges.size() == 1)
@@ -76,21 +83,21 @@ handleRequestPiecewise(const Wt::Http::Request& request,
response.setStatus(206);
startByte = ranges[0].firstByte();
continuationData.beyondLastByte = ranges[0].lastByte() + 1;
_beyondLastByte = ranges[0].lastByte() + 1;
std::ostringstream contentRange;
contentRange << "bytes " << startByte << "-"
<< continuationData.beyondLastByte - 1 << "/" << fileSize;
<< _beyondLastByte - 1 << "/" << fileSize;
response.addHeader("Content-Range", contentRange.str());
response.setContentLength(continuationData.beyondLastByte - startByte);
response.setContentLength(_beyondLastByte - startByte);
}
else
{
LMS_LOG(UTILS, DEBUG) << "No range requested";
continuationData.beyondLastByte = fileSize;
response.setContentLength(continuationData.beyondLastByte);
_beyondLastByte = fileSize;
response.setContentLength(_beyondLastByte);
}
}
@@ -99,7 +106,7 @@ handleRequestPiecewise(const Wt::Http::Request& request,
std::vector<char> buf;
buf.resize(_chunkSize);
::uint64_t restSize = continuationData.beyondLastByte - startByte;
::uint64_t restSize = _beyondLastByte - startByte;
::uint64_t pieceSize = buf.size() > restSize ? restSize : buf.size();
ifs.read(&buf[0], pieceSize);
@@ -111,47 +118,21 @@ handleRequestPiecewise(const Wt::Http::Request& request,
LMS_LOG(UTILS, DEBUG) << "Progress: " << actualPieceSize << "/" << restSize;
if (ifs.good() && actualPieceSize < restSize)
{
ContinuationData newContinuationData {continuationData};
newContinuationData.offset = startByte + actualPieceSize;
_offset = startByte + actualPieceSize;
LMS_LOG(UTILS, DEBUG) << "Job not complete! Next chunk offset = " << newContinuationData.offset;
return newContinuationData;
LMS_LOG(UTILS, DEBUG) << "Job not complete! Next chunk offset = " << _offset;
}
else
{
_isFinished = true;
LMS_LOG(UTILS, DEBUG) << "Job complete!";
}
return std::nullopt;
}
std::optional<ContinuationData>
handleInitialRequest(const Wt::Http::Request& request,
Wt::Http::Response& response,
const std::filesystem::path& path)
bool
FileResourceHandler::isFinished() const
{
ContinuationData continuationData;
continuationData.path = path;
continuationData.offset = 0;
continuationData.beyondLastByte = 0;
LMS_LOG(UTILS, DEBUG) << "Initial request for file '" << path << "'";
return handleRequestPiecewise(request, response, continuationData);
return _isFinished;
}
std::optional<ContinuationData>
handleContinuationRequest(const Wt::Http::Request& request,
Wt::Http::Response& response,
const ContinuationData& continuationData)
{
LMS_LOG(UTILS, DEBUG) << "Continuation request for file '" << continuationData.path << "', offset = " << continuationData.offset;
return handleRequestPiecewise(request, response, continuationData);
}
} // ns FileResourceHandler
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2020 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 <filesystem>
#include "utils/IResourceHandler.hpp"
class FileResourceHandler final : public IResourceHandler
{
public:
FileResourceHandler(const std::filesystem::path& filePath);
private:
void processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
bool isFinished() const override;
static constexpr std::size_t _chunkSize {262144};
std::filesystem::path _path;
::uint64_t _beyondLastByte {};
::uint64_t _offset {};
bool _isFinished {};
};
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2020 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 <filesystem>
#include <memory>
#include "utils/IResourceHandler.hpp"
std::unique_ptr<IResourceHandler> createFileResourceHandler(const std::filesystem::path& path);
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2020 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 <Wt/Http/Request.h>
#include <Wt/Http/Response.h>
// Helper class to serve a resource (must be saved as continuation data if not complete)
class IResourceHandler
{
public:
virtual ~IResourceHandler() = default;
virtual void processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) = 0;
virtual bool isFinished() const = 0;
};
+10 -8
View File
@@ -24,7 +24,7 @@
#include "av/AvInfo.hpp"
#include "database/Track.hpp"
#include "utils/FileResourceHandler.hpp"
#include "utils/FileResourceHandlerCreator.hpp"
#include "utils/Logger.hpp"
#include "utils/String.hpp"
#include "LmsApplication.hpp"
@@ -88,29 +88,31 @@ void
AudioFileResource::handleRequest(const Wt::Http::Request& request,
Wt::Http::Response& response)
{
std::optional<FileResourceHandler::ContinuationData> continuationData;
std::shared_ptr<IResourceHandler> fileResourceHandler;
if (!request.continuation())
{
auto trackPath {getTrackPathFromURLArgs(request)};
if (!trackPath)
return;
continuationData = FileResourceHandler::handleInitialRequest(request, response, *trackPath);
fileResourceHandler = createFileResourceHandler(*trackPath);
}
else
{
auto currentContinuationData {Wt::cpp17::any_cast<FileResourceHandler::ContinuationData>(request.continuation()->data())};
continuationData = FileResourceHandler::handleContinuationRequest(request, response, currentContinuationData);
fileResourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(request.continuation()->data());
}
if (continuationData)
fileResourceHandler->processRequest(request, response);
if (!fileResourceHandler->isFinished())
{
auto* continuation {response.createContinuation()};
continuation->setData(*continuationData);
continuation->setData(fileResourceHandler);
}
}
} // namespace UserInterface