From d7271d466917678f7485c0e1ead1d09f6b27eb4c Mon Sep 17 00:00:00 2001 From: Jonas Rabenstein Date: Sat, 20 Sep 2025 13:34:01 +0200 Subject: [PATCH] rework layout --- flake.nix | 84 +++++---- module/plugins.nix | 47 ++++-- package/jellyfin.nix | 159 ------------------ package/jellyfin/default.nix | 19 +++ .../ignore-files-with-no-audio-stream.patch | 26 +++ package/jellyfin/plugin.nix | 153 +++++++++++++++++ package/{jprm.nix => jprm/default.nix} | 0 plugin/dlna/default.nix | 2 +- 8 files changed, 283 insertions(+), 207 deletions(-) delete mode 100644 package/jellyfin.nix create mode 100644 package/jellyfin/default.nix create mode 100644 package/jellyfin/ignore-files-with-no-audio-stream.patch create mode 100644 package/jellyfin/plugin.nix rename package/{jprm.nix => jprm/default.nix} (100%) diff --git a/flake.nix b/flake.nix index f185360..d222f21 100644 --- a/flake.nix +++ b/flake.nix @@ -20,43 +20,59 @@ pkgs' = pkgs: pkgs.extend overlay; + scan'directory = scan (name: type: if type == "directory" then name else null); + scan'regular = scan (name: type: if type == "regular" then name else null); + scan'nix = + let + stem = lib.strings.removeSuffix ".nix"; + in + scan (name: type: if type == "regular" && name == "${stem name}.nix" then stem name else null); + per-pkgs = fn: builtins.mapAttrs (_: fn) nixpkgs.legacyPackages; scan = - base: fn: + filter: fn: base: let + empty = { }; + create = + name: entry: + lib.optionalAttrs (name != null) { + ${name} = fn name (base + "/${entry}"); + }; fold = - acc: entry: kind: - let - name = lib.strings.removeSuffix ".nix" entry; - attr = lib.optionalAttrs (lib.strings.hasSuffix ".nix" entry) { - ${name} = fn name "${base}/${entry}"; - }; - in - acc // attr; + acc: name: kind: + acc // create (filter name kind) name; files = lib.optionalAttrs (builtins.pathExists base) (builtins.readDir base); in - lib.attrsets.foldlAttrs fold { } files; + lib.attrsets.foldlAttrs fold empty files; packages = - pkgs: scan ./package (name: def: (pkgs' pkgs).callPackage def { original = pkgs.${name} or null; }); + pkgs: + let + package = + name: path: + let + pkg = (pkgs' pkgs).callPackage path { + original = pkgs.${name} or null; + }; + patches = scan (name: type: if lib.strings.removeSuffix ".patch" name != name then name else null) ( + name: path: path + ) path; + in + pkg.overrideAttrs ( + final: prev: { + patches = (prev.patches or [ ]) ++ builtins.attrValues patches; + } + ); + + in + scan'directory package ./package; + plugins = pkgs: let - base = ./plugin; - result = lib.attrsets.foldlAttrs ( - acc: name: kind: - acc - // lib.optionalAttrs (kind == "directory") { - ${name} = (pkgs' pkgs).jellyfin.plugin (base + "/${name}") { }; - } - ) { } (builtins.readDir base); + plugin = name: path: (pkgs' pkgs).jellyfin.plugin path { }; in - if builtins.pathExists base then result else { }; - - module = defs: { - imports = lib.toList defs; - config.nixpkgs.overlays = [ overlay ]; - }; + scan'directory plugin ./plugin; in { @@ -73,14 +89,20 @@ nixosModules = let - modules = scan ./module ( - _: def: { + directories = scan'directory (_: path: path) ./module; + files = scan'nix (_: path: path) ./module; + modules = directories // { + default.imports = builtins.attrValues files ++ (directories.default or [ ]); + }; + module = + name: defs: + { ... }: + builtins.trace "module: jellyfin.${name}" { + imports = lib.toList defs; config.nixpkgs.overlays = [ overlay ]; - imports = [ def ]; - } - ); + }; in - { default.imports = builtins.attrValues modules; } // modules; + builtins.mapAttrs module modules; formatter = per-pkgs ({ nixfmt-tree, ... }: nixfmt-tree); }; diff --git a/module/plugins.nix b/module/plugins.nix index 31937e3..c75986f 100644 --- a/module/plugins.nix +++ b/module/plugins.nix @@ -7,9 +7,9 @@ let cfg = config.services.jellyfin; - plugin = name: pks."jellyfin-plugin-${name}"; + plugin = name: pkgs."jellyfin-plugin-${name}"; - per-file = pkg: fn: lib.lists.foldl (acc: dll: acc ++ (fn dll)) pkg.pluginLibraries; + per-file = pkg: fn: lib.lists.foldl (acc: dll: acc ++ lib.toList (fn dll)) [ ] pkg.pluginLibraries; serviceConfig = pkg: @@ -23,11 +23,11 @@ let in { BindReadOnlyPaths = per-file pkg ( - name: "${pkg}/${name}:${cfg.dataDir}/plugins/${pkg.name}/${name}" - ); - BindPaths = per-file pkg ( - name: lib.optional (rw name) "${cfg.dataDir}/plugins/${pkg.name}/${name}" + name: "${pkg}/${name}.dll:${cfg.dataDir}/plugins/${pkg.name}/${name}.dll" ); + BindPaths = [ + "${cfg.dataDir}/plugins/${pkg.name}/meta.json" + ]; }; type.plugin = lib.types.addCheck lib.types.package (builtins.hasAttr "pluginLibraries"); @@ -35,27 +35,42 @@ in { options.services.jellyfin.plugins = lib.mkOption { type = lib.types.listOf (lib.types.coercedTo lib.types.nonEmptyStr plugin type.plugin); - default = { }; + default = [ ]; }; config.systemd = lib.mkIf (cfg.plugins != { }) { - services.jellyfin.serviceConfig = lib.mkMerge ( - [ - { TemporaryFileSystem = [ "${cfg.dataDir}/plugins:ro" ]; } - { BindPaths = [ "${cfg.dataDir}/plugins/configurations/" ]; } - ] - ++ builtins.map serviceConfig cfg.plugins - ); + services.jellyfin.serviceConfig = + let + plugins = pkgs.linkFarm "jellyfin-plugins" ( + lib.lists.foldl ( + acc: pkg: + acc + ++ per-file pkg (dll: { + name = "${pkg.name}/${dll}.dll"; + path = pkg + "/${dll}.dll"; + }) + ) [ ] cfg.plugins + ); + in + { + BindReadOnlyPaths = [ + "${plugins}:${cfg.dataDir}/plugins" + ]; + BindPaths = [ + "${cfg.dataDir}/plugins/configurations/" + ] + ++ builtins.map (pkg: "${cfg.dataDir}/pluins/${pkg.name}/meta.json") cfg.plugins; + }; tmpfiles.settings = lib.lists.foldl ( acc: pkg: acc // { - ${pkg.name}."${cfg.dataDir}/plugins/${pkgs.name}/${name}".C = { + ${pkg.name}."${cfg.dataDir}/plugins/${pkg.name}/meta.json".C = { group = cfg.group; user = cfg.user; mode = "0700"; - argument = "${pkg}/${name}"; + argument = "${pkg}/meta.json"; }; } ) { } cfg.plugins; diff --git a/package/jellyfin.nix b/package/jellyfin.nix deleted file mode 100644 index 14728a3..0000000 --- a/package/jellyfin.nix +++ /dev/null @@ -1,159 +0,0 @@ -{ - lib, - original, - callPackage, - dotnetCorePackages, - fetchFromGitHub, - buildDotnetModule, - gnused, - jprm, - unzip, -}: -let - capitalize = - upper: str: - let - length = builtins.stringLength str; - head = builtins.substring 0 1 str; - tail = builtins.substring 1 length str; - in - "${upper head}${tail}"; - jellyfin = original.overrideAttrs ( - final: prev: - assert !prev ? "plugin"; - { - passthru.plugin = - base: args: - let - meta = from: { inherit (from) license homepage description; }; - - helper = { - inherit base; - name = builtins.baseNameOf info.base; - self = info; - owner = "jellyfin"; - hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; - rev = "v${info.version}"; - description = "${info.name} plugin for jellyfin"; - homepage = original.meta.homepage; - license = original.meta.license; - override = - assert false; - null; - overrideAttrs = - assert false; - null; - overrideDerivation = - assert false; - null; - mkPlugin = info: result: buildDotnetModule result; - inherit jellyfin; - }; - - defaults = { - pname = "jellyfin-plugin-${info.name}"; - nugetDeps = - let - options = builtins.filter builtins.pathExists [ - "${info.base}/deps.json" - ]; - in - assert options != [ ]; - builtins.head options; - pluginLibraries = lib.attrsets.foldlAttrs ( - acc: name: value: - acc ++ lib.optional (value == "directory") name - ) [ ] (builtins.readDir ("${info.src}/src")); - dotnet-sdk = dotnetCorePackages.sdk_8_0; - dotnet-runtime = dotnetCorePackages.aspnetcore_8_0; - dontDotnetBuild = true; - dontDotnetInstall = true; - project = "Jellyfin.Plugin.${capitalize lib.strings.toUpper info.name}"; - prePatch = '' - sed --sandbox --separate \ - -e 's:\(PackageReference Include="Jellyfin\..*" Version="\)[^"]\+":\1${info.jellyfin.version}":' \ - -e 's:<\(enerateDocumentationFile\|TreatWarningsAsErrors\)>true:<\1>false:' \ - -i ${ - lib.strings.escapeShellArgs (builtins.map (lib: "src/${lib}/${lib}.csproj") info.pluginLibraries) - } - - success=true - for x in ${ - lib.strings.escapeShellArgs (builtins.map (lib: "src/${lib}/${lib}.csproj") info.pluginLibraries) - } - do - diff -q $src/$x $x 2>/dev/null || continue - printf >&2 'no change: %s\n' $x - success=false - done - $success || exit 1 - ''; - projectFile = "src/${info.project}/${info.project}.csproj"; - src = fetchFromGitHub { - owner = info.owner; - repo = "jellyfin-plugin-${info.name}"; - inherit (info) rev hash; - }; - nativeBuildInputs = [ - gnused - jprm - unzip - ]; - patches = - lib.optional (builtins.pathExists "${info.base}.patch") "${info.base}.patch" - ++ lib.optionals (builtins.pathExists info.base) ( - lib.attrsets.foldlAttrs ( - acc: name: type: - acc - ++ lib.optional (type == "regular" && lib.strings.hasSuffix ".patch" name) "${info.base}/${name}" - ) [ ] (builtins.readDir info.base) - ); - outputs = [ - "out" - "zip" - ]; - postInstall = - let - dlls = builtins.map (name: "${name}.dll") info.pluginLibraries; - in - '' - tmp_output_dir="$(mktemp -d)" - jprm plugin build . --output="''${tmp_output_dir}" --version="${info.version}" --dotnet-configuration="''${dotnetBuildType-Release}" - mv "''${tmp_output_dir}/${info.name}_${info.version}.zip" $zip - mkdir -p $out - unzip $zip -d $out - - success=true - for file in $out/*; - do - case "''${file##*/}" in - meta.json) - ;; - ${builtins.concatStringsSep "|" (builtins.map lib.strings.escapeShellArg dlls)}) ;; - *) - printf 'unknown file: %s\n' ''${file@Q} - success=false - ;; - esac - done - - for file in meta.json ${lib.strings.escapeShellArgs dlls} - do - [[ -f "$out/$file" ]] && continue - printf 'missing file: %s\n' ''${file@Q} - success=false - done - - $success || exit 42 - ''; - meta = meta original.meta // meta info; - }; - - info = helper // defaults // callPackage base ({ inherit info; } // args); - result = lib.attrsets.removeAttrs info (builtins.attrNames helper); - in - info.mkPlugin info result; - } - ); -in -jellyfin diff --git a/package/jellyfin/default.nix b/package/jellyfin/default.nix new file mode 100644 index 0000000..29532a8 --- /dev/null +++ b/package/jellyfin/default.nix @@ -0,0 +1,19 @@ +{ + original, + callPackage, +}: +let + jellyfin = original.overrideAttrs ( + final: prev: + assert !prev ? "plugin"; + { + passthru.plugin = + base: + callPackage ./plugin.nix { + plugin = base; + jellyfin = jellyfin; + }; + } + ); +in +jellyfin diff --git a/package/jellyfin/ignore-files-with-no-audio-stream.patch b/package/jellyfin/ignore-files-with-no-audio-stream.patch new file mode 100644 index 0000000..1ae41a2 --- /dev/null +++ b/package/jellyfin/ignore-files-with-no-audio-stream.patch @@ -0,0 +1,26 @@ +diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs +index 1801db70d..9fe792b26 100644 +--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs ++++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs +@@ -100,6 +100,11 @@ namespace MediaBrowser.Model.Dlna + } + + MediaStream audioStream = item.GetDefaultAudioStream(null); ++ if (audioStream is null) ++ { ++ _logger.LogError("No audio stream for {0}", item.Path ?? ""); ++ return null; ++ } + + var directPlayInfo = GetAudioDirectPlayProfile(item, audioStream, options); + +@@ -434,6 +439,9 @@ namespace MediaBrowser.Model.Dlna + TranscodeReason transcodeReasons = 0; + if (directPlayProfile is null) + { ++ _logger.LogDebug("Profile: {0}", options.Profile.Name ?? ""); ++ _logger.LogDebug("Path: {0}", item.Path ?? ""); ++ _logger.LogDebug("Codec: {0}", audioStream.Codec ?? ""); + _logger.LogDebug( + "Profile: {0}, No audio direct play profiles found for {1} with codec {2}", + options.Profile.Name ?? "Unknown Profile", diff --git a/package/jellyfin/plugin.nix b/package/jellyfin/plugin.nix new file mode 100644 index 0000000..f3094f9 --- /dev/null +++ b/package/jellyfin/plugin.nix @@ -0,0 +1,153 @@ +{ + lib, + jellyfin, + plugin, + fetchFromGitHub, + buildDotnetModule, + gnused, + jprm, + unzip, + dotnetCorePackages, + callPackage, +}: +let + drv = + args: + let + meta = from: { inherit (from) license homepage description; }; + + capitalize = + upper: str: + let + length = builtins.stringLength str; + head = builtins.substring 0 1 str; + tail = builtins.substring 1 length str; + in + "${upper head}${tail}"; + + helper = { + base = plugin; + name = builtins.baseNameOf info.base; + self = info; + owner = "jellyfin"; + repo = "jellyfin-plugin-${info.name}"; + hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + rev = "v${info.version}"; + description = "${info.name} plugin for jellyfin"; + homepage = jellyfin.meta.homepage; + license = jellyfin.meta.license; + mkPlugin = info: result: buildDotnetModule result; + inherit jellyfin; + ignore = builtins.attrNames helper ++ [ + "override" "overrideAttrs" "overrideDerivation" + ]; + }; + + defaults = { + pname = "jellyfin-plugin-${info.name}"; + nugetDeps = + let + options = builtins.filter builtins.pathExists [ + "${info.base}/deps.json" + ]; + in + assert options != [ ]; + builtins.head options; + pluginLibraries = lib.attrsets.foldlAttrs ( + acc: name: value: + acc ++ lib.optional (value == "directory") name + ) [ ] (builtins.readDir ("${info.src}/src")); + dotnet-sdk = dotnetCorePackages.sdk_8_0; + dotnet-runtime = dotnetCorePackages.aspnetcore_8_0; + dontDotnetBuild = true; + dontDotnetInstall = true; + project = "Jellyfin.Plugin.${capitalize lib.strings.toUpper info.name}"; + prePatch = '' + sed --sandbox --separate \ + -e 's:\(PackageReference Include="Jellyfin\..*" Version="\)[^"]\+":\1${info.jellyfin.version}":' \ + -e 's:<\(enerateDocumentationFile\|TreatWarningsAsErrors\)>true:<\1>false:' \ + -i ${ + lib.strings.escapeShellArgs (builtins.map (lib: "src/${lib}/${lib}.csproj") info.pluginLibraries) + } + + success=true + for x in ${ + lib.strings.escapeShellArgs (builtins.map (lib: "src/${lib}/${lib}.csproj") info.pluginLibraries) + } + do + diff -q $src/$x $x 2>/dev/null || continue + printf >&2 'no change: %s\n' $x + success=false + done + $success || exit 1 + ''; + projectFile = "src/${info.project}/${info.project}.csproj"; + src = fetchFromGitHub { + owner = info.owner; + repo = info.repo; + inherit (info) rev hash; + }; + nativeBuildInputs = [ + gnused + jprm + unzip + ]; + patches = + lib.optional (builtins.pathExists "${info.base}.patch") "${info.base}.patch" + ++ lib.optionals (builtins.pathExists info.base) ( + lib.attrsets.foldlAttrs ( + acc: name: type: + acc + ++ lib.optional (type == "regular" && lib.strings.hasSuffix ".patch" name) "${info.base}/${name}" + ) [ ] (builtins.readDir info.base) + ); + outputs = [ + "out" + "zip" + ]; + postInstall = + let + dlls = builtins.map (name: "${name}.dll") info.pluginLibraries; + in + '' + tmp_output_dir="$(mktemp -d)" + jprm plugin build . --output="''${tmp_output_dir}" --version="${info.version}" --dotnet-configuration="''${dotnetBuildType-Release}" + mv "''${tmp_output_dir}/${info.name}_${info.version}.zip" $zip + mkdir -p $out + unzip $zip -d $out + + success=true + for file in $out/*; + do + case "''${file##*/}" in + meta.json) + ;; + ${builtins.concatStringsSep "|" (builtins.map lib.strings.escapeShellArg dlls)}) ;; + *) + printf 'unknown file: %s\n' ''${file@Q} + success=false + ;; + esac + done + + for file in meta.json ${lib.strings.escapeShellArgs dlls} + do + [[ -f "$out/$file" ]] && continue + printf 'missing file: %s\n' ''${file@Q} + success=false + done + + $success || exit 42 + ''; + meta = meta jellyfin.meta // meta info; + }; + info = helper // defaults // callPackage plugin ({ inherit info; } // args); + in info.mkPlugin info (lib.attrsets.removeAttrs info info.ignore); + + functor = args: (drv args).overrideAttrs (final: prev: { + passthru = builtins.trace "override passthru" (prev.passthru or {}) // { + in-version = { version, rev ? null, hash ? null }@args': + callPackage functor (builtins.removeAttrs args [ "version" "rev" "hash" ] // args'); + }; + }); +in lib.trivial.mirrorFunctionArgs plugin functor diff --git a/package/jprm.nix b/package/jprm/default.nix similarity index 100% rename from package/jprm.nix rename to package/jprm/default.nix diff --git a/plugin/dlna/default.nix b/plugin/dlna/default.nix index 62c1bd1..69dcd40 100644 --- a/plugin/dlna/default.nix +++ b/plugin/dlna/default.nix @@ -12,7 +12,7 @@ in { lib, version ? current lib, - hash ? v.${version}.hash or "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + hash ? v.${version}.hash, rev ? v.${version}.rev or "v${version}", ... }: