Merge branch 'develop' into features-training

This commit is contained in:
emeric
2019-11-18 13:11:06 +01:00
13 changed files with 331 additions and 54 deletions
+4 -4
View File
@@ -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:
+1
View File
@@ -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>
+1
View File
@@ -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>
+1 -1
View File
@@ -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)
+230
View File
@@ -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 <itmfr@yahoo.fr>
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"]
+65 -29
View File
@@ -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);
}
+7 -1
View File
@@ -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;
}
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
+1
View File
@@ -44,6 +44,7 @@ enum class AudioFormat
OGG_OPUS = 2,
OGG_VORBIS = 3,
WEBM_VORBIS = 4,
MATROSKA_OPUS = 5,
};
using Bitrate = std::size_t;
+1
View File
@@ -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);
}
+3
View File
@@ -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;