This commit is contained in:
Jonas Rabenstein 2025-11-29 18:14:10 +01:00
commit 794925acb2
15 changed files with 516 additions and 529 deletions

View file

@ -2,7 +2,7 @@
description = "A very basic flake"; description = "A very basic flake";
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
}; };
outputs = outputs =
@ -10,16 +10,6 @@
let let
inherit (nixpkgs) lib; inherit (nixpkgs) lib;
overlay =
final: pkgs:
(packages pkgs)
// lib.attrsets.mapAttrs' (name: value: {
name = "jellyfin-plugin-${name}";
inherit value;
}) (plugins final);
pkgs' = pkgs: pkgs.extend overlay;
scan'directory = scan (name: type: if type == "directory" then name else null); 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'regular = scan (name: type: if type == "regular" then name else null);
scan'nix = scan'nix =
@ -45,62 +35,42 @@
in in
lib.attrsets.foldlAttrs fold empty files; lib.attrsets.foldlAttrs fold empty files;
packages = scope = pkgs: lib.makeScope pkgs.newScope (
pkgs: self:
let lib.packagesFromDirectoryRecursive {
package = callPackage = self.callPackage;
name: path: directory = ./package;
let }
pkg = pkgs.lib.callPackageWith ((pkgs' pkgs) // { original = pkgs.${name} or null; }) path {}; // {
patches = scan (name: type: if lib.strings.removeSuffix ".patch" name != name then name else null) ( jellyfin = (
name: path: path self.callPackage ./package/jellyfin/package.nix {
) path; jellyfin = pkgs.jellyfin;
in
pkg.overrideAttrs (
final: prev: {
patches = (prev.patches or [ ]) ++ builtins.attrValues patches;
} }
); );
}
in //
scan'directory package ./package; lib.attrsets.mapAttrs'
(name: value: {
plugins = name = "jellyfin-plugin-${name}";
pkgs: value = value;
let })
plugin = name: path: (pkgs' pkgs).jellyfin.plugin path { }; (
in lib.packagesFromDirectoryRecursive {
scan'directory plugin ./plugin; callPackage = self.callPackage;
directory = ./plugin;
in }
{ )
overlays.default = overlay;
packages = per-pkgs (
pkgs:
(packages pkgs)
// lib.attrsets.mapAttrs' (name: value: {
name = "plugin-${name}";
inherit value;
}) (plugins pkgs)
); );
nixosModules =
let
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:
{ ... }:
{
imports = lib.toList defs;
config.nixpkgs.overlays = [ overlay ];
};
in in
builtins.mapAttrs module modules; {
overlay = final: scope;
legacyPackages = per-pkgs scope;
packages = per-pkgs (pkgs: lib.filterAttrs (_: lib.isDerivation) (scope pkgs));
nixosModules.default = import ./module/default.nix scope;
formatter = per-pkgs ({ nixfmt-tree, ... }: nixfmt-tree); formatter = per-pkgs ({ nixfmt-tree, ... }: nixfmt-tree);
}; };

View file

