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
-[](https://travis-ci.org/epoupon/lms)
+[](https://travis-ci.org/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/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/configure.ac b/configure.ac
index 14b7db36..e429eec6 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)
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"]
+
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;