Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "Lightweight Music Server" application + service #147408

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,7 @@
./services/misc/lidarr.nix
./services/misc/libreddit.nix
./services/misc/lifecycled.nix
./services/misc/lms.nix
./services/misc/mame.nix
./services/misc/matrix-appservice-discord.nix
./services/misc/matrix-appservice-irc.nix
Expand Down
164 changes: 164 additions & 0 deletions nixos/modules/services/misc/lms.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
{ config, lib, pkgs, ... }:

with lib;

let
cfg = config.services.lms;
settingsFormat = pkgs.formats.toml {};
in
{
options.services.lms = {
enable = mkEnableOption "LMS (Lightweight Music Server). A self-hosted, music streaming server";

stateDir = mkOption {
type = types.path;
default = "/var/lib/lms";
description = ''
The directory where lms uses as a working directory to store its state.
If left as the default value this directory will automatically be
created before the lms server starts, otherwise the sysadmin is
responsible for ensuring the directory exists with appropriate ownership
and permissions.
'';
};

virtualHost = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Name of the nginx virtualhost to setup as reverse proxy. If null, do
not setup any virtualhost.
'';
};

user = mkOption {
type = types.str;
default = "lms";
description = ''
User account under which LMS runs. If left as the default value this
user will automatically be created on system activation, otherwise the
sysadmin is responsible for ensuring the user exists before the lms
service starts.
'';
};

settings = mkOption {
type = types.submodule {
freeformType = settingsFormat.type;
options = {
listen-addr = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
The address on which to bind LMS.
'';
};

listen-port = mkOption {
type = types.port;
default = 5082;
description = ''
The port on which LMS will listen to.
'';
};

api-subsonic = mkOption {
type = types.bool;
default = true;
description = ''
Enable the Subsonic API.
'';
};
};
};
default = {};
description = ''
Configuration for LMS, see
<link xlink:href="https://github.com/epoupon/lms/blob/master/conf/lms.conf"/>
for full list of supported values.
'';
};
};

config = mkIf cfg.enable {
services.lms.settings = {
working-dir = cfg.stateDir;
behind-reverse-proxy = cfg.virtualHost != null;
docroot = "${pkgs.lms}/share/lms/docroot/;/resources,/css,/images,/js,/favicon.ico";
approot = "${pkgs.lms}/share/lms/approot";
cover-max-file-size = mkDefault 10;
cover-max-cache-size = mkDefault 30;
};

systemd.services.lms = {
description = "Lightweight Music Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];

serviceConfig = mkMerge [
{
ExecStart =
let lmsConfig = settingsFormat.generate "lms.conf" cfg.settings;
in "${pkgs.lms}/bin/lms ${lmsConfig}";
Restart = "on-failure";
RestartSec = 5;
User = cfg.user;
Group = "lms";
WorkingDirectory = cfg.stateDir;
UMask = "0022";

NoNewPrivileges = true;
ProtectSystem = true;
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
}
(mkIf (cfg.stateDir == "/var/lib/lms") {
StateDirectory = "/var/lib/lms";
})
];
};

# from: https://github.com/epoupon/lms#reverse-proxy-settings
services.nginx = mkIf (cfg.virtualHost != null) {
enable = true;
virtualHosts.${cfg.virtualHost} = {
locations."/" = {
proxyPass = "http://${cfg.settings.listen-addr}:${toString cfg.settings.listen-port}";
extraConfig = ''
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120;
'';
};
extraConfig = ''
proxy_request_buffering off;
proxy_buffering off;
proxy_buffer_size 4k;
proxy_read_timeout 10m;
proxy_send_timeout 10m;
keepalive_timeout 10m;
'';
};
};

users.users = mkIf (cfg.user == "lms") {
"${cfg.user}" = {
isSystemUser = true;
description = "LMS service user";
name = cfg.user;
group = "lms";
home = cfg.stateDir;
};
};

users.groups.lms = {};
};
}
96 changes: 96 additions & 0 deletions pkgs/servers/misc/lms/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
{ stdenv
, lib
, fetchFromGitHub
, writeText
, cmake
, pkg-config
, gtest
, boost
, zlib
, ffmpeg_4
, graphicsmagick
, libconfig
, taglib
, wt, useMinimalWt ? false
}:

let
ffmpeg = ffmpeg_4;

# Wt with the minimum set of optional dependencies required for LMS
wt-minimal = wt.override {
# Docs not needed
doxygen = null;
# Graphics libraries not needed
glew = null; libharu = null; pango = null; graphicsmagick = null;
# LMS uses only sqlite
firebird = null; libmysqlclient = null; postgresql = null;
# Qt not used
qt48Full = null;
Comment on lines +22 to +29
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really hacky and ? null should be avoided as much as possible. Can we do this different?

Copy link
Contributor Author

@arthurl arthurl Nov 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what better way there is. 1) The wt derivation (in development/libraries/wt/default.nix) does not provide a nicer mechanism to remove components and 2) I'm don't think that the wt derivation should be expanded, given that wt simply does not build the optional components if it doesn't find the required dependencies, and that cutting out optional dependencies is likely only useful on a system that isn't served by a binary cache (e.g. armv7l, on which those dependencies are broken half the time). Having the caller pass in null seems like a robust and minimal way to include or drop those optional portions.

