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:
emeric
2023-11-13 11:45:12 +01:00
parent 9e2040b449
commit 28a80b3c9d
11 changed files with 130 additions and 51 deletions
@@ -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 };
+1 -1
View File
@@ -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;
}
+12 -3
View File
@@ -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());