Subsonic API: added a per-user setting to enable transcoding by default (breaking change: default is disabled). fixes #351, ref #367
This commit is contained in:
@@ -243,6 +243,12 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
void migrateFromV45(Session& session)
|
||||
{
|
||||
// add subsonic_enable_transcoding_by_default, default is disabled
|
||||
session.getDboSession().execute("ALTER TABLE user ADD subsonic_enable_transcoding_by_default INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*User::defaultSubsonicEnableTranscodingByDefault*/0)) + ")");
|
||||
}
|
||||
|
||||
void doDbMigration(Session& session)
|
||||
{
|
||||
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||
@@ -266,6 +272,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
||||
{42, migrateFromV42},
|
||||
{43, migrateFromV43},
|
||||
{44, migrateFromV44},
|
||||
{45, migrateFromV45},
|
||||
};
|
||||
|
||||
{
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Database
|
||||
class Session;
|
||||
|
||||
using Version = std::size_t;
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 45 };
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 46 };
|
||||
class VersionInfo
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -58,6 +58,7 @@ namespace Database {
|
||||
|
||||
static inline constexpr std::size_t MinNameLength{ 3 };
|
||||
static inline constexpr std::size_t MaxNameLength{ 15 };
|
||||
static inline constexpr bool defaultSubsonicEnableTranscodingByDefault{ false };
|
||||
static inline constexpr TranscodingOutputFormat defaultSubsonicTranscodingOutputFormat{ TranscodingOutputFormat::OGG_OPUS };
|
||||
static inline constexpr Bitrate defaultSubsonicTranscodingOutputBitrate{ 128000 };
|
||||
static inline constexpr UITheme defaultUITheme{ UITheme::Dark };
|
||||
@@ -83,6 +84,7 @@ namespace Database {
|
||||
void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; }
|
||||
void setPasswordHash(const PasswordHash& passwordHash) { _passwordSalt = passwordHash.salt; _passwordHash = passwordHash.hash; }
|
||||
void setType(UserType type) { _type = type; }
|
||||
void setSubsonicEnableTranscodingByDefault(bool value) { _subsonicEnableTranscodingByDefault = value; }
|
||||
void setSubsonicDefaultTranscodintOutputFormat(TranscodingOutputFormat encoding) { _subsonicDefaultTranscodingOutputFormat = encoding; }
|
||||
void setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate);
|
||||
void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; }
|
||||
@@ -99,6 +101,7 @@ namespace Database {
|
||||
bool isAdmin() const { return _type == UserType::ADMIN; }
|
||||
bool isDemo() const { return _type == UserType::DEMO; }
|
||||
UserType getType() const { return _type; }
|
||||
bool getSubsonicEnableTranscodingByDefault() const { return _subsonicEnableTranscodingByDefault; }
|
||||
TranscodingOutputFormat getSubsonicDefaultTranscodingOutputFormat() const { return _subsonicDefaultTranscodingOutputFormat; }
|
||||
Bitrate getSubsonicDefaultTranscodingOutputBitrate() const { return _subsonicDefaultTranscodingOutputBitrate; }
|
||||
std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; }
|
||||
@@ -118,6 +121,7 @@ namespace Database {
|
||||
Wt::Dbo::field(a, _passwordSalt, "password_salt");
|
||||
Wt::Dbo::field(a, _passwordHash, "password_hash");
|
||||
Wt::Dbo::field(a, _lastLogin, "last_login");
|
||||
Wt::Dbo::field(a, _subsonicEnableTranscodingByDefault, "subsonic_enable_transcoding_by_default");
|
||||
Wt::Dbo::field(a, _subsonicDefaultTranscodingOutputFormat, "subsonic_default_transcode_format");
|
||||
Wt::Dbo::field(a, _subsonicDefaultTranscodingOutputBitrate, "subsonic_default_transcode_bitrate");
|
||||
Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode");
|
||||
@@ -153,6 +157,7 @@ namespace Database {
|
||||
|
||||
// User defined settings
|
||||
SubsonicArtistListMode _subsonicArtistListMode{ defaultSubsonicArtistListMode };
|
||||
bool _subsonicEnableTranscodingByDefault{ defaultSubsonicEnableTranscodingByDefault };
|
||||
TranscodingOutputFormat _subsonicDefaultTranscodingOutputFormat{ defaultSubsonicTranscodingOutputFormat };
|
||||
int _subsonicDefaultTranscodingOutputBitrate{ defaultSubsonicTranscodingOutputBitrate };
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace API::Subsonic
|
||||
};
|
||||
|
||||
static inline constexpr ProtocolVersion defaultServerProtocolVersion{ 1, 16, 0 };
|
||||
static inline constexpr std::string_view serverVersion{ "3" };
|
||||
static inline constexpr std::string_view serverVersion{ "4" };
|
||||
}
|
||||
|
||||
namespace StringUtils
|
||||
|
||||
@@ -40,8 +40,9 @@ namespace API::Subsonic
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
namespace {
|
||||
std::optional<Av::Transcoding::OutputFormat> subsonicStreamFormatToAvFormat(std::string_view format)
|
||||
namespace
|
||||
{
|
||||
std::optional<Av::Transcoding::OutputFormat> subsonicStreamFormatToAvOutputFormat(std::string_view format)
|
||||
{
|
||||
for (const auto& [str, avFormat] : std::initializer_list<std::pair<std::string_view, Av::Transcoding::OutputFormat>>{
|
||||
{"mp3", Av::Transcoding::OutputFormat::MP3},
|
||||
@@ -68,6 +69,25 @@ namespace API::Subsonic
|
||||
return Av::Transcoding::OutputFormat::OGG_OPUS;
|
||||
}
|
||||
|
||||
bool isCodecCompatibleWithOutputFormat(Av::DecodingCodec codec, Av::Transcoding::OutputFormat outputFormat)
|
||||
{
|
||||
switch (outputFormat)
|
||||
{
|
||||
case Av::Transcoding::OutputFormat::MP3:
|
||||
return codec == Av::DecodingCodec::MP3;
|
||||
|
||||
case Av::Transcoding::OutputFormat::OGG_OPUS:
|
||||
case Av::Transcoding::OutputFormat::MATROSKA_OPUS:
|
||||
return codec == Av::DecodingCodec::OPUS;
|
||||
|
||||
case Av::Transcoding::OutputFormat::OGG_VORBIS:
|
||||
case Av::Transcoding::OutputFormat::WEBM_VORBIS:
|
||||
return codec == Av::DecodingCodec::VORBIS;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
struct StreamParameters
|
||||
{
|
||||
Av::Transcoding::InputParameters inputParameters;
|
||||
@@ -75,70 +95,93 @@ namespace API::Subsonic
|
||||
bool estimateContentLength{};
|
||||
};
|
||||
|
||||
bool isOutputFormatCompatible(const std::filesystem::path& trackPath, Av::Transcoding::OutputFormat outputFormat)
|
||||
{
|
||||
try
|
||||
{
|
||||
const auto audioFile{ Av::parseAudioFile(trackPath) };
|
||||
|
||||
const auto streamInfo{ audioFile->getBestStreamInfo() };
|
||||
if (!streamInfo)
|
||||
throw RequestedDataNotFoundError{}; // TODO 404?
|
||||
|
||||
return isCodecCompatibleWithOutputFormat(streamInfo->codec, outputFormat);
|
||||
}
|
||||
catch (const Av::Exception& e)
|
||||
{
|
||||
// TODO 404?
|
||||
throw RequestedDataNotFoundError{};
|
||||
}
|
||||
}
|
||||
|
||||
StreamParameters getStreamParameters(RequestContext& context)
|
||||
{
|
||||
// Mandatory params
|
||||
const TrackId id{ getMandatoryParameterAs<TrackId>(context.parameters, "id") };
|
||||
|
||||
// Optional params
|
||||
std::size_t maxBitRate{ getParameterAs<std::size_t>(context.parameters, "maxBitRate").value_or(0) }; // "If set to zero, no limit is imposed"
|
||||
std::size_t maxBitRate{ getParameterAs<std::size_t>(context.parameters, "maxBitRate").value_or(0) * 1000 }; // "If set to zero, no limit is imposed", given in kpbs
|
||||
const std::string format{ getParameterAs<std::string>(context.parameters, "format").value_or("") };
|
||||
std::size_t timeOffset{ getParameterAs<std::size_t>(context.parameters, "timeOffset").value_or(0) };
|
||||
bool estimateContentLength{ getParameterAs<bool>(context.parameters, "estimateContentLength").value_or(false) };
|
||||
|
||||
StreamParameters parameters;
|
||||
|
||||
std::size_t bitrate{};
|
||||
parameters.estimateContentLength = estimateContentLength;
|
||||
|
||||
{
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
const auto track{ Track::find(context.dbSession, id) };
|
||||
if (!track)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
parameters.inputParameters.trackPath = track->getPath();
|
||||
parameters.inputParameters.duration = track->getDuration();
|
||||
bitrate = track->getBitrate() / 1000;
|
||||
}
|
||||
|
||||
if (format == "raw") // raw => no transcode
|
||||
return parameters;
|
||||
|
||||
const auto audioFile{ Av::parseAudioFile(parameters.inputParameters.trackPath) };
|
||||
|
||||
// check if transcode is really needed or not
|
||||
// same format as requested, bitrate is lower than requested => no need to transcode
|
||||
if (const auto streamInfo{ audioFile->getBestStreamInfo() })
|
||||
{
|
||||
// assume reported codec is "mp3", "opus", "vorbis", etc.
|
||||
if (StringUtils::stringCaseInsensitiveEqual(streamInfo->codecName, format) && (maxBitRate == 0 || (bitrate != 0 && bitrate <= maxBitRate)))
|
||||
{
|
||||
LMS_LOG(API_SUBSONIC, DEBUG) << "stream parameters are compatible with actual file: no transcode";
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
const User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
const auto track{ Track::find(context.dbSession, id) };
|
||||
if (!track)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
parameters.inputParameters.trackPath = track->getPath();
|
||||
parameters.inputParameters.duration = track->getDuration();
|
||||
parameters.estimateContentLength = estimateContentLength;
|
||||
|
||||
if (format == "raw") // raw => no transcoding
|
||||
return parameters;
|
||||
|
||||
std::optional<Av::Transcoding::OutputFormat> requestedFormat{ subsonicStreamFormatToAvOutputFormat(format) };
|
||||
if (!requestedFormat)
|
||||
{
|
||||
if (user->getSubsonicEnableTranscodingByDefault())
|
||||
requestedFormat = userTranscodeFormatToAvFormat(user->getSubsonicDefaultTranscodingOutputFormat());
|
||||
}
|
||||
|
||||
if (!requestedFormat && (maxBitRate == 0 || track->getBitrate() <= maxBitRate ))
|
||||
{
|
||||
LMS_LOG(API_SUBSONIC, DEBUG) << "File's bitrate is compatible with parameters => no transcoding";
|
||||
return parameters; // no transcoding needed
|
||||
}
|
||||
|
||||
// scan the file to check if its format is compatible with the actual requested format
|
||||
// same codec => apply max bitrate
|
||||
// otherwise => apply default bitrate (because we can't really compare bitrates between formats) + max bitrate)
|
||||
std::size_t bitrate{};
|
||||
if (requestedFormat && isOutputFormatCompatible(track->getPath(), *requestedFormat))
|
||||
{
|
||||
if (maxBitRate == 0 || track->getBitrate() <= maxBitRate)
|
||||
{
|
||||
LMS_LOG(API_SUBSONIC, DEBUG) << "File's bitrate and format are compatible with parameters => no transcoding";
|
||||
return parameters; // no transcoding needed
|
||||
}
|
||||
bitrate = maxBitRate;
|
||||
}
|
||||
|
||||
if (!requestedFormat)
|
||||
requestedFormat = userTranscodeFormatToAvFormat(user->getSubsonicDefaultTranscodingOutputFormat());
|
||||
if (!bitrate)
|
||||
bitrate = std::min<std::size_t>(user->getSubsonicDefaultTranscodingOutputBitrate(), maxBitRate);
|
||||
|
||||
Av::Transcoding::OutputParameters& outputParameters{ parameters.outputParameters.emplace() };
|
||||
|
||||
outputParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.)
|
||||
outputParameters.offset = std::chrono::seconds{ timeOffset };
|
||||
|
||||
if (std::optional<Av::Transcoding::OutputFormat> requestedFormat{ subsonicStreamFormatToAvFormat(format) })
|
||||
outputParameters.format = *requestedFormat;
|
||||
else
|
||||
outputParameters.format = userTranscodeFormatToAvFormat(user->getSubsonicDefaultTranscodingOutputFormat());
|
||||
|
||||
outputParameters.bitrate = user->getSubsonicDefaultTranscodingOutputBitrate();
|
||||
if (maxBitRate != 0)
|
||||
outputParameters.bitrate = Utils::clamp(outputParameters.bitrate, std::size_t{ 48000 }, maxBitRate * 1000);
|
||||
outputParameters.format = *requestedFormat;
|
||||
outputParameters.bitrate = bitrate;
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
#include "LmsApplication.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
|
||||
namespace UserInterface
|
||||
namespace UserInterface
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
@@ -58,6 +58,7 @@ namespace UserInterface
|
||||
static inline const Field ReplayGainModeField{ "replaygain-mode" };
|
||||
static inline const Field ReplayGainPreAmpGainField{ "replaygain-preamp" };
|
||||
static inline const Field ReplayGainPreAmpGainIfNoInfoField{ "replaygain-preamp-no-rg-info" };
|
||||
static inline const Field SubsonicEnableTranscodingByDefault{ "subsonic-enable-transcoding-by-default" };
|
||||
static inline const Field SubsonicArtistListModeField{ "subsonic-artist-list-mode" };
|
||||
static inline const Field SubsonicTranscodingOutputFormatField{ "subsonic-transcoding-output-format" };
|
||||
static inline const Field SubsonicTranscodingOutputBitrateField{ "subsonic-transcoding-output-bitrate" };
|
||||
@@ -85,6 +86,7 @@ namespace UserInterface
|
||||
addField(ReplayGainModeField);
|
||||
addField(ReplayGainPreAmpGainField);
|
||||
addField(ReplayGainPreAmpGainIfNoInfoField);
|
||||
addField(SubsonicEnableTranscodingByDefault);
|
||||
addField(SubsonicTranscodingOutputBitrateField);
|
||||
addField(SubsonicTranscodingOutputFormatField);
|
||||
addField(FeedbackBackendField);
|
||||
@@ -162,6 +164,9 @@ namespace UserInterface
|
||||
}
|
||||
|
||||
{
|
||||
bool subsonicEnableTranscodingByDefault{ Wt::asNumber(value(SubsonicEnableTranscodingByDefault)) != 0 };
|
||||
user.modify()->setSubsonicEnableTranscodingByDefault(subsonicEnableTranscodingByDefault);
|
||||
|
||||
auto subsonicTranscodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromString(valueText(SubsonicTranscodingOutputBitrateField)) };
|
||||
if (subsonicTranscodingOutputBitrateRow)
|
||||
user.modify()->setSubsonicDefaultTranscodingOutputBitrate(_transcodingOutputBitrateModel->getValue(*subsonicTranscodingOutputBitrateRow));
|
||||
@@ -173,7 +178,6 @@ namespace UserInterface
|
||||
auto subsonicArtistListModeRow{ _subsonicArtistListModeModel->getRowFromString(valueText(SubsonicArtistListModeField)) };
|
||||
if (subsonicArtistListModeRow)
|
||||
user.modify()->setSubsonicArtistListMode(_subsonicArtistListModeModel->getValue(*subsonicArtistListModeRow));
|
||||
|
||||
}
|
||||
|
||||
{
|
||||
@@ -232,6 +236,8 @@ namespace UserInterface
|
||||
}
|
||||
|
||||
{
|
||||
setValue(SubsonicEnableTranscodingByDefault, user->getSubsonicEnableTranscodingByDefault());
|
||||
|
||||
auto subsonicTranscodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromValue(user->getSubsonicDefaultTranscodingOutputBitrate()) };
|
||||
if (subsonicTranscodingOutputBitrateRow)
|
||||
setValue(SubsonicTranscodingOutputBitrateField, _transcodingOutputBitrateModel->getString(*subsonicTranscodingOutputBitrateRow));
|
||||
@@ -356,7 +362,7 @@ namespace UserInterface
|
||||
::Auth::IPasswordService* _authPasswordService{};
|
||||
bool _withOldPassword{};
|
||||
|
||||
std::shared_ptr<TranscodingModeModel> _transcodingModeModeModel;
|
||||
std::shared_ptr<TranscodingModeModel> _transcodingModeModeModel;
|
||||
std::shared_ptr<ValueStringModel<Bitrate>> _transcodingOutputBitrateModel;
|
||||
std::shared_ptr<ValueStringModel<TranscodingOutputFormat>> _transcodingOutputFormatModel;
|
||||
std::shared_ptr<ReplayGainModeModel> _replayGainModeModel;
|
||||
@@ -490,6 +496,9 @@ namespace UserInterface
|
||||
{
|
||||
t->setCondition("if-has-subsonic-api", Service<IConfig>::get()->getBool("api-subsonic", true));
|
||||
|
||||
// Enable transcoding by default
|
||||
t->setFormWidget(SettingsModel::SubsonicEnableTranscodingByDefault, std::make_unique<Wt::WCheckBox>());
|
||||
|
||||
// Format
|
||||
auto transcodingOutputFormat{ std::make_unique<Wt::WComboBox>() };
|
||||
transcodingOutputFormat->setModel(model->getTranscodingOutputFormatModel());
|
||||
|
||||
Reference in New Issue
Block a user