Or, are you against providing the "useMinimalWt" option at all? I'm happy to remove that and keep it in my overlays; I included it because I thought others might find it useful.

};

wt-lms = if useMinimalWt then wt-minimal else wt;
in

stdenv.mkDerivation rec {
pname = "lms";
version = "3.27.0";

src = fetchFromGitHub {
owner = "epoupon";
repo = pname;
rev = "v${version}";
sha256 = "0rkky9c1yiqlgs9amrcnqsxdcwc7s7qghc1s1m0mbannsr1rb8rn";
};

nativeBuildInputs = [ cmake pkg-config gtest ];
buildInputs = [
boost.dev zlib.dev ffmpeg.dev graphicsmagick libconfig taglib wt-lms
];

patches = [
(writeText "hardcode-dependency-paths.patch" ''
diff --git a/src/libs/av/impl/Transcoder.cpp b/src/libs/av/impl/Transcoder.cpp
index 7efa044..16a6e4c 100644
--- a/src/libs/av/impl/Transcoder.cpp
+++ b/src/libs/av/impl/Transcoder.cpp
@@ -38,7 +38,7 @@ static std::filesystem::path ffmpegPath;
void
Transcoder::init()
{
- ffmpegPath = Service<IConfig>::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
+ ffmpegPath = Service<IConfig>::get()->getPath("ffmpeg-file", "${ffmpeg.bin}/bin/ffmpeg");
if (!std::filesystem::exists(ffmpegPath))
throw Exception {"File '" + ffmpegPath.string() + "' does not exist!"};
}
Comment on lines +55 to +65
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to https://github.com/epoupon/lms/blob/835e705e09176797ce9f910640ac091c80b5f717/conf/lms.conf#L8 we could do this in the module instead:

services.lms.settings.ffmpeg-file = "${pkgs.ffmpeg_4.bin}/bin/ffmpeg";

Or is the issue that you want to be able to use this software without a configuration file?

diff --git a/src/lms/main.cpp b/src/lms/main.cpp
index 98c0cec..b60e26a 100644
--- a/src/lms/main.cpp
+++ b/src/lms/main.cpp
@@ -53,7 +53,7 @@ generateWtConfig(std::string execPath)
const std::filesystem::path wtConfigPath {Service<IConfig>::get()->getPath("working-dir") / "wt_config.xml"};
const std::filesystem::path wtLogFilePath {Service<IConfig>::get()->getPath("log-file", "/var/log/lms.log")};
Comment on lines +51 to +72
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please write that into a file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not possible, because I need a reference to the ffmpeg and wt paths (which is the whole point of the patch).

Or am I missing something..?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to add: the author of lms did not provide for a command line option to set those paths, so the usual solution of writing a wrapper shell script won't work here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be cleaner to write the diff into a file with placeholders for the parts you need to modify and then use sed or something to fix them up at configure time. I think that's what Sandro meant.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's possible, but seems way uglier than simply inlining the patch?

const std::filesystem::path wtAccessLogFilePath {Service<IConfig>::get()->getPath("access-log-file", "/var/log/lms.access.log")};
- const std::filesystem::path wtResourcesPath {Service<IConfig>::get()->getPath("wt-resources", "/usr/share/Wt/resources")};
+ const std::filesystem::path wtResourcesPath {Service<IConfig>::get()->getPath("wt-resources", "${wt-lms}/share/Wt/resources")};
Comment on lines +74 to +75
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is everyone opposed to:

postPatch = ''
  substituteInPlace src/lms/main.cpp \
    --replace "/usr/share/Wt/resources" "${wt-lms}/share/Wt/resources" \
    --replace "/usr/bin/ffmpeg" "${ffmpeg_4.bin}/bin/ffmpeg"
'';

?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess there is a bit of a worry that it might accidentally match other lines than the ones in this patch? But then again those paths would never be correct on NixOS you have a fair point there.

Then again less patching is better so if we can get away with only a config tweak I'd just leave it entirely unpatched. Seems like the config file has an entry for the wt-resources dir too.

const unsigned long configHttpServerThreadCount {Service<IConfig>::get()->getULong("http-server-thread-count", 0)};

args.push_back(execPath);
'')
];

meta = with lib; {
description = "Lightweight / low memory music streaming server";
longDescription = ''
LMS is a self-hosted music streaming software: access your music
collection from anywhere using a web interface or the subsonic API!
Written in C++, it has a low memory footprint and even comes with a
recommendation engine.
'';
homepage = https://github.com/epoupon/lms;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

downloadPage = "https://github.com/epoupon/lms";
maintainers = with maintainers; [ arthur ];
license = licenses.gpl3;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gpl3 was deprecated a little while ago: please choose gpl3Plus or gpl3Only.

platforms = platforms.all;
};
}
2 changes: 2 additions & 0 deletions pkgs/top-level/all-packages.nix
Original file line number Diff line number Diff line change
Expand Up @@ -20948,6 +20948,8 @@ with pkgs;

livepeer = callPackage ../servers/livepeer { };

lms = callPackage ../servers/misc/lms { };

lwan = callPackage ../servers/http/lwan { };

labelImg = callPackage ../applications/science/machine-learning/labelimg { };
Expand Down