diff --git a/approot/messages.xml b/approot/messages.xml index 3b9ef2f2..0a3eb8e2 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -161,6 +161,7 @@ Transcode bitrate Transcode format Enable transcoding +Matroska/Opus MP3 Ogg/Opus Ogg/Vorbis diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 9f8ceb58..4bb46d9f 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -161,6 +161,7 @@ Bitrate du transcodage Format du transcodage Activer le transcodage +Matroska/Opus MP3 Ogg/Opus Ogg/Vorbis diff --git a/src/api/subsonic/SubsonicResource.cpp b/src/api/subsonic/SubsonicResource.cpp index ee385f80..52b2dbc2 100644 --- a/src/api/subsonic/SubsonicResource.cpp +++ b/src/api/subsonic/SubsonicResource.cpp @@ -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(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(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 createTranscoder(RequestContext& context) @@ -1695,34 +1721,44 @@ createTranscoder(RequestContext& context) // Optional params std::optional maxBitRate {getParameterAs(context.parameters, "maxBitRate")}; + std::optional format {getParameterAs(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(trackPath, parameters); } diff --git a/src/av/AvTranscoder.cpp b/src/av/AvTranscoder.cpp index 19eea335..fd13e053 100644 --- a/src/av/AvTranscoder.cpp +++ b/src/av/AvTranscoder.cpp @@ -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; } diff --git a/src/av/AvTranscoder.hpp b/src/av/AvTranscoder.hpp index d50044be..efe02f9b 100644 --- a/src/av/AvTranscoder.hpp +++ b/src/av/AvTranscoder.hpp @@ -33,10 +33,10 @@ namespace Av { struct TranscodeParameters { - std::optional encoding; // If not set, no transcoding is performed + std::optional encoding; // If not set, no transcoding is performed std::size_t bitrate {128000}; std::optional stream; // Id of the stream to be transcoded (auto detect by default) - std::optional offset {}; + std::optional offset; bool stripMetadata {true}; }; diff --git a/src/av/AvTypes.cpp b/src/av/AvTypes.cpp index faf099bd..9ad5bd5d 100644 --- a/src/av/AvTypes.cpp +++ b/src/av/AvTypes.cpp @@ -23,21 +23,18 @@ namespace Av { -std::string encodingToMimetype(Encoding encoding) +const char* encodingToMimetype(Encoding encoding) { - static const std::map 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"); } } diff --git a/src/av/AvTypes.hpp b/src/av/AvTypes.hpp index 3d79eedd..3eca84d3 100644 --- a/src/av/AvTypes.hpp +++ b/src/av/AvTypes.hpp @@ -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); } diff --git a/src/database/User.hpp b/src/database/User.hpp index 9db125dd..2d250edc 100644 --- a/src/database/User.hpp +++ b/src/database/User.hpp @@ -44,6 +44,7 @@ enum class AudioFormat OGG_OPUS = 2, OGG_VORBIS = 3, WEBM_VORBIS = 4, + MATROSKA_OPUS = 5, }; using Bitrate = std::size_t; diff --git a/src/ui/SettingsView.cpp b/src/ui/SettingsView.cpp index 4084bfc9..3b983866 100644 --- a/src/ui/SettingsView.cpp +++ b/src/ui/SettingsView.cpp @@ -212,6 +212,7 @@ class SettingsModel : public Wt::WFormModel _transcodeFormatModel = std::make_shared>(); _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); } diff --git a/src/ui/resource/AudioResource.cpp b/src/ui/resource/AudioResource.cpp index 92537e90..520ce7ed 100644 --- a/src/ui/resource/AudioResource.cpp +++ b/src/ui/resource/AudioResource.cpp @@ -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;