uboot: (firmwareOdroidC2/C4) don't invoke patch tool, use patches = [] instead

https://github.com/NixOS/nixpkgs/blob/master/pkgs/stdenv/generic/setup.sh#L948
this can do it nicely.

Signed-off-by: Anton Arapov <anton@deadbeef.mx>
This commit is contained in:
Anton Arapov 2021-04-03 12:58:10 +02:00 committed by Alan Daniels
commit 56de2bcd43
30691 changed files with 3076956 additions and 0 deletions

View file

@ -0,0 +1,133 @@
# ALSA sound support.
{ config, lib, pkgs, ... }:
with lib;
let
inherit (pkgs) alsa-utils;
pulseaudioEnabled = config.hardware.pulseaudio.enable;
in
{
imports = [
(mkRenamedOptionModule [ "sound" "enableMediaKeys" ] [ "sound" "mediaKeys" "enable" ])
];
###### interface
options = {
sound = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable ALSA sound.
'';
};
enableOSSEmulation = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable ALSA OSS emulation (with certain cards sound mixing may not work!).
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
example = ''
defaults.pcm.!card 3
'';
description = ''
Set addition configuration for system-wide alsa.
'';
};
mediaKeys = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable volume and capture control with keyboard media keys.
You want to leave this disabled if you run a desktop environment
like KDE, Gnome, Xfce, etc, as those handle such things themselves.
You might want to enable this if you run a minimalistic desktop
environment or work from bare linux ttys/framebuffers.
Enabling this will turn on <option>services.actkbd</option>.
'';
};
volumeStep = mkOption {
type = types.str;
default = "1";
example = "1%";
description = ''
The value by which to increment/decrement volume on media keys.
See amixer(1) for allowed values.
'';
};
};
};
};
###### implementation
config = mkIf config.sound.enable {
environment.systemPackages = [ alsa-utils ];
environment.etc = mkIf (!pulseaudioEnabled && config.sound.extraConfig != "")
{ "asound.conf".text = config.sound.extraConfig; };
# ALSA provides a udev rule for restoring volume settings.
services.udev.packages = [ alsa-utils ];
boot.kernelModules = optional config.sound.enableOSSEmulation "snd_pcm_oss";
systemd.services.alsa-store =
{ description = "Store Sound Card State";
wantedBy = [ "multi-user.target" ];
unitConfig.RequiresMountsFor = "/var/lib/alsa";
unitConfig.ConditionVirtualization = "!systemd-nspawn";
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${pkgs.coreutils}/bin/mkdir -p /var/lib/alsa";
ExecStop = "${alsa-utils}/sbin/alsactl store --ignore";
};
};
services.actkbd = mkIf config.sound.mediaKeys.enable {
enable = true;
bindings = [
# "Mute" media key
{ keys = [ 113 ]; events = [ "key" ]; command = "${alsa-utils}/bin/amixer -q set Master toggle"; }
# "Lower Volume" media key
{ keys = [ 114 ]; events = [ "key" "rep" ]; command = "${alsa-utils}/bin/amixer -q set Master ${config.sound.mediaKeys.volumeStep}- unmute"; }
# "Raise Volume" media key
{ keys = [ 115 ]; events = [ "key" "rep" ]; command = "${alsa-utils}/bin/amixer -q set Master ${config.sound.mediaKeys.volumeStep}+ unmute"; }
# "Mic Mute" media key
{ keys = [ 190 ]; events = [ "key" ]; command = "${alsa-utils}/bin/amixer -q set Capture toggle"; }
];
};
};
}

View file

@ -0,0 +1,115 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.botamusique;
format = pkgs.formats.ini {};
configFile = format.generate "botamusique.ini" cfg.settings;
in
{
meta.maintainers = with lib.maintainers; [ hexa ];
options.services.botamusique = {
enable = mkEnableOption "botamusique, a bot to play audio streams on mumble";
package = mkOption {
type = types.package;
default = pkgs.botamusique;
defaultText = literalExpression "pkgs.botamusique";
description = "The botamusique package to use.";
};
settings = mkOption {
type = with types; submodule {
freeformType = format.type;
options = {
server.host = mkOption {
type = types.str;
default = "localhost";
example = "mumble.example.com";
description = "Hostname of the mumble server to connect to.";
};
server.port = mkOption {
type = types.port;
default = 64738;
description = "Port of the mumble server to connect to.";
};
bot.username = mkOption {
type = types.str;
default = "botamusique";
description = "Name the bot should appear with.";
};
bot.comment = mkOption {
type = types.str;
default = "Hi, I'm here to play radio, local music or youtube/soundcloud music. Have fun!";
description = "Comment displayed for the bot.";
};
};
};
default = {};
description = ''
Your <filename>configuration.ini</filename> as a Nix attribute set. Look up
possible options in the <link xlink:href="https://github.com/azlux/botamusique/blob/master/configuration.example.ini">configuration.example.ini</link>.
'';
};
};
config = mkIf cfg.enable {
systemd.services.botamusique = {
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
unitConfig.Documentation = "https://github.com/azlux/botamusique/wiki";
environment.HOME = "/var/lib/botamusique";
serviceConfig = {
ExecStart = "${cfg.package}/bin/botamusique --config ${configFile}";
Restart = "always"; # the bot exits when the server connection is lost
# Hardening
CapabilityBoundingSet = [ "" ];
DynamicUser = true;
IPAddressDeny = [
"link-local"
"multicast"
];
LockPersonality = true;
MemoryDenyWriteExecute = true;
ProcSubset = "pid";
PrivateDevices = true;
PrivateUsers = true;
PrivateTmp = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
StateDirectory = "botamusique";
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
"~@resources"
];
UMask = "0077";
WorkingDirectory = "/var/lib/botamusique";
};
};
};
}

View file

