From 68e6fb9603abd209d920c86281e6ccafb32bd2d4 Mon Sep 17 00:00:00 2001 From: Chris Moultrie <821688+tebriel@users.noreply.github.com> Date: Wed, 15 Apr 2026 09:07:08 -0400 Subject: [PATCH] navidrome: introduce plugin infrastructure --- nixos/modules/services/audio/navidrome.nix | 62 ++++++++++++++++- nixos/tests/navidrome.nix | 4 +- pkgs/by-name/na/navidrome/package.nix | 11 +++ pkgs/by-name/na/navidrome/plugins/README.md | 8 +++ .../na/navidrome/plugins/build-plugin.nix | 67 +++++++++++++++++++ pkgs/top-level/all-packages.nix | 11 +++ 6 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 pkgs/by-name/na/navidrome/plugins/README.md create mode 100644 pkgs/by-name/na/navidrome/plugins/build-plugin.nix diff --git a/nixos/modules/services/audio/navidrome.nix b/nixos/modules/services/audio/navidrome.nix index 8fa7e6dc3db5..47f7a17162e9 100644 --- a/nixos/modules/services/audio/navidrome.nix +++ b/nixos/modules/services/audio/navidrome.nix @@ -7,13 +7,18 @@ let inherit (lib) + literalExpression + mkDefault mkEnableOption mkPackageOption mkOption maintainers ; inherit (lib.types) + addCheck bool + listOf + package port str submodule @@ -29,6 +34,36 @@ in package = mkPackageOption pkgs "navidrome" { }; + plugins = mkOption { + type = listOf ( + addCheck package (p: p.isNavidromePlugin or false) + // { + name = "navidrome plugin"; + description = "package that is a navidrome plugin"; + } + ); + default = [ ]; + description = "List of Navidrome plugins"; + example = literalExpression '' + with pkgs.navidromePlugins; [ + ]; + ''; + }; + + finalPackage = mkOption { + type = package; + readOnly = true; + default = cfg.package.override { + inherit (cfg) plugins; + }; + defaultText = literalExpression '' + config.services.navidrome.package.override { + inherit (config.services.navidrome) plugins; + } + ''; + description = "The final navidrome package including all selected plugins."; + }; + settings = mkOption { type = submodule { freeformType = settingsFormat.type; @@ -51,6 +86,29 @@ in description = "Enable anonymous usage data collection, see for details."; type = bool; }; + + Plugins = { + Enabled = mkOption { + default = (builtins.length cfg.plugins) != 0; + defaultText = literalExpression "builtins.length \"\${config.services.navidrome.plugins != 0}\""; + description = '' + Enable plugin support in navidrome. + + This is automatically enabled if {option}`services.navidrome.plugins` is used. + ''; + }; + Folder = mkOption { + default = "${cfg.finalPackage}/share/plugins"; + description = '' + The folder containing navidrome plugins. + + This directory is automatically created from plugins defined in {option}`services.navidrome.plugins`. + ''; + readOnly = true; + type = str; + defaultText = literalExpression "\"\${config.services.navidrome.finalPackage}/share/plugins\""; + }; + }; }; }; default = { }; @@ -78,7 +136,7 @@ in description = "Whether to open the TCP port in the firewall"; }; - environmentFile = lib.mkOption { + environmentFile = mkOption { type = lib.types.nullOr lib.types.path; default = null; description = "Environment file, used to set any secret ND_* environment variables."; @@ -114,7 +172,7 @@ in wantedBy = [ "multi-user.target" ]; serviceConfig = { ExecStart = '' - ${getExe cfg.package} --configfile ${settingsFormat.generate "navidrome.json" cfg.settings} + ${getExe cfg.finalPackage} --configfile ${settingsFormat.generate "navidrome.json" cfg.settings} ''; EnvironmentFile = lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ]; User = cfg.user; diff --git a/nixos/tests/navidrome.nix b/nixos/tests/navidrome.nix index e57cec0d2830..1ae2ac19af13 100644 --- a/nixos/tests/navidrome.nix +++ b/nixos/tests/navidrome.nix @@ -5,7 +5,9 @@ nodes.machine = { ... }: { - services.navidrome.enable = true; + services.navidrome = { + enable = true; + }; }; testScript = '' diff --git a/pkgs/by-name/na/navidrome/package.nix b/pkgs/by-name/na/navidrome/package.nix index 242bd807785c..034239e25f58 100644 --- a/pkgs/by-name/na/navidrome/package.nix +++ b/pkgs/by-name/na/navidrome/package.nix @@ -16,6 +16,7 @@ nix-update-script, ffmpegSupport ? true, versionCheckHook, + plugins ? [ ], }: buildGoModule (finalAttrs: { @@ -46,6 +47,8 @@ buildGoModule (finalAttrs: { pkg-config ]; + runtimeInputs = plugins; + overrideModAttrs = oldAttrs: { nativeBuildInputs = lib.filter (drv: drv != npmHooks.npmConfigHook) oldAttrs.nativeBuildInputs; preBuild = null; @@ -77,6 +80,13 @@ buildGoModule (finalAttrs: { make buildjs ''; + postInstall = '' + mkdir -p $out/share/plugins/ + ${lib.concatMapStringsSep "\n" (plugin: '' + ln -s ${plugin}/share/${plugin.pname}.ndp $out/share/plugins/ + '') plugins} + ''; + tags = [ "netgo" "sqlite_fts5" @@ -91,6 +101,7 @@ buildGoModule (finalAttrs: { ''; passthru = { + inherit plugins; tests.navidrome = nixosTests.navidrome; updateScript = nix-update-script { }; }; diff --git a/pkgs/by-name/na/navidrome/plugins/README.md b/pkgs/by-name/na/navidrome/plugins/README.md new file mode 100644 index 000000000000..a59ba3caa74a --- /dev/null +++ b/pkgs/by-name/na/navidrome/plugins/README.md @@ -0,0 +1,8 @@ +# Packaging guidelines + +## Basics + +Each navidrome plugin is a `.zip` file named `${pname}.ndp`. Inside this zip is +two files named exactly: +- `manifest.json` which defines permissions and configuration +- `plugin.wasm` which is the output of the plugin build diff --git a/pkgs/by-name/na/navidrome/plugins/build-plugin.nix b/pkgs/by-name/na/navidrome/plugins/build-plugin.nix new file mode 100644 index 000000000000..ae91384401c5 --- /dev/null +++ b/pkgs/by-name/na/navidrome/plugins/build-plugin.nix @@ -0,0 +1,67 @@ +{ + buildGoModule, + lib, + navidrome, + zip, +}: + +{ + pname, + src, + version, + vendorHash, + meta, + ... +}@args: + +buildGoModule ( + finalAttrs: + { + inherit + pname + version + src + vendorHash + ; + + env = { + CGO_ENABLED = "0"; + } + // (args.env or { }); + + postBuild = '' + GOOS=wasip1 \ + GOARCH=wasm \ + go build \ + -buildmode=c-shared \ + -o $GOPATH/bin/plugin.wasm . + ''; + + postInstall = '' + mkdir $out/share + pushd $(mktemp -d) + cp $GOPATH/bin/plugin.wasm . + cp ${finalAttrs.src}/manifest.json . + ${lib.getExe zip} \ + $out/share/${finalAttrs.pname}.ndp \ + plugin.wasm \ + manifest.json + popd + rm -r $out/bin + ''; + + passthru = { + isNavidromePlugin = true; + } + // args.passthru or { }; + + meta = { + inherit (navidrome.meta) platforms maintainers; + } + // args.meta or { }; + } + // removeAttrs args [ + "meta" + "passthru" + ] +) diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index e2857a88f812..1013e783e5db 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -2686,6 +2686,17 @@ with pkgs; nanoemoji = with python3Packages; toPythonApplication nanoemoji; + buildNavidromePlugin = callPackage ../by-name/na/navidrome/plugins/build-plugin.nix { }; + navidromePlugins = recurseIntoAttrs ( + lib.makeExtensible ( + self: + lib.packagesFromDirectoryRecursive { + inherit callPackage; + directory = ../by-name/na/navidrome/plugins; + } + ) + ); + netdata = callPackage ../tools/system/netdata { protobuf = protobuf_21; };