@ -1,15 +1,18 @@
scope:
{ {
lib,
pkgs, pkgs,
config, config,
lib,
... ...
}: }:
let let
cfg = config.services.jellyfin; cfg = config.services.jellyfin;
plugin = name: pkgs."jellyfin-plugin-${name}"; plugin = name: (scope pkgs)."jellyfin-plugin-${name}".override {
jellyfin = cfg.package;
};
per-file = pkg: fn: lib.lists.foldl (acc: dll: acc ++ lib.toList (fn dll)) [ ] pkg.pluginLibraries; libs = pkg: fn: lib.lists.foldl (acc: lib: acc ++ lib.toList (fn lib)) [ ] pkg.pluginLibraries;
serviceConfig = serviceConfig =
pkg: pkg:
@ -22,15 +25,13 @@ let
''; '';
in in
{ {
BindReadOnlyPaths = per-file pkg ( BindReadOnlyPaths = map (name: "${pkg}/${name}:${cfg.dataDir}/plugins/${pkg.name}/${name}") pkg.jellyfinPluginFiles;
name: "${pkg}/${name}.dll:${cfg.dataDir}/plugins/${pkg.name}/${name}.dll"
);
BindPaths = [ BindPaths = [
"${cfg.dataDir}/plugins/${pkg.name}/meta.json" "${cfg.dataDir}/plugins/${pkg.name}/meta.json"
]; ];
}; };
type.plugin = lib.types.addCheck lib.types.package (builtins.hasAttr "pluginLibraries"); type.plugin = lib.types.addCheck lib.types.package (builtins.hasAttr "jellyfinPluginFiles");
in in
{ {
options.services.jellyfin.plugins = lib.mkOption { options.services.jellyfin.plugins = lib.mkOption {
@ -41,7 +42,7 @@ in
config.systemd = lib.mkIf (cfg.plugins != { }) { config.systemd = lib.mkIf (cfg.plugins != { }) {
services.jellyfin.serviceConfig = lib.mkMerge ( services.jellyfin.serviceConfig = lib.mkMerge (
[ [
{ TemporaryFileSystem = [ "${cfg.dataDir}/plugins:ro" ]; } { BindPaths = [ "${cfg.dataDir}/plugins" ]; }
{ BindPaths = [ "${cfg.dataDir}/plugins/configurations/" ]; } { BindPaths = [ "${cfg.dataDir}/plugins/configurations/" ]; }
] ]
++ builtins.map serviceConfig cfg.plugins ++ builtins.map serviceConfig cfg.plugins

View file

@ -0,0 +1,146 @@
{
lib,
newScope,
dotnetCorePackages,
buildDotnetModule,
fetchJellyfinPlugin,
jellyfin,
jprm,
unzip,
}@args:
let
buildJellyfinPlugin = (lib.makeScope newScope (_: args)).callPackage package;
capitalize =
str:
let
length = builtins.stringLength str;
head = builtins.substring 0 1 str;
tail = builtins.substring 1 length str;
in
"${lib.strings.toUpper head}${tail}";
package =
{
lib,
name,
version,
dotnetCorePackages,
buildDotnetModule,
fetchJellyfinPlugin,
jellyfin,
jprm,
unzip,
...
}@params:
let
self = buildJellyfinPlugin params;
project = params.project or "Jellyfin.Plugin.${capitalize name}";
pluginLibraries = params.pluginLibraries or lib.attrsets.foldlAttrs (
acc: name: value:
acc ++ lib.optional (value == "directory") name
) [ ] (builtins.readDir "${self.src}/src");
dlls = builtins.map (name: "${name}.dll") pluginLibraries;
defaults = {
pname = "jellyfin-plugin-${name}";
version = version;
description = "${name} plugin for ${jellyfin.name}";
projectFile = "src/${project}/${project}.csproj";
dotnet-sdk = dotnetCorePackages.sdk_9_0;
dotnet-runtime = jellyfin.dotnet-runtime;
dontDotnetBuild = true;
dontDotnetInstall = true;
src = fetchJellyfinPlugin {
inherit name version;
tag = "v${builtins.head (builtins.splitVersion version)}";
hash = params.hash or "";
};
};
override =
(lib.attrsets.removeAttrs params (
builtins.attrNames (lib.functionArgs package)
++ [
"project"
"pluginLibraries"
]
))
// {
inherit jellyfin;
jellyfinPluginFiles = (params.jellyfinPluginFiles or []) ++ dlls;
meta = {
inherit (jellyfin.meta) license homepage;
maintaner = [ "nonapode@fbs42.ddnss.de" ];
}
// (params.meta or { });
nativeBuildInputs = (params.nativeBuildInputs or [ ]) ++ [
jprm
unzip
];
outputs = (params.outputs or [ ]) ++ [
"out"
"zip"
];
prePatch = ''
sed --sandbox --separate \
-e 's:\(PackageReference Include="Jellyfin\..*" Version="\)[^"]\+":\1${jellyfin.version}":' \
-e 's:<\(enerateDocumentationFile\|TreatWarningsAsErrors\)>true</\1>:<\1>false</\1>:' \
-i ${lib.strings.escapeShellArgs (builtins.map (lib: "src/${lib}/${lib}.csproj") pluginLibraries)}
success=true
for x in ${
lib.strings.escapeShellArgs (builtins.map (lib: "src/${lib}/${lib}.csproj") pluginLibraries)
}
do
diff -q $src/$x $x 2>/dev/null || continue
printf >&2 'no change: %s\n' $x
success=false
done
$success || exit 1
''
+ (params.prePatch or "");
postInstall = (params.postInstall or "") + ''
tmp_output_dir="$(mktemp -d)"
jprm plugin build . --output="''${tmp_output_dir}" --version="${version}" --dotnet-configuration="''${dotnetBuildType-Release}"
env
mv "''${tmp_output_dir}/${name}_${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
'';
};
in
buildDotnetModule (defaults // override);
in
buildJellyfinPlugin

View file

@ -0,0 +1,35 @@
{
lib,
fetchFromGitHub,
newScope,
}@args:
let
fetchJellyfinPlugin = (lib.makeScope newScope (_: args)).callPackage package;
package =
{
name,
fetchFromGitHub,
...
}@args:
let
self = fetchJellyfinPlugin args;
extraArgs = lib.attrsets.removeAttrs args (builtins.attrNames (lib.functionArgs package));
owner = args.owner or "jellyfin";
repo = args.repo or "jellyfin-plugin-${args.name}";
name = builtins.concatStringsSep "-" (
[ repo ] ++ lib.optional (args ? version) args.version ++ [ "source" ]
);
in
fetchFromGitHub (
{
inherit owner repo name;
passthru.override = self.override;
}
// extraArgs
);
in
fetchJellyfinPlugin

View file

@ -1,110 +0,0 @@
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}";
buildJellyfinPlugin = {
lib,
name,
version,
dotnetCorePackages,
buildDotnetModule,
fetchJellyfinPlugin,
jellyfin,
jprm,
unzip,
...
}@params: let
project = params.project or "Jellyfin.Plugin.${capitalize lib.strings.toUpper name}";
src = params.src or (fetchJellyfinPlugin { name = name; tag = "v${builtins.head (builtins.splitVersion version)}"; hash = params.hash or ""; });
extraArgs = lib.attrsets.removeAttrs params (builtins.attrNames (lib.functionArgs buildJellyfinPlugin));
pluginLibraries = extraArgs.pluginLibraries or lib.attrsets.foldlAttrs (
acc: name: value:
acc ++ lib.optional (value == "directory") name
) [ ] (builtins.readDir "${src}/src");
args = {
pname = "jellyfin-plugin-${name}";
version = version;
description = "${name} plugin for jellyfin";
projectFile = "src/${project}/${project}.csproj";
dotnet-sdk = dotnetCorePackages.sdk_9_0;
dotnet-runtime = jellyfin.dotnet-runtime;
dontDotnetBuild = true;
dontDontnetInstall = true;
} // extraArgs // {
inherit src;
meta = { inherit (jellyfin.meta) license homepage;
maintainer = [ "nonapode@fbs42.ddnss.de" ];
} // (extraArgs.meta or {});
nativeBuildInputs = (extraArgs.nativeBuildInputs or []) ++ [
jprm
unzip
];
outputs = (extraArgs.outputs or []) ++ [ "out" "zip" ];
prePatch = ''
sed --sandbox --separate \
-e 's:\(PackageReference Include="Jellyfin\..*" Version="\)[^"]\+":\1${jellyfin.version}":' \
-e 's:<\(enerateDocumentationFile\|TreatWarningsAsErrors\)>true</\1>:<\1>false</\1>:' \
-i ${
lib.strings.escapeShellArgs (builtins.map (lib: "src/${lib}/${lib}.csproj") pluginLibraries)
}
success=true
for x in ${
lib.strings.escapeShellArgs (builtins.map (lib: "src/${lib}/${lib}.csproj") pluginLibraries)
}
do
diff -q $src/$x $x 2>/dev/null || continue
printf >&2 'no change: %s\n' $x
success=false
done
$success || exit 1
'' + (extraArgs.prePatch or "");
postInstall =
let
dlls = builtins.map (name: "${name}.dll") pluginLibraries;
in
''
tmp_output_dir="$(mktemp -d)"
jprm plugin build . --output="''${tmp_output_dir}" --version="${version}" --dotnet-configuration="''${dotnetBuildType-Release}"
mv "''${tmp_output_dir}/${name}_${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
'';
};
in buildDotnetModule args;
in buildJellyfinPlugin

View file

@ -1,34 +0,0 @@
{
pkgs,
lib,
original,
fetchFromGitHub,
gnused,
jprm,
unzip,
dotnetCorePackages,
buildDotnetModule,
...
}:
let
context = pkgs // {
inherit callPackage fetchJellyfinPlugin buildJellyfinPlugin jellyfin;
};
callPackage = lib.callPackageWith context;
fetchJellyfinPlugin = callPackage ./fetch-plugin.nix;
buildJellyfinPlugin = callPackage ./build-plugin.nix;
jellyfin = original.overrideAttrs (
final: { passthru ? {}, ... }@prev:
assert !prev ? "plugin";
{
passthru = passthru // {
plugin =
base: callPackage base;
};
}
);
in
jellyfin

View file

@ -1,16 +0,0 @@
let
fetchJellyfinPlugin = {
lib,
fetchFromGitHub,
runCommandLocal,
...
}@args: let
owner = args.owner or "jellyfin";
repo = args.repo or "jellyfin-plugin-${args.name}";
extraArgs = lib.attrsets.removeAttrs args (builtins.attrNames (lib.functionArgs fetchJellyfinPlugin));
git = fetchFromGitHub ({
inherit owner repo;
} // extraArgs);
in git;
in fetchJellyfinPlugin

View file

@ -0,0 +1,23 @@
{
pkgs,
lib,
jellyfin,
fetchFromGitHub,
gnused,
jprm,
unzip,
dotnetCorePackages,
buildDotnetModule,
...
}: jellyfin.overrideAttrs (
final: { passthru ? {}, ...}@prev: {
passthru = passthru // {
plugins = lib.attrsets.foldlAttrs
(acc: name: value: assert (builtins.trace name true); if lib.strings.hasPrefix "jellyfin-plugin-" name
then acc // {
${lib.strings.substring (builtins.stringLength "jellyfin-plugin-") (builtins.stringLength name) name} = value;
} else acc) {} pkgs;
};
}
)

View file

@ -1,177 +0,0 @@
{
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_9_0;
dotnet-runtime = jellyfin.dotnet-runtime;
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>:<\1>false</\1>:' \
-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 // {
inherit (callPackage plugin ({ inherit info; } // args))
version hash rev;
};
in
info.mkPlugin info (lib.attrsets.removeAttrs info info.ignore);
functor =
args:
(drv args).overrideAttrs (
final: { passthru ? {}, ... }@prev: {
passthru = passthru // {
in-version =
{
version,
rev ? null,
hash ? null,
}@args':
callPackage functor (
builtins.removeAttrs args [
"version"
"rev"
"hash"
]
// args'
);
};
}
);
in
#lib.trivial.mirrorFunctionArgs plugin functor
drv

View file

@ -21,7 +21,7 @@ python3Packages.buildPythonApplication {
src = fetchFromGitHub { src = fetchFromGitHub {
owner = "oddstr13"; owner = "oddstr13";
repo = "jellyfin-plugin-repository-manager"; repo = "jellyfin-plugin-repository-manager";
tag = "v${version}"; rev = "v${version}";
hash = hash; hash = hash;
}; };

View file

@ -1,32 +0,0 @@
let
v."8.0.0.0" = {
hash = "sha256-5YUX+w4n3nBhAkdgjF9D5yY/jzRKxpW+mTQCBluzsVI=";
rev = "v8";
};
v."10.0.0.0" = {
hash = "sha256-pPhMmH17RKktIX16ozSxsigxo6tU8tlST4IAm3vpjrw=";
rev = "v10";
};
dlna = {
lib,
buildJellyfinPlugin,
fetchJellyfinPlugin,
...
}@args: let
version = args.version or "10.0.0.0";
hash = args.hash or v.${version}.hash or "";
rev = args.rev or v.${version}.rev or "v${lib.versions.major version}";
extra = lib.attrsets.removeAttrs args (builtins.attrNames (lib.functionArgs dlna));
plain = buildJellyfinPlugin {
name = "dlna";
inherit version;
src = fetchJellyfinPlugin {
name = "dlna";
inherit hash rev;
};
nugetDeps = ./deps.json;
};
in plain.override extra;
in dlna

56
plugin/dlna/package.nix Normal file
View file

@ -0,0 +1,56 @@
let
v."8.0.0.0" = {
hash = "sha256-5YUX+w4n3nBhAkdgjF9D5yY/jzRKxpW+mTQCBluzsVI=";
rev = "v8";
};
v."10.0.0.0" = {
hash = "sha256-pPhMmH17RKktIX16ozSxsigxo6tU8tlST4IAm3vpjrw=";
rev = "v10";
};
latest =
lib:
builtins.foldl' (acc: next: if lib.versionOlder acc next then next else acc) "0" (
builtins.attrNames v
);
plugin =
{
lib,
buildJellyfinPlugin,
fetchJellyfinPlugin,
...
}@params:
let
argNames = builtins.attrNames (lib.trivial.functionArgs plugin) ++ [
"name"
"version"
"hash"
];
extraArgs = lib.attrsets.removeAttrs params argNames;
self = plugin params;
name = params.name or "dlna";
version = params.version or (latest lib); # "10.0.0.0"; # TODO latest
defaultRev = {
tag = "v${lib.versions.major version}";
hash = params.hash or "";
};
args = {
name = name;
version = args.src.version or version;
nugetDeps = ./deps.json;
src = fetchJellyfinPlugin (
{
inherit name version;
}
// (v.${version} or defaultRev)
);
}
// extraArgs;
in
buildJellyfinPlugin args;
in
plugin

View file

@ -0,0 +1,125 @@
diff --git a/src/Jellyfin.Plugin.Dlna/Didl/DidlBuilder.cs b/src/Jellyfin.Plugin.Dlna/Didl/DidlBuilder.cs
index 150fb49..9160d8a 100644
--- a/src/Jellyfin.Plugin.Dlna/Didl/DidlBuilder.cs
+++ b/src/Jellyfin.Plugin.Dlna/Didl/DidlBuilder.cs
@@ -239,6 +239,17 @@ public class DidlBuilder
writer.WriteFullEndElement();
}
+ private string sanitize(string input)
+ {
+ var valid = input.Where(ch => System.Xml.XmlConvert.IsXmlChar(ch)).ToArray();
+ if (valid.Length == input.Length)
+ {
+ return input;
+ }
+ _logger.LogInformation("Sanitized: {0} (was: {1})", valid, input.Where(ch => true).ToArray());
+ return new string(valid);
+ }
+
private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo? streamInfo = null)
{
if (streamInfo is null)
@@ -1009,7 +1020,7 @@ public class DidlBuilder
writer.WriteStartElement("upnp", "artist", NsUpnp);
writer.WriteAttributeString("role", "AlbumArtist");
- writer.WriteString(name);
+ writer.WriteString(sanitize(name));
writer.WriteFullEndElement();
}
@@ -1023,7 +1034,7 @@ public class DidlBuilder
{
try
{
- writer.WriteElementString(prefix, name, namespaceUri, value);
+ writer.WriteElementString(prefix, name, namespaceUri, sanitize(value));
}
catch (XmlException ex)
{
diff --git a/src/Jellyfin.Plugin.Dlna/PlayTo/PlayToController.cs b/src/Jellyfin.Plugin.Dlna/PlayTo/PlayToController.cs
index 4d822e2..9105d46 100644
--- a/src/Jellyfin.Plugin.Dlna/PlayTo/PlayToController.cs
+++ b/src/Jellyfin.Plugin.Dlna/PlayTo/PlayToController.cs
@@ -386,34 +386,38 @@ public class PlayToController : ISessionController, IDisposable
? null :
_userManager.GetUserById(command.ControllingUserId);
- var items = new List<BaseItem>();
- foreach (var id in command.ItemIds)
- {
- AddItemFromId(id, items);
- }
-
var startIndex = command.StartIndex ?? 0;
- int len = items.Count - startIndex;
- if (startIndex > 0)
- {
- items = items.GetRange(startIndex, len);
- }
-
- var playlist = new PlaylistItem[len];
-
- // Not nullable enabled - so this is required.
- playlist[0] = CreatePlaylistItem(
- items[0],
- user,
- command.StartPositionTicks ?? 0,
- command.MediaSourceId ?? string.Empty,
- command.AudioStreamIndex,
- command.SubtitleStreamIndex);
+ var playlist = new List<PlaylistItem>();
+ var first = true;
+ foreach (var id in command.ItemIds) {
+ var item = _libraryManager.GetItemById(id);
+ if (item?.MediaType != MediaType.Audio && item?.MediaType != MediaType.Video)
+ {
+ continue;
+ }
- for (int i = 1; i < len; i++)
- {
- playlist[i] = CreatePlaylistItem(items[i], user, 0, string.Empty, null, null);
- }
+ if (startIndex > 0) {
+ startIndex -= 1;
+ continue;
+ }
+
+ PlaylistItem? playlistItem = null;
+ try {
+ playlistItem = CreatePlaylistItem(item, user,
+ first ? (command.StartPositionTicks ?? 0) : 0,
+ first ? (command.MediaSourceId ?? string.Empty) : string.Empty,
+ first ? command.AudioStreamIndex : null,
+ first ? command.SubtitleStreamIndex : null);
+ } catch (System.NullReferenceException) {
+ _logger.LogError("{0}: could not create playlist item.", item.Path ?? "<unknown>");
+ }
+ first = false;
+
+ if (playlistItem != null)
+ {
+ playlist.Add(playlistItem);
+ }
+ }
_logger.LogDebug("{0} - Playlist created", _session.DeviceName);
@@ -509,15 +513,6 @@ public class PlayToController : ISessionController, IDisposable
return info.IsDirectStream;
}
- private void AddItemFromId(Guid id, List<BaseItem> list)
- {
- var item = _libraryManager.GetItemById(id);
- if (item?.MediaType == MediaType.Audio || item?.MediaType == MediaType.Video)
- {
- list.Add(item);
- }
- }
-
private PlaylistItem CreatePlaylistItem(
BaseItem item,
User? user,