Subsonic API: added support for the format parameter in the stream request (only format=raw is handled, otherwise, it is ignored).
Added Matroksa/Opus output to ease compatibility with old devices. Fixes #8
This commit is contained in:
@@ -161,6 +161,7 @@
|
||||
<message id="Lms.Settings.transcoding-bitrate">Transcode bitrate</message>
|
||||
<message id="Lms.Settings.transcoding-format">Transcode format</message>
|
||||
<message id="Lms.Settings.transcoding-enable">Enable transcoding</message>
|
||||
<message id="Lms.Settings.transcoding.matroska_opus">Matroska/Opus</message>
|
||||
<message id="Lms.Settings.transcoding.mp3">MP3</message>
|
||||
<message id="Lms.Settings.transcoding.ogg_opus">Ogg/Opus</message>
|
||||
<message id="Lms.Settings.transcoding.ogg_vorbis">Ogg/Vorbis</message>
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
<message id="Lms.Settings.transcoding-bitrate">Bitrate du transcodage</message>
|
||||
<message id="Lms.Settings.transcoding-format">Format du transcodage</message>
|
||||
<message id="Lms.Settings.transcoding-enable">Activer le transcodage</message>
|
||||
<message id="Lms.Settings.transcoding.matroska_opus">Matroska/Opus</message>
|
||||
<message id="Lms.Settings.transcoding.mp3">MP3</message>
|
||||
<message id="Lms.Settings.transcoding.ogg_opus">Ogg/Opus</message>
|
||||
<message id="Lms.Settings.transcoding.ogg_vorbis">Ogg/Vorbis</message>
|
||||
|
||||
@@ -46,11 +46,6 @@
|
||||
using namespace Database;
|
||||
|
||||
static const std::string genreClusterName {"GENRE"};
|
||||
// Files are always reported to be in the same format
|
||||
static const std::size_t reportedBitrate {128};
|
||||
static const std::string reportedFileSuffix {"mp3"};
|
||||
static const Av::Encoding reportedEncoding {Av::Encoding::MP3};
|
||||
static const Av::Encoding transcodeEncoding {Av::Encoding::MP3};
|
||||
static const std::string reportedStarredDate {"2000-01-01T00:00:00"};
|
||||
|
||||
template<>
|
||||
@@ -362,7 +357,9 @@ getTrackPath(const Track::pointer& track)
|
||||
{
|
||||
std::string path;
|
||||
|
||||
auto release {track->getRelease()};
|
||||
// The track path has to be relative from the root
|
||||
|
||||
const auto release {track->getRelease()};
|
||||
if (release)
|
||||
{
|
||||
auto artists {release->getReleaseArtists()};
|
||||
@@ -382,7 +379,12 @@ getTrackPath(const Track::pointer& track)
|
||||
if (track->getTrackNumber())
|
||||
path += std::to_string(*track->getTrackNumber()) + "-";
|
||||
|
||||
return path + makeNameFilesystemCompatible(track->getName()) + "." + reportedFileSuffix;
|
||||
path += makeNameFilesystemCompatible(track->getName());
|
||||
|
||||
if (track->getPath().has_extension())
|
||||
path += track->getPath().extension();
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
static
|
||||
@@ -400,7 +402,20 @@ trackToResponseNode(const Track::pointer& track, Session& dbSession, const User:
|
||||
trackResponse.setAttribute("discNumber", std::to_string(*track->getDiscNumber()));
|
||||
if (track->getYear())
|
||||
trackResponse.setAttribute("year", std::to_string(*track->getYear()));
|
||||
trackResponse.setAttribute("size", std::to_string(reportedBitrate * 1000 / 8 * std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count()));
|
||||
|
||||
trackResponse.setAttribute("path", getTrackPath(track));
|
||||
{
|
||||
std::error_code ec;
|
||||
const auto fileSize {std::filesystem::file_size(track->getPath(), ec)};
|
||||
if (!ec)
|
||||
trackResponse.setAttribute("size", std::to_string(fileSize));
|
||||
}
|
||||
|
||||
if (track->getPath().has_extension())
|
||||
{
|
||||
auto extension {track->getPath().extension()};
|
||||
trackResponse.setAttribute("suffix", extension.string().substr(1));
|
||||
}
|
||||
|
||||
trackResponse.setAttribute("coverArt", IdToString({Id::Type::Track, track.id()}));
|
||||
|
||||
@@ -420,11 +435,7 @@ trackToResponseNode(const Track::pointer& track, Session& dbSession, const User:
|
||||
trackResponse.setAttribute("parent", IdToString({Id::Type::Release, track->getRelease().id()}));
|
||||
}
|
||||
|
||||
trackResponse.setAttribute("path", getTrackPath(track));
|
||||
trackResponse.setAttribute("bitRate", std::to_string(reportedBitrate));
|
||||
trackResponse.setAttribute("duration", std::to_string(std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count()));
|
||||
trackResponse.setAttribute("suffix", reportedFileSuffix);
|
||||
trackResponse.setAttribute("contentType", Av::encodingToMimetype(reportedEncoding));
|
||||
trackResponse.setAttribute("type", "music");
|
||||
|
||||
if (user->hasStarredTrack(track))
|
||||
@@ -1686,6 +1697,21 @@ handleUpdatePlaylistRequest(RequestContext& context)
|
||||
return Response::createOkResponse();
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -1695,34 +1721,44 @@ createTranscoder(RequestContext& context)
|
||||
|
||||
// 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()};
|
||||
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError {};
|
||||
{
|
||||
auto track {Track::getById(context.dbSession, id.value)};
|
||||
if (!track)
|
||||
throw RequestedDataNotFoundError {};
|
||||
|
||||
// "If set to zero, no limit is imposed"
|
||||
if (!maxBitRate || *maxBitRate == 0)
|
||||
maxBitRate = user->getAudioTranscodeBitrate() / 1000;
|
||||
trackPath = track->getPath();
|
||||
}
|
||||
|
||||
*maxBitRate = clamp(*maxBitRate, std::size_t {48}, user->getMaxAudioTranscodeBitrate() / 1000);
|
||||
{
|
||||
const User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError {};
|
||||
|
||||
auto track {Track::getById(context.dbSession, id.value)};
|
||||
if (!track)
|
||||
throw RequestedDataNotFoundError {};
|
||||
// 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;
|
||||
|
||||
trackPath = track->getPath();
|
||||
*maxBitRate = clamp(*maxBitRate, std::size_t {48}, user->getMaxAudioTranscodeBitrate() / 1000);
|
||||
|
||||
parameters.bitrate = *maxBitRate * 1000;
|
||||
parameters.encoding = userTranscodeFormatToAvEncoding(user->getAudioTranscodeFormat());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Av::TranscodeParameters parameters {};
|
||||
|
||||
parameters.stripMetadata = false; // Since it can be cached and some players read the metadata from the downloaded file
|
||||
parameters.bitrate = *maxBitRate * 1000;
|
||||
parameters.encoding = transcodeEncoding;
|
||||
|
||||
return std::make_shared<Av::Transcoder>(trackPath, parameters);
|
||||
}
|
||||
|
||||
|
||||
@@ -120,6 +120,13 @@ Transcoder::start()
|
||||
args.emplace_back("ogg");
|
||||
break;
|
||||
|
||||
case Encoding::MATROSKA_OPUS:
|
||||
args.emplace_back("-acodec");
|
||||
args.emplace_back("libopus");
|
||||
args.emplace_back("-f");
|
||||
args.emplace_back("matroska");
|
||||
break;
|
||||
|
||||
case Encoding::OGG_VORBIS:
|
||||
args.emplace_back("-acodec");
|
||||
args.emplace_back("libvorbis");
|
||||
@@ -134,7 +141,6 @@ Transcoder::start()
|
||||
args.emplace_back("webm");
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -33,10 +33,10 @@ namespace Av {
|
||||
|
||||
struct TranscodeParameters
|
||||
{
|
||||
std::optional<Encoding> encoding; // If not set, no transcoding is performed
|
||||
std::optional<Encoding> encoding; // If not set, no transcoding is performed
|
||||
std::size_t bitrate {128000};
|
||||
std::optional<std::size_t> stream; // Id of the stream to be transcoded (auto detect by default)
|
||||
std::optional<std::chrono::seconds> offset {};
|
||||
std::optional<std::chrono::seconds> offset;
|
||||
bool stripMetadata {true};
|
||||
};
|
||||
|
||||
|
||||
+9
-12
@@ -23,21 +23,18 @@
|
||||
|
||||
namespace Av {
|
||||
|
||||
std::string encodingToMimetype(Encoding encoding)
|
||||
const char* encodingToMimetype(Encoding encoding)
|
||||
{
|
||||
static const std::map<Encoding, std::string> encodings
|
||||
switch (encoding)
|
||||
{
|
||||
{Encoding::MP3, "audio/mpeg"},
|
||||
{Encoding::OGG_VORBIS, "audio/ogg"},
|
||||
{Encoding::OGG_OPUS, "audio/opus"},
|
||||
{Encoding::WEBM_VORBIS, "audio/webm"},
|
||||
};
|
||||
case Encoding::MP3: return "audio/mpeg";
|
||||
case Encoding::OGG_OPUS: return "audio/opus";
|
||||
case Encoding::MATROSKA_OPUS: return "audio/x-matroska";
|
||||
case Encoding::OGG_VORBIS: return "audio/ogg";
|
||||
case Encoding::WEBM_VORBIS: return "audio/webm";
|
||||
}
|
||||
|
||||
auto it {encodings.find(encoding)};
|
||||
if (it == encodings.end())
|
||||
throw AvException("Invalid encoding");
|
||||
|
||||
return it->second;
|
||||
throw AvException("Invalid encoding");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-5
@@ -34,13 +34,14 @@ class AvException : public LmsException
|
||||
enum class Encoding
|
||||
{
|
||||
// Values are important and must not be changed
|
||||
MP3 = 0,
|
||||
OGG_OPUS = 1,
|
||||
OGG_VORBIS = 2,
|
||||
WEBM_VORBIS = 3,
|
||||
MP3,
|
||||
OGG_OPUS,
|
||||
MATROSKA_OPUS,
|
||||
OGG_VORBIS,
|
||||
WEBM_VORBIS,
|
||||
};
|
||||
|
||||
std::string encodingToMimetype(Encoding encoding);
|
||||
const char* encodingToMimetype(Encoding encoding);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ enum class AudioFormat
|
||||
OGG_OPUS = 2,
|
||||
OGG_VORBIS = 3,
|
||||
WEBM_VORBIS = 4,
|
||||
MATROSKA_OPUS = 5,
|
||||
};
|
||||
|
||||
using Bitrate = std::size_t;
|
||||
|
||||
@@ -212,6 +212,7 @@ class SettingsModel : public Wt::WFormModel
|
||||
_transcodeFormatModel = std::make_shared<ValueStringModel<AudioFormat>>();
|
||||
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding.mp3"), AudioFormat::MP3);
|
||||
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding.ogg_opus"), AudioFormat::OGG_OPUS);
|
||||
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding.matroska_opus"), AudioFormat::MATROSKA_OPUS);
|
||||
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding.ogg_vorbis"), AudioFormat::OGG_VORBIS);
|
||||
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding.webm_vorbis"), AudioFormat::WEBM_VORBIS);
|
||||
}
|
||||
|
||||
@@ -105,6 +105,9 @@ AudioResource::handleRequest(const Wt::Http::Request& request,
|
||||
case Database::AudioFormat::OGG_OPUS:
|
||||
parameters.encoding = Av::Encoding::OGG_OPUS;
|
||||
break;
|
||||
case Database::AudioFormat::MATROSKA_OPUS:
|
||||
parameters.encoding = Av::Encoding::MATROSKA_OPUS;
|
||||
break;
|
||||
case Database::AudioFormat::OGG_VORBIS:
|
||||
parameters.encoding = Av::Encoding::OGG_VORBIS;
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user