From e9673e44e3440176b4fce9bf74af21323999cf14 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 10 Nov 2019 14:52:37 +0100 Subject: [PATCH 1/3] 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 --- approot/messages.xml | 1 + approot/messages_fr.xml | 1 + src/api/subsonic/SubsonicResource.cpp | 94 ++++++++++++++++++--------- src/av/AvTranscoder.cpp | 8 ++- src/av/AvTranscoder.hpp | 4 +- src/av/AvTypes.cpp | 21 +++--- src/av/AvTypes.hpp | 11 ++-- src/database/User.hpp | 1 + src/ui/SettingsView.cpp | 1 + src/ui/resource/AudioResource.cpp | 3 + 10 files changed, 96 insertions(+), 49 deletions(-) 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; From 7f5b6a969018a1908d11c59d1ab22cddfe7711f6 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 12 Nov 2019 22:07:32 +0100 Subject: [PATCH 2/3] Bumped version to 3.2.0 --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 82abff3d..9184ff02 100644 --- a/configure.ac +++ b/configure.ac @@ -1,5 +1,5 @@ AC_PREREQ(2.59) -AC_INIT(lms, 3.1.0, test@test) +AC_INIT(lms, 3.2.0, test@test) AM_INIT_AUTOMAKE AC_CONFIG_HEADER(src/config/config.h) From 3fec59d4040cae00d1c09810a1ddf16e4ae14e85 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 17 Nov 2019 20:51:23 +0100 Subject: [PATCH 3/3] Provide docker images. Fixes #3 --- README.md | 8 +- docker/Dockerfile | 230 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+), 4 deletions(-) create mode 100644 docker/Dockerfile diff --git a/README.md b/README.md index 2b2ddd29..6e07b808 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # LMS - Lightweight Music Server -[![Build Status](https://travis-ci.org/epoupon/lms.svg?branch=master)](https://travis-ci.org/epoupon/lms) +[![Build Status](https://travis-ci.org/epoupon/lms.svg?branch=master)](https://travis-ci.org/epoupon/lms) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/epoupon/lms) _LMS_ is a self-hosted music streaming software: access your music collection from anywhere using a web interface! @@ -51,10 +51,10 @@ __Note__: since _LMS_ stores hashed and salted passwords, it cannot handle the _ ## Installation -### From packages - -#### Debian Buster packages +### Docker +_Docker_ images are available, please see detailed instructions on https://hub.docker.com/r/epoupon/lms. +### Debian Buster packages _Buster_ packages are provided for _amd64_ and _armhf_ architectures. As root, trust the following debian package provider and add it in your list of repositories: diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..87ea4c43 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,230 @@ +FROM alpine:3.10 AS build + +WORKDIR /tmp/workdir + +ARG MAKEFLAGS="-j2" +ARG FFMPEG_VERSION=4.1.4 +ARG WT_VERSION=4.1.1 +ARG IMAGEMAGICK6_VERSION=6.9.10-71 +ARG PSTREAMS_VERSION=1.0.1 +ARG LMS_VERSION=3.2.0 + +ARG PREFIX="/tmp/install" + +ARG BUILD_PACKAGES=" \ + ca-certificates \ + curl \ + bzip2 \ + pkgconfig \ + coreutils \ + autoconf \ + automake \ + libtool \ + g++ \ + make \ + libjpeg-turbo-dev \ + openjpeg-dev \ + libpng-dev \ + tiff-dev \ + nasm \ + yasm \ + curl \ + libogg-dev \ + opus-dev \ + libvorbis-dev \ + lame-dev \ + cmake \ + zlib-dev \ + openssl-dev \ + boost-dev \ + libconfig-dev \ + taglib-dev" + +RUN apk add --no-cache --update ${BUILD_PACKAGES} + +## ffmpeg +RUN \ + DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \ + curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \ + tar -jx --strip-components=1 -f ffmpeg-${FFMPEG_VERSION}.tar.bz2 + +RUN \ + DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \ + ./configure \ + --prefix=${PREFIX} \ + --disable-autodetect \ + --disable-debug \ + --disable-doc \ + --disable-ffplay \ + --disable-ffprobe \ + --disable-openssl \ + --disable-postproc \ + --disable-pixelutils \ + --disable-network \ + --enable-shared \ + --disable-static \ + --enable-gpl \ + --enable-small \ + --enable-version3 \ + --enable-nonfree \ + --enable-libmp3lame \ + --enable-libopenjpeg \ + --enable-libopus \ + --enable-libvorbis \ + --disable-everything \ + --enable-decoder=aac*,ac3*,flac,mp3*,libopus,pcm*,libvorbis,wavpack,wma*,libopenjpg,png,tiff \ + --enable-encoder=libmp3lame,libopus,libvorbis \ + --enable-demuxer=aiff,asf,flac,ipod,ogg,matroska,mp3,mp4,wav,wv,webm \ + --enable-muxer=ogg,matroska,mp3,webm \ + --enable-protocol=file,pipe \ + --enable-filter=aresample \ + --extra-libs=-ldl && \ + make && \ + make install && \ + make distclean + +# WT +RUN \ + DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \ + curl -sLO https://github.com/emweb/wt/archive/${WT_VERSION}.tar.gz && \ + tar -x --strip-components=1 -f ${WT_VERSION}.tar.gz + +RUN \ + DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \ + cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} && \ + make && \ + make install + +# ImageMagick +RUN \ + DIR=/tmp/imagemagick && mkdir -p ${DIR} && cd ${DIR} && \ + curl -sLO https://github.com/ImageMagick/ImageMagick6/archive/${IMAGEMAGICK6_VERSION}.tar.gz && \ + tar -x --strip-components=1 -f ${IMAGEMAGICK6_VERSION}.tar.gz + +RUN \ + DIR=/tmp/imagemagick && mkdir -p ${DIR} && cd ${DIR} && \ + ./configure \ + --prefix=${PREFIX} \ + --enable-shared=yes \ + --disable-static \ + --disable-docs \ + --with-magick-plus-plus \ + --without-zstd \ + --without-dps \ + --without-autotrace \ + --without-dps \ + --without-fftw \ + --without-flif \ + --without-fpx \ + --without-djvu \ + --without-fontconfig \ + --without-freetype \ + --without-raqm \ + --without-gslib \ + --without-gvc \ + --without-heic \ + --without-jbig \ + --with-jpeg \ + --without-jxl \ + --without-lcms \ + --with-openjp2 \ + --without-lqr \ + --with-lzma \ + --without-openexr \ + --without-pango \ + --with-png \ + --without-raw \ + --without-rsvg \ + --with-tiff \ + --without-webp \ + --without-wmf \ + --without-xml && \ + make && \ + make install && \ + make distclean + +# libpstreams +RUN \ + DIR=/tmp/libpstreams && mkdir -p ${DIR} && cd ${DIR} && \ + curl -sLO https://sourceforge.net/projects/pstreams/files/pstreams/Release%201.0/pstreams-${PSTREAMS_VERSION}.tar.gz && \ + tar -x --strip-components=1 -f pstreams-${PSTREAMS_VERSION}.tar.gz && \ + prefix=${PREFIX} make install prefix=/ DESTDIR=${PREFIX} + +# LMS +RUN \ + DIR=/tmp/lms && mkdir -p ${DIR} && cd ${DIR} && \ + curl -sLO https://github.com/epoupon/lms/archive/v${LMS_VERSION}.tar.gz && \ + tar -x --strip-components=1 -f v${LMS_VERSION}.tar.gz + +RUN \ + DIR=/tmp/lms && mkdir -p ${DIR} && cd ${DIR} && \ + autoreconf -vfi && \ + PKG_CONFIG_PATH=/tmp/install/lib/pkgconfig CXXFLAGS="-O2 -I${PREFIX}/include" LDFLAGS="-L${PREFIX}/lib -Wl,--rpath-link=${PREFIX}/lib" ./configure --prefix=${PREFIX} && \ + make && \ + make install && \ + make distclean && \ + mkdir -p ${PREFIX}/etc/ && \ + cp conf/lms.conf ${PREFIX}/etc + +# Now copy all the stuff installed in a new folder (/tmp/fakeroot/) +RUN \ + mkdir -p /tmp/fakeroot/bin && \ + for bin in ${PREFIX}/bin/ffmpeg ${PREFIX}/bin/lms; \ + do \ + strip --strip-all $bin && \ + cp $bin /tmp/fakeroot/bin/; \ + done && \ + for lib in ${PREFIX}/lib/*.so; \ + do \ + strip --strip-all $lib; \ + done && \ + cp -r ${PREFIX}/lib /tmp/fakeroot/lib && \ + cp -r ${PREFIX}/share /tmp/fakeroot/share && \ + LD_LIBRARY_PATH=/tmp/fakeroot/lib /tmp/fakeroot/bin/ffmpeg -buildconf + +## Release Stage +FROM alpine:3.10 AS release +MAINTAINER Emeric Poupon + +ARG RUNTIME_PACKAGES=" \ + openssl \ + libjpeg-turbo \ + openjpeg \ + libpng \ + tiff \ + libogg \ + opus \ + libvorbis \ + lame \ + zlib \ + boost-filesystem \ + boost-program_options \ + boost-system \ + boost-thread \ + libconfig++ \ + taglib \ + libgomp" + +ARG LMS_USER=lms +ARG LMS_GROUP=lms + +RUN apk add --no-cache --update ${RUNTIME_PACKAGES} + +RUN addgroup -S ${LMS_GROUP} && \ + adduser -S -H ${LMS_USER} && \ + adduser ${LMS_USER} ${LMS_GROUP} && \ + mkdir -p /var/lms && chown -R ${LMS_USER}:${LMS_GROUP} /var/lms + +VOLUME /var/lms +VOLUME /music +VOLUME /usr/local/etc + +USER ${LMS_USER}:${LMS_GROUP} + +COPY --from=build /tmp/fakeroot/ /usr +COPY --from=build /tmp/fakeroot/share/lms/lms.conf /etc/lms.conf + +EXPOSE 5082 + +ENTRYPOINT ["/usr/bin/lms"] +