diff --git a/nixos/modules/services/audio/mpd.nix b/nixos/modules/services/audio/mpd.nix index 904b0a8c60a4..1a58732ca944 100644 --- a/nixos/modules/services/audio/mpd.nix +++ b/nixos/modules/services/audio/mpd.nix @@ -12,46 +12,85 @@ let gid = config.ids.gids.mpd; cfg = config.services.mpd; - credentialsPlaceholder = ( - creds: - let - placeholders = ( - lib.imap0 ( - i: c: ''password "{{password-${toString i}}}@${lib.concatStringsSep "," c.permissions}"'' - ) creds - ); - in - lib.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" - - ${lib.optionalString ( - cfg.network.listenAddress != "any" - ) ''bind_to_address "${cfg.network.listenAddress}"''} - ${lib.optionalString (cfg.network.port != 6600) ''port "${toString cfg.network.port}"''} - ${lib.optionalString (cfg.fluidsynth) '' - decoder { - plugin "fluidsynth" - soundfont "${pkgs.soundfont-fluid}/share/soundfonts/FluidR3_GM2-2.sf2" - } - ''} - - ${lib.optionalString (cfg.credentials != [ ]) (credentialsPlaceholder cfg.credentials)} - - ${cfg.extraConfig} - ''; + mkKeyValue = + a: + lib.mapAttrsToList ( + k: v: + k + + " " + + ( + if builtins.isBool v then + # Mainly for https://mpd.readthedocs.io/en/stable/user.html#zeroconf + "\"" + (lib.boolToYesNo v) + "\"" + else + "\"" + (toString v) + "\"" + ) + ) a; + nonBlockSettings = lib.filterAttrs (n: v: !(builtins.isAttrs v || builtins.isList v)) cfg.settings; + pureBlockSettings = builtins.removeAttrs cfg.settings (builtins.attrNames nonBlockSettings); + blocks = + pureBlockSettings + // lib.optionalAttrs cfg.fluidsynth { + decoder = (pureBlockSettings.decoder or [ ]) ++ [ + { + plugin = "fluidsynth"; + soundfont = "${pkgs.soundfont-fluid}/share/soundfonts/FluidR3_GM2-2.sf2"; + } + ]; + }; + processSingleBlock = + n: v: + [ + (n + " {") + ] + # Add indentation, for better readability + ++ (map (l: " " + l) (mkKeyValue v)) + ++ [ "}" ]; + mpdConf = pkgs.writeTextFile { + name = "mpd.conf"; + text = '' + # 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. + '' + + lib.concatStringsSep "\n" ( + mkKeyValue ( + { + state_file = "${cfg.dataDir}/state"; + sticker_file = "${cfg.dataDir}/sticker.sql"; + } + // nonBlockSettings + ) + ++ lib.flatten ( + lib.mapAttrsToList ( + n: v: if builtins.isList v then (map (b: processSingleBlock n b) v) else (processSingleBlock n v) + ) blocks + ) + ++ lib.imap0 ( + i: a: "password \"{{password-${toString i}}}@${lib.concatStringsSep "," a.permissions}\"" + ) cfg.credentials + ); + derivationArgs = { + expectScript = '' + spawn ${lib.getExe pkgs.buildPackages.mpd} --no-daemon "$env(out)" + expect { + "exception: Error in \"$env(out)\"" { + puts "Config file invalid\n" + exit 1 + } + "exception:" { + exit 0 + } + } + ''; + passAsFile = [ + "expectScript" + ]; + }; + checkPhase = '' + ${lib.getExe pkgs.buildPackages.expect} -f "$expectScriptPath" + ''; + }; in { @@ -80,39 +119,16 @@ in ''; }; - musicDirectory = lib.mkOption { - type = with lib.types; either path (strMatching "(http|https|nfs|smb)://.+"); - default = "${cfg.dataDir}/music"; - defaultText = lib.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. - ''; + user = lib.mkOption { + type = lib.types.str; + default = name; + description = "User account under which MPD runs."; }; - playlistDirectory = lib.mkOption { - type = lib.types.path; - default = "${cfg.dataDir}/playlists"; - defaultText = lib.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 = lib.mkOption { - type = lib.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 {manpage}`mpd.conf(5)`. - ''; + group = lib.mkOption { + type = lib.types.str; + default = name; + description = "Group account under which MPD runs."; }; dataDir = lib.mkOption { @@ -126,48 +142,135 @@ in ''; }; - user = lib.mkOption { - type = lib.types.str; - default = name; - description = "User account under which MPD runs."; + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Open ports in the firewall for mpd."; }; - group = lib.mkOption { - type = lib.types.str; - default = name; - description = "Group account under which MPD runs."; - }; + settings = lib.mkOption { + type = lib.types.submodule { + freeformType = + let + inherit (lib.types) + oneOf + attrsOf + listOf + str + int + bool + path + ; + atomType = oneOf [ + str + int + bool + path + ]; + in + attrsOf (oneOf [ + atomType + (listOf (attrsOf atomType)) + ]); + options = { + music_directory = lib.mkOption { + type = with lib.types; either path (strMatching "([a-z]+)://.+"); + default = "${cfg.dataDir}/music"; + defaultText = lib.literalExpression ''"''${dataDir}/music"''; + description = '' + The directory or URI 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. + ''; + }; - network = { + playlist_directory = lib.mkOption { + type = lib.types.path; + default = "${cfg.dataDir}/playlists"; + defaultText = lib.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. + ''; + }; - listenAddress = lib.mkOption { - type = lib.types.str; - default = "127.0.0.1"; - example = "any"; - description = '' - The address for the daemon to listen on. - Use `any` to listen on all addresses. - ''; + bind_to_address = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + example = "any"; + description = '' + The address for the daemon to listen on. + Use `any` to listen on all addresses. + ''; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 6600; + description = '' + This setting is the TCP port that is desired for the daemon to get assigned + to. + ''; + }; + + db_file = lib.mkOption { + type = lib.types.path; + default = "${cfg.dataDir}/tag_cache"; + defaultText = lib.literalExpression ''"''${dataDir}/tag_cache"''; + description = '' + The path to MPD's database. + ''; + }; + }; }; - - port = lib.mkOption { - type = lib.types.port; - default = 6600; - description = '' - This setting is the TCP port that is desired for the daemon to get assigned - to. - ''; - }; - - }; - - dbFile = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = "${cfg.dataDir}/tag_cache"; - defaultText = lib.literalExpression ''"''${dataDir}/tag_cache"''; + default = { }; description = '' - The path to MPD's database. If set to `null` the - parameter is omitted from the configuration. + Configuration for MPD. MPD supports key-value like blocks for settings + like `audio_output` and `neighbor`. Some of these blocks can be + specified multiple times, so the following configuration: + + ```txt + audio_output { + device "iec958:CARD=Intel,DEV=0" + mixer_control "PCM" + name "My specific ALSA output" + type "alsa" + } + audio_output { + mixer_type "null" + name "ALSA Null" + type "alsa" + } + audio_output { + name "The Pulse" + type "pulse" + } + ``` + + Can be inserted with: + + ```nix + audio_output = [ + { + type = "alsa"; + name = "My specific ALSA output"; + device = "iec958:CARD=Intel,DEV=0"; + mixer_control = "PCM"; + } + { + type = "alsa"; + name = "ALSA Null"; + mixer_type = "null"; + } + { + type = "pulse"; + name = "The Pulse"; + } + ]; + ``` ''; }; @@ -226,7 +329,7 @@ in type = lib.types.bool; default = false; description = '' - If set, add fluidsynth soundfont and configure the plugin. + If set, add fluidsynth soundfont `decoder` block. ''; }; }; @@ -235,8 +338,47 @@ in ###### implementation + imports = [ + (lib.mkRenamedOptionModule + [ "services" "mpd" "musicDirectory" ] + [ "services" "mpd" "settings" "music_directory" ] + ) + (lib.mkRenamedOptionModule + [ "services" "mpd" "playlistDirectory" ] + [ "services" "mpd" "settings" "playlist_directory" ] + ) + (lib.mkRenamedOptionModule [ "services" "mpd" "dbFile" ] [ "services" "mpd" "settings" "db_file" ]) + (lib.mkRenamedOptionModule + [ "services" "mpd" "network" "listenAddress" ] + [ "services" "mpd" "settings" "bind_to_address" ] + ) + (lib.mkRenamedOptionModule + [ "services" "mpd" "network" "port" ] + [ "services" "mpd" "settings" "port" ] + ) + (lib.mkRemovedOptionModule + [ + "services" + "mpd" + "extraConfig" + ] + "services.mpd.extraConfig was replaced by the declarative services.mpd.settings option, per RFC42." + ) + ]; + config = lib.mkIf cfg.enable { + warnings = + lib.optional + ( + !(builtins.elem cfg.bind_to_address [ + "localhost" + "127.0.0.1" + ]) + && !cfg.openFirewall + ) + "Using '${cfg.bind_to_address}' as services.mpd.settings.bind_to_address without enabling services.mpd.openFirewall, might prevent you from accessing MPD from other clients."; + # install mpd units systemd.packages = [ pkgs.mpd ]; @@ -245,12 +387,12 @@ in listenStreams = [ "" # Note: this is needed to override the upstream unit ( - if pkgs.lib.hasPrefix "/" cfg.network.listenAddress then - cfg.network.listenAddress + if pkgs.lib.hasPrefix "/" cfg.settings.bind_to_address then + cfg.settings.bind_to_address else "${ - lib.optionalString (cfg.network.listenAddress != "any") "${cfg.network.listenAddress}:" - }${toString cfg.network.port}" + lib.optionalString (cfg.settings.bind_to_address != "any") "${cfg.settings.bind_to_address}:" + }${toString cfg.settings.port}" ) ]; }; @@ -282,17 +424,19 @@ in StateDirectory = [ ] ++ lib.optionals (cfg.dataDir == "/var/lib/${name}") [ name ] - ++ lib.optionals (cfg.playlistDirectory == "/var/lib/${name}/playlists") [ + ++ lib.optionals (cfg.settings.playlist_directory == "/var/lib/${name}/playlists") [ name "${name}/playlists" ] - ++ lib.optionals (cfg.musicDirectory == "/var/lib/${name}/music") [ + ++ lib.optionals (cfg.settings.music_directory == "/var/lib/${name}/music") [ name "${name}/music" ]; }; }; + networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall [ cfg.settings.port ]; + users.users = lib.optionalAttrs (cfg.user == name) { ${name} = { inherit uid; diff --git a/nixos/modules/services/audio/mpdscribble.nix b/nixos/modules/services/audio/mpdscribble.nix index 39dab7105968..cd874f000b3a 100644 --- a/nixos/modules/services/audio/mpdscribble.nix +++ b/nixos/modules/services/audio/mpdscribble.nix @@ -8,7 +8,6 @@ let cfg = config.services.mpdscribble; mpdCfg = config.services.mpd; - mpdOpt = options.services.mpd; endpointUrls = { "last.fm" = "http://post.audioscrobbler.com"; @@ -114,11 +113,11 @@ in host = lib.mkOption { default = ( - if mpdCfg.network.listenAddress != "any" then mpdCfg.network.listenAddress else "localhost" + if mpdCfg.settings.bind_to_address != "any" then mpdCfg.settings.bind_to_address else "localhost" ); defaultText = lib.literalExpression '' - if config.${mpdOpt.network.listenAddress} != "any" - then config.${mpdOpt.network.listenAddress} + if config.services.mpd.settings.bind_to_address != "any" + then config.services.mpd.settings.bind_to_address else "localhost" ''; type = lib.types.str; @@ -148,8 +147,8 @@ in }; port = lib.mkOption { - default = mpdCfg.network.port; - defaultText = lib.literalExpression "config.${mpdOpt.network.port}"; + default = mpdCfg.settings.port; + defaultText = lib.literalExpression "config.services.mpd.settings.port"; type = lib.types.port; description = '' Port for the mpdscribble daemon to search for a mpd daemon on. diff --git a/nixos/modules/services/audio/ympd.nix b/nixos/modules/services/audio/ympd.nix index 12672eb935f1..0999291ca0da 100644 --- a/nixos/modules/services/audio/ympd.nix +++ b/nixos/modules/services/audio/ympd.nix @@ -33,8 +33,8 @@ in port = lib.mkOption { type = lib.types.port; - default = config.services.mpd.network.port; - defaultText = lib.literalExpression "config.services.mpd.network.port"; + default = config.services.mpd.settings.port; + defaultText = lib.literalExpression "config.services.mpd.settings.port"; description = "The port where MPD is listening."; example = 6600; }; diff --git a/nixos/tests/mpd.nix b/nixos/tests/mpd.nix index f80c6658c621..36337bfbb4de 100644 --- a/nixos/tests/mpd.nix +++ b/nixos/tests/mpd.nix @@ -1,28 +1,18 @@ { pkgs, lib, ... }: let track = pkgs.fetchurl { - # Sourced from http://freemusicarchive.org/music/Blue_Wave_Theory/Surf_Music_Month_Challenge/Skyhawk_Beach_fade_in + # Sourced from https://freemusicarchive.org/music/Jazz_at_Mladost_Club/Jazz_Night/Blue_bossa/ - name = "Blue_Wave_Theory-Skyhawk_Beach.mp3"; - url = "https://freemusicarchive.org/file/music/ccCommunity/Blue_Wave_Theory/Surf_Music_Month_Challenge/Blue_Wave_Theory_-_04_-_Skyhawk_Beach.mp3"; - hash = "sha256-91VDWwrcP6Cw4rk72VHvZ8RGfRBrpRE8xo/02dcJhHc="; + name = "Blue bossa - Jazz at Miadost Club.mp3"; + url = "https://files.freemusicarchive.org/storage-freemusicarchive-org/music/no_curator/Jazz_at_Mladost_Club/Jazz_Night/Jazz_at_Mladost_Club_-_07_-_Blue_bossa.mp3?download=1&name=Jazz%20at%20Mladost%20Club%20-%20Blue%20bossa.mp3"; + hash = "sha256-cAG4nBuc97J3ZJc9cm/6vWTgnPL/Hfkar7cA3+89rto="; meta.license = lib.licenses.cc-by-sa-40; }; - defaultCfg = rec { + defaultMpdCfg = { user = "mpd"; group = "mpd"; dataDir = "/var/lib/mpd"; - musicDirectory = "${dataDir}/music"; - }; - - defaultMpdCfg = { - inherit (defaultCfg) - dataDir - musicDirectory - user - group - ; enable = true; }; @@ -30,7 +20,7 @@ let { user, group, - musicDirectory, + dataDir, }: { description = "Sets up the music file(s) for MPD to use."; @@ -38,7 +28,7 @@ let after = [ "mpd.service" ]; wantedBy = [ "default.target" ]; script = '' - cp ${track} ${musicDirectory} + cp ${track} ${dataDir}/music ''; serviceConfig = { User = user; @@ -57,7 +47,10 @@ in { name = "mpd"; meta = { - maintainers = with lib.maintainers; [ emmanuelrosa ]; + maintainers = with lib.maintainers; [ + emmanuelrosa + doronbehar + ]; }; nodes = { @@ -68,18 +61,20 @@ in lib.mkMerge [ (mkServer { mpd = defaultMpdCfg // { - network.listenAddress = "any"; - extraConfig = '' - audio_output { - type "alsa" - name "ALSA" - mixer_type "null" - } - ''; + settings = { + bind_to_address = "any"; + audio_output = [ + { + type = "alsa"; + name = "ALSA"; + mixer_type = "null"; + } + ]; + }; + openFirewall = true; }; - musicService = musicService { inherit (defaultMpdCfg) user group musicDirectory; }; + musicService = musicService { inherit (defaultMpdCfg) user group dataDir; }; }) - { networking.firewall.allowedTCPPorts = [ 6600 ]; } ]; serverPulseAudio = @@ -87,15 +82,15 @@ in lib.mkMerge [ (mkServer { mpd = defaultMpdCfg // { - extraConfig = '' - audio_output { - type "pulse" - name "The Pulse" + settings.audio_output = [ + { + type = "pulse"; + name = "The Pulse"; } - ''; + ]; }; - musicService = musicService { inherit (defaultMpdCfg) user group musicDirectory; }; + musicService = musicService { inherit (defaultMpdCfg) user group dataDir; }; }) { services.pulseaudio = { @@ -144,5 +139,8 @@ in # The PulseAudio-based server is configured not to accept external client connections # to perform the following test: client.fail(f"{mpc} -h serverPulseAudio status") + # For inspecting these files + serverALSA.copy_from_vm("/run/mpd/mpd.conf", "ALSA") + serverPulseAudio.copy_from_vm("/run/mpd/mpd.conf", "PulseAudio") ''; } diff --git a/pkgs/by-name/mp/mpd/package.nix b/pkgs/by-name/mp/mpd/package.nix index 661f95ed122b..0620c6e60c71 100644 --- a/pkgs/by-name/mp/mpd/package.nix +++ b/pkgs/by-name/mp/mpd/package.nix @@ -232,8 +232,8 @@ stdenv.mkDerivation (finalAttrs: { (stdenv.hostPlatform.isDarwin && lib.versionOlder stdenv.hostPlatform.darwinSdkVersion "12.0") '' substituteInPlace src/output/plugins/OSXOutputPlugin.cxx \ - --replace kAudioObjectPropertyElement{Main,Master} \ - --replace kAudioHardwareServiceDeviceProperty_Virtual{Main,Master}Volume + --replace-fail kAudioObjectPropertyElement{Main,Master} \ + --replace-fail kAudioHardwareServiceDeviceProperty_Virtual{Main,Master}Volume '' + lib.optionalString @@ -262,17 +262,19 @@ stdenv.mkDerivation (finalAttrs: { ]; mesonFlags = [ - "-Dtest=true" - "-Dmanpages=true" - "-Dhtml_manual=true" + (lib.mesonBool "test" true) + (lib.mesonBool "manpages" true) + (lib.mesonBool "html_manual" true) ] - ++ map (x: "-D${x}=enabled") features_ - ++ map (x: "-D${x}=disabled") (lib.subtractLists features_ knownFeatures) + ++ map (x: lib.mesonEnable x true) features_ + ++ map (x: lib.mesonEnable x false) (lib.subtractLists features_ knownFeatures) ++ lib.optional (builtins.elem "zeroconf" features_) ( - "-Dzeroconf=" + (if stdenv.hostPlatform.isDarwin then "bonjour" else "avahi") + lib.mesonOption "zeroconf" (if stdenv.hostPlatform.isDarwin then "bonjour" else "avahi") ) - ++ lib.optional (builtins.elem "systemd" features_) "-Dsystemd_system_unit_dir=etc/systemd/system" - ++ lib.optional (builtins.elem "qobuz" features_) "-Dnlohmann_json=enabled"; + ++ lib.optional (builtins.elem "systemd" features_) ( + lib.mesonOption "systemd_system_unit_dir" "etc/systemd/system" + ) + ++ lib.optional (builtins.elem "qobuz" features_) (lib.mesonEnable "nlohmann_json" true); passthru.tests.nixos = nixosTests.mpd; @@ -282,6 +284,7 @@ stdenv.mkDerivation (finalAttrs: { license = lib.licenses.gpl2Only; maintainers = with lib.maintainers; [ tobim + doronbehar ]; platforms = lib.platforms.unix; mainProgram = "mpd";