@ -0,0 +1,142 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.hqplayerd;
pkg = pkgs.hqplayerd;
# XXX: This is hard-coded in the distributed binary, don't try to change it.
stateDir = "/var/lib/hqplayer";
configDir = "/etc/hqplayer";
in
{
options = {
services.hqplayerd = {
enable = mkEnableOption "HQPlayer Embedded";
auth = {
username = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Username used for HQPlayer's WebUI.
Without this you will need to manually create the credentials after
first start by going to http://your.ip/8088/auth
'';
};
password = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Password used for HQPlayer's WebUI.
Without this you will need to manually create the credentials after
first start by going to http://your.ip/8088/auth
'';
};
};
licenseFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to the HQPlayer license key file.
Without this, the service will run in trial mode and restart every 30
minutes.
'';
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Opens ports needed for the WebUI and controller API.
'';
};
config = mkOption {
type = types.nullOr types.lines;
default = null;
description = ''
HQplayer daemon configuration, written to /etc/hqplayer/hqplayerd.xml.
Refer to share/doc/hqplayerd/readme.txt in the hqplayerd derivation for possible values.
'';
};
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = (cfg.auth.username != null -> cfg.auth.password != null)
&& (cfg.auth.password != null -> cfg.auth.username != null);
message = "You must set either both services.hqplayer.auth.username and password, or neither.";
}
];
environment = {
etc = {
"hqplayer/hqplayerd.xml" = mkIf (cfg.config != null) { source = pkgs.writeText "hqplayerd.xml" cfg.config; };
"hqplayer/hqplayerd4-key.xml" = mkIf (cfg.licenseFile != null) { source = cfg.licenseFile; };
"modules-load.d/taudio2.conf".source = "${pkg}/etc/modules-load.d/taudio2.conf";
};
systemPackages = [ pkg ];
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ 8088 4321 ];
};
services.udev.packages = [ pkg ];
systemd = {
tmpfiles.rules = [
"d ${configDir} 0755 hqplayer hqplayer - -"
"d ${stateDir} 0755 hqplayer hqplayer - -"
"d ${stateDir}/home 0755 hqplayer hqplayer - -"
];
packages = [ pkg ];
services.hqplayerd = {
wantedBy = [ "multi-user.target" ];
after = [ "systemd-tmpfiles-setup.service" ];
environment.HOME = "${stateDir}/home";
unitConfig.ConditionPathExists = [ configDir stateDir ];
restartTriggers = optionals (cfg.config != null) [ config.environment.etc."hqplayer/hqplayerd.xml".source ];
preStart = ''
cp -r "${pkg}/var/lib/hqplayer/web" "${stateDir}"
chmod -R u+wX "${stateDir}/web"
if [ ! -f "${configDir}/hqplayerd.xml" ]; then
echo "creating initial config file"
install -m 0644 "${pkg}/etc/hqplayer/hqplayerd.xml" "${configDir}/hqplayerd.xml"
fi
'' + optionalString (cfg.auth.username != null && cfg.auth.password != null) ''
${pkg}/bin/hqplayerd -s ${cfg.auth.username} ${cfg.auth.password}
'';
};
};
users.groups = {
hqplayer.gid = config.ids.gids.hqplayer;
};
users.users = {
hqplayer = {
description = "hqplayer daemon user";
extraGroups = [ "audio" ];
group = "hqplayer";
uid = config.ids.uids.hqplayer;
};
};
};
}

View file

@ -0,0 +1,131 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.icecast;
configFile = pkgs.writeText "icecast.xml" ''
<icecast>
<hostname>${cfg.hostname}</hostname>
<authentication>
<admin-user>${cfg.admin.user}</admin-user>
<admin-password>${cfg.admin.password}</admin-password>
</authentication>
<paths>
<logdir>${cfg.logDir}</logdir>
<adminroot>${pkgs.icecast}/share/icecast/admin</adminroot>
<webroot>${pkgs.icecast}/share/icecast/web</webroot>
<alias source="/" dest="/status.xsl"/>
</paths>
<listen-socket>
<port>${toString cfg.listen.port}</port>
<bind-address>${cfg.listen.address}</bind-address>
</listen-socket>
<security>
<chroot>0</chroot>
<changeowner>
<user>${cfg.user}</user>
<group>${cfg.group}</group>
</changeowner>
</security>
${cfg.extraConf}
</icecast>
'';
in {
###### interface
options = {
services.icecast = {
enable = mkEnableOption "Icecast server";
hostname = mkOption {
type = types.nullOr types.str;
description = "DNS name or IP address that will be used for the stream directory lookups or possibily the playlist generation if a Host header is not provided.";
default = config.networking.domain;
defaultText = literalExpression "config.networking.domain";
};
admin = {
user = mkOption {
type = types.str;
description = "Username used for all administration functions.";
default = "admin";
};
password = mkOption {
type = types.str;
description = "Password used for all administration functions.";
};
};
logDir = mkOption {
type = types.path;
description = "Base directory used for logging.";
default = "/var/log/icecast";
};
listen = {
port = mkOption {
type = types.int;
description = "TCP port that will be used to accept client connections.";
default = 8000;
};
address = mkOption {
type = types.str;
description = "Address Icecast will listen on.";
default = "::";
};
};
user = mkOption {
type = types.str;
description = "User privileges for the server.";
default = "nobody";
};
group = mkOption {
type = types.str;
description = "Group privileges for the server.";
default = "nogroup";
};
extraConf = mkOption {
type = types.lines;
description = "icecast.xml content.";
default = "";
};
};
};
###### implementation
config = mkIf cfg.enable {
systemd.services.icecast = {
after = [ "network.target" ];
description = "Icecast Network Audio Streaming Server";
wantedBy = [ "multi-user.target" ];
preStart = "mkdir -p ${cfg.logDir} && chown ${cfg.user}:${cfg.group} ${cfg.logDir}";
serviceConfig = {
Type = "simple";
ExecStart = "${pkgs.icecast}/bin/icecast -c ${configFile}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
};
};
};
}

View file

@ -0,0 +1,294 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.jack;
pcmPlugin = cfg.jackd.enable && cfg.alsa.enable;
loopback = cfg.jackd.enable && cfg.loopback.enable;
enable32BitAlsaPlugins = cfg.alsa.support32Bit && pkgs.stdenv.isx86_64 && pkgs.pkgsi686Linux.alsa-lib != null;
umaskNeeded = versionOlder cfg.jackd.package.version "1.9.12";
bridgeNeeded = versionAtLeast cfg.jackd.package.version "1.9.12";
in {
options = {
services.jack = {
jackd = {
enable = mkEnableOption ''
JACK Audio Connection Kit. You need to add yourself to the "jackaudio" group
'';
package = mkOption {
# until jack1 promiscuous mode is fixed
internal = true;
type = types.package;
default = pkgs.jack2;
defaultText = literalExpression "pkgs.jack2";
example = literalExpression "pkgs.jack1";
description = ''
The JACK package to use.
'';
};
extraOptions = mkOption {
type = types.listOf types.str;
default = [
"-dalsa"
];
example = literalExpression ''
[ "-dalsa" "--device" "hw:1" ];
'';
description = ''
Specifies startup command line arguments to pass to JACK server.
'';
};
session = mkOption {
type = types.lines;
description = ''
Commands to run after JACK is started.
'';
};
};
alsa = {
enable = mkOption {
type = types.bool;
default = true;
description = ''
Route audio to/from generic ALSA-using applications using ALSA JACK PCM plugin.
'';
};
support32Bit = mkOption {
type = types.bool;
default = false;
description = ''
Whether to support sound for 32-bit ALSA applications on 64-bit system.
'';
};
};
loopback = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Create ALSA loopback device, instead of using PCM plugin. Has broader
application support (things like Steam will work), but may need fine-tuning
for concrete hardware.
'';
};
index = mkOption {
type = types.int;
default = 10;
description = ''
Index of an ALSA loopback device.
'';
};
config = mkOption {
type = types.lines;
description = ''
ALSA config for loopback device.
'';
};
dmixConfig = mkOption {
type = types.lines;
default = "";
example = ''
period_size 2048
periods 2
'';
description = ''
For music production software that still doesn't support JACK natively you
would like to put buffer/period adjustments here
to decrease dmix device latency.
'';
};
session = mkOption {
type = types.lines;
description = ''
Additional commands to run to setup loopback device.
'';
};
};
};
};
config = mkMerge [
(mkIf pcmPlugin {
sound.extraConfig = ''
pcm_type.jack {
libs.native = ${pkgs.alsa-plugins}/lib/alsa-lib/libasound_module_pcm_jack.so ;
${lib.optionalString enable32BitAlsaPlugins
"libs.32Bit = ${pkgs.pkgsi686Linux.alsa-plugins}/lib/alsa-lib/libasound_module_pcm_jack.so ;"}
}
pcm.!default {
@func getenv
vars [ PCM ]
default "plug:jack"
}
'';
})
(mkIf loopback {
boot.kernelModules = [ "snd-aloop" ];
boot.kernelParams = [ "snd-aloop.index=${toString cfg.loopback.index}" ];
sound.extraConfig = cfg.loopback.config;
})
(mkIf cfg.jackd.enable {
services.jack.jackd.session = ''
${lib.optionalString bridgeNeeded "${pkgs.a2jmidid}/bin/a2jmidid -e &"}
'';
# https://alsa.opensrc.org/Jack_and_Loopback_device_as_Alsa-to-Jack_bridge#id06
services.jack.loopback.config = ''
pcm.loophw00 {
type hw
card ${toString cfg.loopback.index}
device 0
subdevice 0
}
pcm.amix {
type dmix
ipc_key 219345
slave {
pcm loophw00
${cfg.loopback.dmixConfig}
}
}
pcm.asoftvol {
type softvol
slave.pcm "amix"
control { name Master }
}
pcm.cloop {
type hw
card ${toString cfg.loopback.index}
device 1
subdevice 0
format S32_LE
}
pcm.loophw01 {
type hw
card ${toString cfg.loopback.index}
device 0
subdevice 1
}
pcm.ploop {
type hw
card ${toString cfg.loopback.index}
device 1
subdevice 1
format S32_LE
}
pcm.aduplex {
type asym
playback.pcm "asoftvol"
capture.pcm "loophw01"
}
pcm.!default {
type plug
slave.pcm aduplex
}
'';
services.jack.loopback.session = ''
alsa_in -j cloop -dcloop &
alsa_out -j ploop -dploop &
while [ "$(jack_lsp cloop)" == "" ] || [ "$(jack_lsp ploop)" == "" ]; do sleep 1; done
jack_connect cloop:capture_1 system:playback_1
jack_connect cloop:capture_2 system:playback_2
jack_connect system:capture_1 ploop:playback_1
jack_connect system:capture_2 ploop:playback_2
'';
assertions = [
{
assertion = !(cfg.alsa.enable && cfg.loopback.enable);
message = "For JACK both alsa and loopback options shouldn't be used at the same time.";
}
];
users.users.jackaudio = {
group = "jackaudio";
extraGroups = [ "audio" ];
description = "JACK Audio system service user";
isSystemUser = true;
};
# http://jackaudio.org/faq/linux_rt_config.html
security.pam.loginLimits = [
{ domain = "@jackaudio"; type = "-"; item = "rtprio"; value = "99"; }
{ domain = "@jackaudio"; type = "-"; item = "memlock"; value = "unlimited"; }
];
users.groups.jackaudio = {};
environment = {
systemPackages = [ cfg.jackd.package ];
etc."alsa/conf.d/50-jack.conf".source = "${pkgs.alsa-plugins}/etc/alsa/conf.d/50-jack.conf";
variables.JACK_PROMISCUOUS_SERVER = "jackaudio";
};
services.udev.extraRules = ''
ACTION=="add", SUBSYSTEM=="sound", ATTRS{id}!="Loopback", TAG+="systemd", ENV{SYSTEMD_WANTS}="jack.service"
'';
systemd.services.jack = {
description = "JACK Audio Connection Kit";
serviceConfig = {
User = "jackaudio";
SupplementaryGroups = lib.optional
(config.hardware.pulseaudio.enable
&& !config.hardware.pulseaudio.systemWide) "users";
ExecStart = "${cfg.jackd.package}/bin/jackd ${lib.escapeShellArgs cfg.jackd.extraOptions}";
LimitRTPRIO = 99;
LimitMEMLOCK = "infinity";
} // optionalAttrs umaskNeeded {
UMask = "007";
};
path = [ cfg.jackd.package ];
environment = {
JACK_PROMISCUOUS_SERVER = "jackaudio";
JACK_NO_AUDIO_RESERVATION = "1";
};
restartIfChanged = false;
};
systemd.services.jack-session = {
description = "JACK session";
script = ''
jack_wait -w
${cfg.jackd.session}
${lib.optionalString cfg.loopback.enable cfg.loopback.session}
'';
serviceConfig = {
RemainAfterExit = true;
User = "jackaudio";
StateDirectory = "jack";
LimitRTPRIO = 99;
LimitMEMLOCK = "infinity";
};
path = [ cfg.jackd.package ];
environment = {
JACK_PROMISCUOUS_SERVER = "jackaudio";
HOME = "/var/lib/jack";
};
wantedBy = [ "jack.service" ];
partOf = [ "jack.service" ];
after = [ "jack.service" ];
restartIfChanged = false;
};
})
];
meta.maintainers = [ ];
}

View file

@ -0,0 +1,48 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.jmusicbot;
in
{
options = {
services.jmusicbot = {
enable = mkEnableOption "jmusicbot, a Discord music bot that's easy to set up and run yourself";
package = mkOption {
type = types.package;
default = pkgs.jmusicbot;
defaultText = literalExpression "pkgs.jmusicbot";
description = "JMusicBot package to use";
};
stateDir = mkOption {
type = types.path;
description = ''
The directory where config.txt and serversettings.json is saved.
If left as the default value this directory will automatically be created before JMusicBot starts, otherwise the sysadmin is responsible for ensuring the directory exists with appropriate ownership and permissions.
Untouched by the value of this option config.txt needs to be placed manually into this directory.
'';
default = "/var/lib/jmusicbot/";
};
};
};
config = mkIf cfg.enable {
systemd.services.jmusicbot = {
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
description = "Discord music bot that's easy to set up and run yourself!";
serviceConfig = mkMerge [{
ExecStart = "${cfg.package}/bin/JMusicBot";
WorkingDirectory = cfg.stateDir;
Restart = "always";
RestartSec = 20;
DynamicUser = true;
}
(mkIf (cfg.stateDir == "/var/lib/jmusicbot") { StateDirectory = "jmusicbot"; })];
};
};
meta.maintainers = with maintainers; [ SuperSandro2000 ];
}

View file

@ -0,0 +1,69 @@
{ config, lib, pkgs, ... }:
with lib;
let
streams = builtins.attrNames config.services.liquidsoap.streams;
streamService =
name:
let stream = builtins.getAttr name config.services.liquidsoap.streams; in
{ inherit name;
value = {
after = [ "network-online.target" "sound.target" ];
description = "${name} liquidsoap stream";
wantedBy = [ "multi-user.target" ];
path = [ pkgs.wget ];
serviceConfig = {
ExecStart = "${pkgs.liquidsoap}/bin/liquidsoap ${stream}";
User = "liquidsoap";
LogsDirectory = "liquidsoap";
};
};
};
in
{
##### interface
options = {
services.liquidsoap.streams = mkOption {
description =
''
Set of Liquidsoap streams to start,
one systemd service per stream.
'';
default = {};
example = {
myStream1 = "/etc/liquidsoap/myStream1.liq";
myStream2 = literalExpression "./myStream2.liq";
myStream3 = "out(playlist(\"/srv/music/\"))";
};
type = types.attrsOf (types.either types.path types.str);
};
};
##### implementation
config = mkIf (builtins.length streams != 0) {
users.users.liquidsoap = {
uid = config.ids.uids.liquidsoap;
group = "liquidsoap";
extraGroups = [ "audio" ];
description = "Liquidsoap streaming user";
home = "/var/lib/liquidsoap";
createHome = true;
};
users.groups.liquidsoap.gid = config.ids.gids.liquidsoap;
systemd.services = builtins.listToAttrs ( map streamService streams );
};
}

View file

@ -0,0 +1,108 @@
{ config, lib, pkgs, ... }:
with pkgs;
with lib;
let
uid = config.ids.uids.mopidy;
gid = config.ids.gids.mopidy;
cfg = config.services.mopidy;
mopidyConf = writeText "mopidy.conf" cfg.configuration;
mopidyEnv = buildEnv {
name = "mopidy-with-extensions-${mopidy.version}";
paths = closePropagation cfg.extensionPackages;
pathsToLink = [ "/${mopidyPackages.python.sitePackages}" ];
buildInputs = [ makeWrapper ];
postBuild = ''
makeWrapper ${mopidy}/bin/mopidy $out/bin/mopidy \
--prefix PYTHONPATH : $out/${mopidyPackages.python.sitePackages}
'';
};
in {
options = {
services.mopidy = {
enable = mkEnableOption "Mopidy, a music player daemon";
dataDir = mkOption {
default = "/var/lib/mopidy";
type = types.str;
description = ''
The directory where Mopidy stores its state.
'';
};
extensionPackages = mkOption {
default = [];
type = types.listOf types.package;
example = literalExpression "[ pkgs.mopidy-spotify ]";
description = ''
Mopidy extensions that should be loaded by the service.
'';
};
configuration = mkOption {
default = "";
type = types.lines;
description = ''
The configuration that Mopidy should use.
'';
};
extraConfigFiles = mkOption {
default = [];
type = types.listOf types.str;
description = ''
Extra config file read by Mopidy when the service starts.
Later files in the list overrides earlier configuration.
'';
};
};
};
###### implementation
config = mkIf cfg.enable {
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' - mopidy mopidy - -"
];
systemd.services.mopidy = {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" "sound.target" ];
description = "mopidy music player daemon";
serviceConfig = {
ExecStart = "${mopidyEnv}/bin/mopidy --config ${concatStringsSep ":" ([mopidyConf] ++ cfg.extraConfigFiles)}";
User = "mopidy";
};
};
systemd.services.mopidy-scan = {
description = "mopidy local files scanner";
serviceConfig = {
ExecStart = "${mopidyEnv}/bin/mopidy --config ${concatStringsSep ":" ([mopidyConf] ++ cfg.extraConfigFiles)} local scan";
User = "mopidy";
Type = "oneshot";
};
};
users.users.mopidy = {
inherit uid;
group = "mopidy";
extraGroups = [ "audio" ];
description = "Mopidy daemon user";
home = cfg.dataDir;
};
users.groups.mopidy.gid = gid;
};
}

View file

@ -0,0 +1,265 @@
{ config, lib, pkgs, ... }:
with lib;
let
name = "mpd";
uid = config.ids.uids.mpd;
gid = config.ids.gids.mpd;
cfg = config.services.mpd;
credentialsPlaceholder = (creds:
let
placeholders = (imap0
(i: c: ''password "{{password-${toString i}}}@${concatStringsSep "," c.permissions}"'')
creds);
in
concatStringsSep "\n" placeholders);
mpdConf = pkgs.writeText "mpd.conf" ''
# This file was automatically generated by NixOS. Edit mpd's configuration
# via NixOS' configuration.nix, as this file will be rewritten upon mpd's
# restart.
music_directory "${cfg.musicDirectory}"
playlist_directory "${cfg.playlistDirectory}"
${lib.optionalString (cfg.dbFile != null) ''
db_file "${cfg.dbFile}"
''}
state_file "${cfg.dataDir}/state"
sticker_file "${cfg.dataDir}/sticker.sql"
${optionalString (cfg.network.listenAddress != "any") ''bind_to_address "${cfg.network.listenAddress}"''}
${optionalString (cfg.network.port != 6600) ''port "${toString cfg.network.port}"''}
${optionalString (cfg.fluidsynth) ''
decoder {
plugin "fluidsynth"
soundfont "${pkgs.soundfont-fluid}/share/soundfonts/FluidR3_GM2-2.sf2"
}
''}
${optionalString (cfg.credentials != []) (credentialsPlaceholder cfg.credentials)}
${cfg.extraConfig}
'';
in {
###### interface
options = {
services.mpd = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable MPD, the music player daemon.
'';
};
startWhenNeeded = mkOption {
type = types.bool;
default = false;
description = ''
If set, <command>mpd</command> is socket-activated; that
is, instead of having it permanently running as a daemon,
systemd will start it on the first incoming connection.
'';
};
musicDirectory = mkOption {
type = with types; either path (strMatching "(http|https|nfs|smb)://.+");
default = "${cfg.dataDir}/music";
defaultText = literalExpression ''"''${dataDir}/music"'';
description = ''
The directory or NFS/SMB network share where MPD reads music from. If left
as the default value this directory will automatically be created before
the MPD server starts, otherwise the sysadmin is responsible for ensuring
the directory exists with appropriate ownership and permissions.
'';
};
playlistDirectory = mkOption {
type = types.path;
default = "${cfg.dataDir}/playlists";
defaultText = literalExpression ''"''${dataDir}/playlists"'';
description = ''
The directory where MPD stores playlists. If left as the default value
this directory will automatically be created before the MPD server starts,
otherwise the sysadmin is responsible for ensuring the directory exists
with appropriate ownership and permissions.
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
Extra directives added to to the end of MPD's configuration file,
mpd.conf. Basic configuration like file location and uid/gid
is added automatically to the beginning of the file. For available
options see <literal>man 5 mpd.conf</literal>'.
'';
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/${name}";
description = ''
The directory where MPD stores its state, tag cache, playlists etc. If
left as the default value this directory will automatically be created
before the MPD server starts, otherwise the sysadmin is responsible for
ensuring the directory exists with appropriate ownership and permissions.
'';
};
user = mkOption {
type = types.str;
default = name;
description = "User account under which MPD runs.";
};
group = mkOption {
type = types.str;
default = name;
description = "Group account under which MPD runs.";
};
network = {
listenAddress = mkOption {
type = types.str;
default = "127.0.0.1";
example = "any";
description = ''
The address for the daemon to listen on.
Use <literal>any</literal> to listen on all addresses.
'';
};
port = mkOption {
type = types.int;
default = 6600;
description = ''
This setting is the TCP port that is desired for the daemon to get assigned
to.
'';
};
};
dbFile = mkOption {
type = types.nullOr types.str;
default = "${cfg.dataDir}/tag_cache";
defaultText = literalExpression ''"''${dataDir}/tag_cache"'';
description = ''
The path to MPD's database. If set to <literal>null</literal> the
parameter is omitted from the configuration.
'';
};
credentials = mkOption {
type = types.listOf (types.submodule {
options = {
passwordFile = mkOption {
type = types.path;
description = ''
Path to file containing the password.
'';
};
permissions = let
perms = ["read" "add" "control" "admin"];
in mkOption {
type = types.listOf (types.enum perms);
default = [ "read" ];
description = ''
List of permissions that are granted with this password.
Permissions can be "${concatStringsSep "\", \"" perms}".
'';
};
};
});
description = ''
Credentials and permissions for accessing the mpd server.
'';
default = [];
example = [
{passwordFile = "/var/lib/secrets/mpd_readonly_password"; permissions = [ "read" ];}
{passwordFile = "/var/lib/secrets/mpd_admin_password"; permissions = ["read" "add" "control" "admin"];}
];
};
fluidsynth = mkOption {
type = types.bool;
default = false;
description = ''
If set, add fluidsynth soundfont and configure the plugin.
'';
};
};
};
###### implementation
config = mkIf cfg.enable {
# install mpd units
systemd.packages = [ pkgs.mpd ];
systemd.sockets.mpd = mkIf cfg.startWhenNeeded {
wantedBy = [ "sockets.target" ];
listenStreams = [
(if pkgs.lib.hasPrefix "/" cfg.network.listenAddress
then cfg.network.listenAddress
else "${optionalString (cfg.network.listenAddress != "any") "${cfg.network.listenAddress}:"}${toString cfg.network.port}")
];
};
systemd.services.mpd = {
wantedBy = optional (!cfg.startWhenNeeded) "multi-user.target";
preStart =
''
set -euo pipefail
install -m 600 ${mpdConf} /run/mpd/mpd.conf
'' + optionalString (cfg.credentials != [])
(concatStringsSep "\n"
(imap0
(i: c: ''${pkgs.replace-secret}/bin/replace-secret '{{password-${toString i}}}' '${c.passwordFile}' /run/mpd/mpd.conf'')
cfg.credentials));
serviceConfig =
{
User = "${cfg.user}";
# Note: the first "" overrides the ExecStart from the upstream unit
ExecStart = [ "" "${pkgs.mpd}/bin/mpd --systemd /run/mpd/mpd.conf" ];
RuntimeDirectory = "mpd";
StateDirectory = []
++ optionals (cfg.dataDir == "/var/lib/${name}") [ name ]
++ optionals (cfg.playlistDirectory == "/var/lib/${name}/playlists") [ name "${name}/playlists" ]
++ optionals (cfg.musicDirectory == "/var/lib/${name}/music") [ name "${name}/music" ];
};
};
users.users = optionalAttrs (cfg.user == name) {
${name} = {
inherit uid;
group = cfg.group;
extraGroups = [ "audio" ];
description = "Music Player Daemon user";
home = "${cfg.dataDir}";
};
};
users.groups = optionalAttrs (cfg.group == name) {
${name}.gid = gid;
};
};
}

View file

@ -0,0 +1,213 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.mpdscribble;
mpdCfg = config.services.mpd;
mpdOpt = options.services.mpd;
endpointUrls = {
"last.fm" = "http://post.audioscrobbler.com";
"libre.fm" = "http://turtle.libre.fm";
"jamendo" = "http://postaudioscrobbler.jamendo.com";
"listenbrainz" = "http://proxy.listenbrainz.org";
};
mkSection = secname: secCfg: ''
[${secname}]
url = ${secCfg.url}
username = ${secCfg.username}
password = {{${secname}_PASSWORD}}
journal = /var/lib/mpdscribble/${secname}.journal
'';
endpoints = concatStringsSep "\n" (mapAttrsToList mkSection cfg.endpoints);
cfgTemplate = pkgs.writeText "mpdscribble.conf" ''
## This file was automatically genenrated by NixOS and will be overwritten.
## Do not edit. Edit your NixOS configuration instead.
## mpdscribble - an audioscrobbler for the Music Player Daemon.
## http://mpd.wikia.com/wiki/Client:mpdscribble
# HTTP proxy URL.
${optionalString (cfg.proxy != null) "proxy = ${cfg.proxy}"}
# The location of the mpdscribble log file. The special value
# "syslog" makes mpdscribble use the local syslog daemon. On most
# systems, log messages will appear in /var/log/daemon.log then.
# "-" means log to stderr (the current terminal).
log = -
# How verbose mpdscribble's logging should be. Default is 1.
verbose = ${toString cfg.verbose}
# How often should mpdscribble save the journal file? [seconds]
journal_interval = ${toString cfg.journalInterval}
# The host running MPD, possibly protected by a password
# ([PASSWORD@]HOSTNAME).
host = ${(optionalString (cfg.passwordFile != null) "{{MPD_PASSWORD}}@") + cfg.host}
# The port that the MPD listens on and mpdscribble should try to
# connect to.
port = ${toString cfg.port}
${endpoints}
'';
cfgFile = "/run/mpdscribble/mpdscribble.conf";
replaceSecret = secretFile: placeholder: targetFile:
optionalString (secretFile != null) ''
${pkgs.replace-secret}/bin/replace-secret '${placeholder}' '${secretFile}' '${targetFile}' '';
preStart = pkgs.writeShellScript "mpdscribble-pre-start" ''
cp -f "${cfgTemplate}" "${cfgFile}"
${replaceSecret cfg.passwordFile "{{MPD_PASSWORD}}" cfgFile}
${concatStringsSep "\n" (mapAttrsToList (secname: cfg:
replaceSecret cfg.passwordFile "{{${secname}_PASSWORD}}" cfgFile)
cfg.endpoints)}
'';
localMpd = (cfg.host == "localhost" || cfg.host == "127.0.0.1");
in {
###### interface
options.services.mpdscribble = {
enable = mkEnableOption "mpdscribble";
proxy = mkOption {
default = null;
type = types.nullOr types.str;
description = ''
HTTP proxy URL.
'';
};
verbose = mkOption {
default = 1;
type = types.int;
description = ''
Log level for the mpdscribble daemon.
'';
};
journalInterval = mkOption {
default = 600;
example = 60;
type = types.int;
description = ''
How often should mpdscribble save the journal file? [seconds]
'';
};
host = mkOption {
default = (if mpdCfg.network.listenAddress != "any" then
mpdCfg.network.listenAddress
else
"localhost");
defaultText = literalExpression ''
if config.${mpdOpt.network.listenAddress} != "any"
then config.${mpdOpt.network.listenAddress}
else "localhost"
'';
type = types.str;
description = ''
Host for the mpdscribble daemon to search for a mpd daemon on.
'';
};
passwordFile = mkOption {
default = if localMpd then
(findFirst
(c: any (x: x == "read") c.permissions)
{ passwordFile = null; }
mpdCfg.credentials).passwordFile
else
null;
defaultText = literalDocBook ''
The first password file with read access configured for MPD when using a local instance,
otherwise <literal>null</literal>.
'';
type = types.nullOr types.str;
description = ''
File containing the password for the mpd daemon.
If there is a local mpd configured using <option>services.mpd.credentials</option>
the default is automatically set to a matching passwordFile of the local mpd.
'';
};
port = mkOption {
default = mpdCfg.network.port;
defaultText = literalExpression "config.${mpdOpt.network.port}";
type = types.port;
description = ''
Port for the mpdscribble daemon to search for a mpd daemon on.
'';
};
endpoints = mkOption {
type = (let
endpoint = { name, ... }: {
options = {
url = mkOption {
type = types.str;
default = endpointUrls.${name} or "";
description =
"The url endpoint where the scrobble API is listening.";
};
username = mkOption {
type = types.str;
description = ''
Username for the scrobble service.
'';
};
passwordFile = mkOption {
type = types.nullOr types.str;
description =
"File containing the password, either as MD5SUM or cleartext.";
};
};
};
in types.attrsOf (types.submodule endpoint));
default = { };
example = {
"last.fm" = {
username = "foo";
passwordFile = "/run/secrets/lastfm_password";
};
};
description = ''
Endpoints to scrobble to.
If the endpoint is one of "${
concatStringsSep "\", \"" (attrNames endpointUrls)
}" the url is set automatically.
'';
};
};
###### implementation
config = mkIf cfg.enable {
systemd.services.mpdscribble = {
after = [ "network.target" ] ++ (optional localMpd "mpd.service");
description = "mpdscribble mpd scrobble client";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = true;
StateDirectory = "mpdscribble";
RuntimeDirectory = "mpdscribble";
RuntimeDirectoryMode = "700";
# TODO use LoadCredential= instead of running preStart with full privileges?
ExecStartPre = "+${preStart}";
ExecStart =
"${pkgs.mpdscribble}/bin/mpdscribble --no-daemon --conf ${cfgFile}";
};
};
};
}

View file

@ -0,0 +1,71 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.navidrome;
settingsFormat = pkgs.formats.json {};
in {
options = {
services.navidrome = {
enable = mkEnableOption "Navidrome music server";
settings = mkOption rec {
type = settingsFormat.type;
apply = recursiveUpdate default;
default = {
Address = "127.0.0.1";
Port = 4533;
};
example = {
MusicFolder = "/mnt/music";
};
description = ''
Configuration for Navidrome, see <link xlink:href="https://www.navidrome.org/docs/usage/configuration-options/"/> for supported values.
'';
};
};
};
config = mkIf cfg.enable {
systemd.services.navidrome = {
description = "Navidrome Media Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = ''
${pkgs.navidrome}/bin/navidrome --configfile ${settingsFormat.generate "navidrome.json" cfg.settings}
'';
DynamicUser = true;
StateDirectory = "navidrome";
WorkingDirectory = "/var/lib/navidrome";
RuntimeDirectory = "navidrome";
RootDirectory = "/run/navidrome";
ReadWritePaths = "";
BindReadOnlyPaths = [
builtins.storeDir
] ++ lib.optional (cfg.settings ? MusicFolder) cfg.settings.MusicFolder;
CapabilityBoundingSet = "";
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
RestrictNamespaces = true;
PrivateDevices = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
SystemCallArchitectures = "native";
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
RestrictRealtime = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
UMask = "0066";
ProtectHostname = true;
};
};
};
}

View file

@ -0,0 +1,19 @@
{ config, lib, pkgs, ... }:
with lib;
let
name = "networkaudiod";
cfg = config.services.networkaudiod;
in {
options = {
services.networkaudiod = {
enable = mkEnableOption "Networkaudiod (NAA)";
};
};
config = mkIf cfg.enable {
systemd.packages = [ pkgs.networkaudiod ];
systemd.services.networkaudiod.wantedBy = [ "multi-user.target" ];
};
}

View file

@ -0,0 +1,76 @@
{ config, lib, pkgs, ... }:
with lib;
let
name = "roon-bridge";
cfg = config.services.roon-bridge;
in {
options = {
services.roon-bridge = {
enable = mkEnableOption "Roon Bridge";
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Open ports in the firewall for the bridge.
'';
};
user = mkOption {
type = types.str;
default = "roon-bridge";
description = ''
User to run the Roon bridge as.
'';
};
group = mkOption {
type = types.str;
default = "roon-bridge";
description = ''
Group to run the Roon Bridge as.
'';
};
};
};
config = mkIf cfg.enable {
systemd.services.roon-bridge = {
after = [ "network.target" ];
description = "Roon Bridge";
wantedBy = [ "multi-user.target" ];
environment.ROON_DATAROOT = "/var/lib/${name}";
serviceConfig = {
ExecStart = "${pkgs.roon-bridge}/start.sh";
LimitNOFILE = 8192;
User = cfg.user;
Group = cfg.group;
StateDirectory = name;
};
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPortRanges = [{ from = 9100; to = 9200; }];
allowedUDPPorts = [ 9003 ];
extraCommands = ''
iptables -A INPUT -s 224.0.0.0/4 -j ACCEPT
iptables -A INPUT -d 224.0.0.0/4 -j ACCEPT
iptables -A INPUT -s 240.0.0.0/5 -j ACCEPT
iptables -A INPUT -m pkttype --pkt-type multicast -j ACCEPT
iptables -A INPUT -m pkttype --pkt-type broadcast -j ACCEPT
'';
};
users.groups.${cfg.group} = {};
users.users.${cfg.user} =
if cfg.user == "roon-bridge" then {
isSystemUser = true;
description = "Roon Bridge user";
group = cfg.group;
extraGroups = [ "audio" ];
}
else {};
};
}

View file

@ -0,0 +1,79 @@
{ config, lib, pkgs, ... }:
with lib;
let
name = "roon-server";
cfg = config.services.roon-server;
in {
options = {
services.roon-server = {
enable = mkEnableOption "Roon Server";
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Open ports in the firewall for the server.
'';
};
user = mkOption {
type = types.str;
default = "roon-server";
description = ''
User to run the Roon Server as.
'';
};
group = mkOption {
type = types.str;
default = "roon-server";
description = ''
Group to run the Roon Server as.
'';
};
};
};
config = mkIf cfg.enable {
systemd.services.roon-server = {
after = [ "network.target" ];
description = "Roon Server";
wantedBy = [ "multi-user.target" ];
environment.ROON_DATAROOT = "/var/lib/${name}";
serviceConfig = {
ExecStart = "${pkgs.roon-server}/bin/RoonServer";
LimitNOFILE = 8192;
User = cfg.user;
Group = cfg.group;
StateDirectory = name;
};
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPortRanges = [
{ from = 9100; to = 9200; }
{ from = 9330; to = 9332; }
];
allowedUDPPorts = [ 9003 ];
extraCommands = ''
iptables -A INPUT -s 224.0.0.0/4 -j ACCEPT
iptables -A INPUT -d 224.0.0.0/4 -j ACCEPT
iptables -A INPUT -s 240.0.0.0/5 -j ACCEPT
iptables -A INPUT -m pkttype --pkt-type multicast -j ACCEPT
iptables -A INPUT -m pkttype --pkt-type broadcast -j ACCEPT
'';
};
users.groups.${cfg.group} = {};
users.users.${cfg.user} =
if cfg.user == "roon-server" then {
isSystemUser = true;
description = "Roon Server user";
group = cfg.group;
extraGroups = [ "audio" ];
}
else {};
};
}

View file

@ -0,0 +1,73 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.slimserver;
in {
options = {
services.slimserver = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable slimserver.
'';
};
package = mkOption {
type = types.package;
default = pkgs.slimserver;
defaultText = literalExpression "pkgs.slimserver";
description = "Slimserver package to use.";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/slimserver";
description = ''
The directory where slimserver stores its state, tag cache,
playlists etc.
'';
};
};
};
###### implementation
config = mkIf cfg.enable {
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' - slimserver slimserver - -"
];
systemd.services.slimserver = {
after = [ "network.target" ];
description = "Slim Server for Logitech Squeezebox Players";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "slimserver";
# Issue 40589: Disable broken image/video support (audio still works!)
ExecStart = "${cfg.package}/slimserver.pl --logdir ${cfg.dataDir}/logs --prefsdir ${cfg.dataDir}/prefs --cachedir ${cfg.dataDir}/cache --noimage --novideo";
};
};
users = {
users.slimserver = {
description = "Slimserver daemon user";
home = cfg.dataDir;
group = "slimserver";
isSystemUser = true;
};
groups.slimserver = {};
};
};
}

View file

@ -0,0 +1,323 @@
{ config, options, lib, pkgs, ... }:
with lib;
let
name = "snapserver";
cfg = config.services.snapserver;
# Using types.nullOr to inherit upstream defaults.
sampleFormat = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Default sample format.
'';
example = "48000:16:2";
};
codec = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Default audio compression method.
'';
example = "flac";
};
streamToOption = name: opt:
let
os = val:
optionalString (val != null) "${val}";
os' = prefix: val:
optionalString (val != null) (prefix + "${val}");
flatten = key: value:
"&${key}=${value}";
in
"--stream.stream=\"${opt.type}://" + os opt.location + "?" + os' "name=" name
+ concatStrings (mapAttrsToList flatten opt.query) + "\"";
optionalNull = val: ret:
optional (val != null) ret;
optionString = concatStringsSep " " (mapAttrsToList streamToOption cfg.streams
# global options
++ [ "--stream.bind_to_address=${cfg.listenAddress}" ]
++ [ "--stream.port=${toString cfg.port}" ]
++ optionalNull cfg.sampleFormat "--stream.sampleformat=${cfg.sampleFormat}"
++ optionalNull cfg.codec "--stream.codec=${cfg.codec}"
++ optionalNull cfg.streamBuffer "--stream.stream_buffer=${toString cfg.streamBuffer}"
++ optionalNull cfg.buffer "--stream.buffer=${toString cfg.buffer}"
++ optional cfg.sendToMuted "--stream.send_to_muted"
# tcp json rpc
++ [ "--tcp.enabled=${toString cfg.tcp.enable}" ]
++ optionals cfg.tcp.enable [
"--tcp.bind_to_address=${cfg.tcp.listenAddress}"
"--tcp.port=${toString cfg.tcp.port}" ]
# http json rpc
++ [ "--http.enabled=${toString cfg.http.enable}" ]
++ optionals cfg.http.enable [
"--http.bind_to_address=${cfg.http.listenAddress}"
"--http.port=${toString cfg.http.port}"
] ++ optional (cfg.http.docRoot != null) "--http.doc_root=\"${toString cfg.http.docRoot}\"");
in {
imports = [
(mkRenamedOptionModule [ "services" "snapserver" "controlPort" ] [ "services" "snapserver" "tcp" "port" ])
];
###### interface
options = {
services.snapserver = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable snapserver.
'';
};
listenAddress = mkOption {
type = types.str;
default = "::";
example = "0.0.0.0";
description = ''
The address where snapclients can connect.
'';
};
port = mkOption {
type = types.port;
default = 1704;
description = ''
The port that snapclients can connect to.
'';
};
openFirewall = mkOption {
type = types.bool;
# Make the behavior consistent with other services. Set the default to
# false and remove the accompanying warning after NixOS 22.05 is released.
default = true;
description = ''
Whether to automatically open the specified ports in the firewall.
'';
};
inherit sampleFormat;
inherit codec;
streamBuffer = mkOption {
type = with types; nullOr int;
default = null;
description = ''
Stream read (input) buffer in ms.
'';
example = 20;
};
buffer = mkOption {
type = with types; nullOr int;
default = null;
description = ''
Network buffer in ms.
'';
example = 1000;
};
sendToMuted = mkOption {
type = types.bool;
default = false;
description = ''
Send audio to muted clients.
'';
};
tcp.enable = mkOption {
type = types.bool;
default = true;
description = ''
Whether to enable the JSON-RPC via TCP.
'';
};
tcp.listenAddress = mkOption {
type = types.str;
default = "::";
example = "0.0.0.0";
description = ''
The address where the TCP JSON-RPC listens on.
'';
};
tcp.port = mkOption {
type = types.port;
default = 1705;
description = ''
The port where the TCP JSON-RPC listens on.
'';
};
http.enable = mkOption {
type = types.bool;
default = true;
description = ''
Whether to enable the JSON-RPC via HTTP.
'';
};
http.listenAddress = mkOption {
type = types.str;
default = "::";
example = "0.0.0.0";
description = ''
The address where the HTTP JSON-RPC listens on.
'';
};
http.port = mkOption {
type = types.port;
default = 1780;
description = ''
The port where the HTTP JSON-RPC listens on.
'';
};
http.docRoot = mkOption {
type = with types; nullOr path;
default = null;
description = ''
Path to serve from the HTTP servers root.
'';
};
streams = mkOption {
type = with types; attrsOf (submodule {
options = {
location = mkOption {
type = types.oneOf [ types.path types.str ];
description = ''
For type <literal>pipe</literal> or <literal>file</literal>, the path to the pipe or file.
For type <literal>librespot</literal>, <literal>airplay</literal> or <literal>process</literal>, the path to the corresponding binary.
For type <literal>tcp</literal>, the <literal>host:port</literal> address to connect to or listen on.
For type <literal>meta</literal>, a list of stream names in the form <literal>/one/two/...</literal>. Don't forget the leading slash.
For type <literal>alsa</literal>, use an empty string.
'';
example = literalExpression ''
"/path/to/pipe"
"/path/to/librespot"
"192.168.1.2:4444"
"/MyTCP/Spotify/MyPipe"
'';
};
type = mkOption {
type = types.enum [ "pipe" "librespot" "airplay" "file" "process" "tcp" "alsa" "spotify" "meta" ];
default = "pipe";
description = ''
The type of input stream.
'';
};
query = mkOption {
type = attrsOf str;
default = {};
description = ''
Key-value pairs that convey additional parameters about a stream.
'';
example = literalExpression ''
# for type == "pipe":
{
mode = "create";
};
# for type == "process":
{
params = "--param1 --param2";
logStderr = "true";
};
# for type == "tcp":
{
mode = "client";
}
# for type == "alsa":
{
device = "hw:0,0";
}
'';
};
inherit sampleFormat;
inherit codec;
};
});
default = { default = {}; };
description = ''
The definition for an input source.
'';
example = literalExpression ''
{
mpd = {
type = "pipe";
location = "/run/snapserver/mpd";
sampleFormat = "48000:16:2";
codec = "pcm";
};
};
'';
};
};
};
###### implementation
config = mkIf cfg.enable {
warnings =
# https://github.com/badaix/snapcast/blob/98ac8b2fb7305084376607b59173ce4097c620d8/server/streamreader/stream_manager.cpp#L85
filter (w: w != "") (mapAttrsToList (k: v: if v.type == "spotify" then ''
services.snapserver.streams.${k}.type = "spotify" is deprecated, use services.snapserver.streams.${k}.type = "librespot" instead.
'' else "") cfg.streams)
# Remove this warning after NixOS 22.05 is released.
++ optional (options.services.snapserver.openFirewall.highestPrio >= (mkOptionDefault null).priority) ''
services.snapserver.openFirewall will no longer default to true starting with NixOS 22.11.
Enable it explicitly if you need to control Snapserver remotely.
'';
systemd.services.snapserver = {
after = [ "network.target" ];
description = "Snapserver";
wantedBy = [ "multi-user.target" ];
before = [ "mpd.service" "mopidy.service" ];
serviceConfig = {
DynamicUser = true;
ExecStart = "${pkgs.snapcast}/bin/snapserver --daemon ${optionString}";
Type = "forking";
LimitRTPRIO = 50;
LimitRTTIME = "infinity";
NoNewPrivileges = true;
PIDFile = "/run/${name}/pid";
ProtectKernelTunables = true;
ProtectControlGroups = true;
ProtectKernelModules = true;
RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK";
RestrictNamespaces = true;
RuntimeDirectory = name;
StateDirectory = name;
};
};
networking.firewall.allowedTCPPorts =
optionals cfg.openFirewall [ cfg.port ]
++ optional (cfg.openFirewall && cfg.tcp.enable) cfg.tcp.port
++ optional (cfg.openFirewall && cfg.http.enable) cfg.http.port;
};
meta = {
maintainers = with maintainers; [ tobim ];
};
}

View file

@ -0,0 +1,68 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.spotifyd;
toml = pkgs.formats.toml {};
warnConfig =
if cfg.config != ""
then lib.trace "Using the stringly typed .config attribute is discouraged. Use the TOML typed .settings attribute instead."
else id;
spotifydConf =
if cfg.settings != {}
then toml.generate "spotify.conf" cfg.settings
else warnConfig (pkgs.writeText "spotifyd.conf" cfg.config);
in
{
options = {
services.spotifyd = {
enable = mkEnableOption "spotifyd, a Spotify playing daemon";
config = mkOption {
default = "";
type = types.lines;
description = ''
(Deprecated) Configuration for Spotifyd. For syntax and directives, see
<link xlink:href="https://github.com/Spotifyd/spotifyd#Configuration"/>.
'';
};
settings = mkOption {
default = {};
type = toml.type;
example = { global.bitrate = 320; };
description = ''
Configuration for Spotifyd. For syntax and directives, see
<link xlink:href="https://github.com/Spotifyd/spotifyd#Configuration"/>.
'';
};
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.config == "" || cfg.settings == {};
message = "At most one of the .config attribute and the .settings attribute may be set";
}
];
systemd.services.spotifyd = {
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" "sound.target" ];
description = "spotifyd, a Spotify playing daemon";
environment.SHELL = "/bin/sh";
serviceConfig = {
ExecStart = "${pkgs.spotifyd}/bin/spotifyd --no-daemon --cache-path /var/cache/spotifyd --config-path ${spotifydConf}";
Restart = "always";
RestartSec = 12;
DynamicUser = true;
CacheDirectory = "spotifyd";
SupplementaryGroups = ["audio"];
};
};
};
meta.maintainers = [ maintainers.anderslundstedt ];
}

View file

@ -0,0 +1,46 @@
{ config, lib, pkgs, ... }:
let
inherit (lib) mkEnableOption mkIf mkOption optionalString types;
dataDir = "/var/lib/squeezelite";
cfg = config.services.squeezelite;
pkg = if cfg.pulseAudio then pkgs.squeezelite-pulse else pkgs.squeezelite;
bin = "${pkg}/bin/${pkg.pname}";
in
{
###### interface
options.services.squeezelite = {
enable = mkEnableOption "Squeezelite, a software Squeezebox emulator";
pulseAudio = mkEnableOption "pulseaudio support";
extraArguments = mkOption {
default = "";
type = types.str;
description = ''
Additional command line arguments to pass to Squeezelite.
'';
};
};
###### implementation
config = mkIf cfg.enable {
systemd.services.squeezelite = {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" "sound.target" ];
description = "Software Squeezebox emulator";
serviceConfig = {
DynamicUser = true;
ExecStart = "${bin} -N ${dataDir}/player-name ${cfg.extraArguments}";
StateDirectory = builtins.baseNameOf dataDir;
SupplementaryGroups = "audio";
};
};
};
}

View file

@ -0,0 +1,57 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.ympd;
in {
###### interface
options = {
services.ympd = {
enable = mkEnableOption "ympd, the MPD Web GUI";
webPort = mkOption {
type = types.either types.str types.port; # string for backwards compat
default = "8080";
description = "The port where ympd's web interface will be available.";
example = "ssl://8080:/path/to/ssl-private-key.pem";
};
mpd = {
host = mkOption {
type = types.str;
default = "localhost";
description = "The host where MPD is listening.";
};
port = mkOption {
type = types.int;
default = config.services.mpd.network.port;
defaultText = literalExpression "config.services.mpd.network.port";
description = "The port where MPD is listening.";
example = 6600;
};
};
};
};
###### implementation
config = mkIf cfg.enable {
systemd.services.ympd = {
description = "Standalone MPD Web GUI written in C";
wantedBy = [ "multi-user.target" ];
serviceConfig.ExecStart = "${pkgs.ympd}/bin/ympd --host ${cfg.mpd.host} --port ${toString cfg.mpd.port} --webport ${toString cfg.webPort} --user nobody";
};
};
}