From aeff324fcf30beebb4a78f59b85ac6dd520c98b9 Mon Sep 17 00:00:00 2001 From: ibizaman Date: Sun, 1 Feb 2026 17:17:58 +0100 Subject: [PATCH] jellyfin: update jellyfin cli for 10.11.6 --- modules/services/jellyfin.nix | 43 +++++++++++++--------- test/common.nix | 10 +++-- test/services/jellyfin.nix | 69 ++++++++++++++++++++++------------- 3 files changed, 77 insertions(+), 45 deletions(-) diff --git a/modules/services/jellyfin.nix b/modules/services/jellyfin.nix index 39c6e64..ee0af63 100644 --- a/modules/services/jellyfin.nix +++ b/modules/services/jellyfin.nix @@ -13,31 +13,38 @@ let fqdn = "${cfg.subdomain}.${cfg.domain}"; - jellyfin-cli = pkgs.buildDotnetModule rec { - pname = "jellyfin-cli"; - version = "10.10.7"; + jellyfin = pkgs.buildDotnetModule rec { + pname = "jellyfin"; + version = "10.11.6"; src = pkgs.fetchFromGitHub { owner = "ibizaman"; repo = "jellyfin"; - rev = "0b1a5d929960f852dba90c1fc36f3a19dc094f8d"; - hash = "sha256-H9V65+886EYMn/xDEgmxvoEOrbZaI1wSfmkN9vAzGhw="; + rev = "c58ca41d9ee76d137be788cd6f2d089e288ad561"; + hash = "sha256-gTHsz5qRT+9FjAqBb4hDBkHChYDU52snBWu6cQb10i4="; }; propagatedBuildInputs = [ pkgs.sqlite ]; - projectFile = "Jellyfin.Cli/Jellyfin.Cli.csproj"; - executables = [ "jellyfin-cli" ]; + projectFile = "Jellyfin.Server/Jellyfin.Server.csproj"; + executables = [ "jellyfin" ]; nugetDeps = "${pkgs.path}/pkgs/by-name/je/jellyfin/nuget-deps.json"; runtimeDeps = [ pkgs.jellyfin-ffmpeg pkgs.fontconfig pkgs.freetype ]; - dotnet-sdk = pkgs.dotnetCorePackages.sdk_8_0; - dotnet-runtime = pkgs.dotnetCorePackages.aspnetcore_8_0; + dotnet-sdk = pkgs.dotnetCorePackages.sdk_9_0; + dotnet-runtime = pkgs.dotnetCorePackages.aspnetcore_9_0; dotnetBuildFlags = [ "--no-self-contained" ]; + makeWrapperArgs = [ + "--append-flags" + "--ffmpeg=${pkgs.jellyfin-ffmpeg}/bin/ffmpeg" + "--append-flags" + "--webdir=${pkgs.jellyfin-web}/share/jellyfin-web" + ]; + passthru.tests = { smoke-test = pkgs.nixosTests.jellyfin; }; @@ -53,7 +60,7 @@ let purcell jojosch ]; - mainProgram = "jellyfin-cli"; + mainProgram = "jellyfin"; platforms = dotnet-runtime.meta.platforms; }; }; @@ -169,9 +176,9 @@ in description = "Pluging used for LDAP authentication."; default = shb.mkJellyfinPlugin (rec { pname = "jellyfin-plugin-ldapauth"; - version = "20"; + version = "22"; url = "https://github.com/jellyfin/${pname}/releases/download/v${version}/ldap-authentication_${version}.0.0.0.zip"; - hash = "sha256-qATHNuiC6u+/vbY610jrFZSC16FG+Zpdjo+dfOVdVIk="; + hash = "sha256-m2oD9woEuoSRiV9OeifAxZN7XQULMKS0Yq4TF+LjjpI="; }); }; @@ -232,9 +239,9 @@ in description = "Pluging used for SSO authentication."; default = shb.mkJellyfinPlugin (rec { pname = "jellyfin-plugin-sso"; - version = "3.5.2.4"; + version = "4.0.0.3"; url = "https://github.com/9p4/${pname}/releases/download/v${version}/sso-authentication_${version}.zip"; - hash = "sha256-e+w5m6/7vRAynStDj34eBexfCIEgDJ09huHzi5gQEbo="; + hash = "sha256-Jkuc+Ua7934iSutf/zTY1phTxaltUkfiujOkCi7BW8w="; }); }; @@ -340,6 +347,7 @@ in ]; services.jellyfin.enable = true; + services.jellyfin.package = jellyfin; networking.firewall = { # from https://jellyfin.org/docs/general/networking/index.html, for auto-discovery @@ -730,6 +738,7 @@ in let # We must always wait for the service to be fully initialized, # even if we're planning on changing the config and restarting. + # And the service is not initialized until this URL returns a 200 and not a 503. waitForCurl = pkgs.writeShellApplication { name = "waitForCurl"; runtimeInputs = [ pkgs.curl ]; @@ -770,14 +779,14 @@ in # # If the file does not exist, write the config, create the file then restart. # If the file exists, do nothing and remove the file, resetting the state for the next time. - restartedFile = "${config.services.jellyfin.dataDir}/.jellyfin-restarted"; + restartedFile = "${config.services.jellyfin.dataDir}/shb-jellyfin-restarted"; writeConfig = pkgs.writeShellApplication { name = "writeConfig"; runtimeInputs = [ pkgs.systemd ]; text = '' if ! [ -f "${restartedFile}" ]; then - ${lib.getExe jellyfin-cli} wizard \ + ${lib.getExe config.services.jellyfin.package} config \ --datadir='${config.services.jellyfin.dataDir}' \ --configdir='${config.services.jellyfin.configDir}' \ --cachedir='${config.services.jellyfin.cacheDir}' \ @@ -799,7 +808,7 @@ in rm "${restartedFile}" else echo "Restarting jellyfin.service" - touch "${restartedFile}" + echo "This file is used by SelfHostBlocks to know when to restart jellyfin" > "${restartedFile}" systemctl reload-or-restart jellyfin.service fi ''; diff --git a/test/common.nix b/test/common.nix index 90ef5fd..a70dc20 100644 --- a/test/common.nix +++ b/test/common.nix @@ -99,19 +99,23 @@ let # otherwise curl will not be able to verify the "legitimacy of the server". + lib.strings.concatMapStrings ( u: + let + url = if builtins.isString u then u else u.url; + status = if builtins.isString u then 200 else u.status; + in '' import time done = False count = 15 while not done and count > 0: - response = curl(client, """{"code":%{response_code}}""", "${u}") + response = curl(client, """{"code":%{response_code}}""", "${url}") time.sleep(5) count -= 1 if isinstance(response, dict): - done = response.get('code') == 200 + done = response.get('code') == ${toString status} if not done: - raise Exception(f"Response was never 200, got last: {response}") + raise Exception(f"Response was never ${toString status}, got last: {response}") '' + "\n" ) (waitForUrls args) diff --git a/test/services/jellyfin.nix b/test/services/jellyfin.nix index 9a9679f..777f597 100644 --- a/test/services/jellyfin.nix +++ b/test/services/jellyfin.nix @@ -2,6 +2,9 @@ let port = 9096; + adminUser = "jellyfin2"; + adminPassword = "admin"; + commonTestScript = shb.test.mkScripts { hasSSL = { node, ... }: !(isNull node.config.shb.jellyfin.ssl); waitForServices = @@ -19,24 +22,36 @@ let { proto_fqdn, ... }: [ "${proto_fqdn}/System/Info/Public" + { + url = "${proto_fqdn}/Users/AuthenticateByName"; + status = 401; + } ]; extraScript = { node, ... }: '' + server.wait_until_succeeds("journalctl --since -1m --unit jellyfin --grep 'Startup complete'") headers = unline_with(" ", """ -H 'Content-Type: application/json' -H 'Authorization: MediaBrowser Client="Android TV", Device="Nvidia Shield", DeviceId="ZQ9YQHHrUzk24vV", Version="0.15.3"' """) + import time with subtest("api login success"): - response = curl(client, """{"code":%{response_code}}""", "${node.config.test.proto_fqdn}/Users/AuthenticateByName", - data="""{"Username": "jellyfin", "Pw": "admin"}""", - extra=headers) - if response['code'] != 200: + ok = False + for i in range(1, 5): + response = curl(client, """{"code":%{response_code}}""", "${node.config.test.proto_fqdn}/Users/AuthenticateByName", + data="""{"Username": "${adminUser}", "Pw": "${adminPassword}"}""", + extra=headers) + if response['code'] == 200: + ok = True + break + time.sleep(5) + if not ok: raise Exception(f"Expected success, got: {response['code']}") with subtest("api login failure"): response = curl(client, """{"code":%{response_code}}""", "${node.config.test.proto_fqdn}/Users/AuthenticateByName", - data="""{"Username": "jellyfin", "Pw": "badpassword"}""", + data="""{"Username": "${adminUser}", "Pw": "badpassword"}""", extra=headers) if response['code'] != 401: raise Exception(f"Expected failure, got: {response['code']}") @@ -50,6 +65,9 @@ let shb.test.baseModule ../../modules/services/jellyfin.nix ]; + # Jellyfin checks for minimum 2Gib on startup. + virtualisation.diskSize = 4096; + virtualisation.memorySize = 4096; test = { subdomain = "j"; }; @@ -59,7 +77,7 @@ let inherit (config.test) subdomain domain; inherit port; admin = { - username = "jellyfin"; + username = adminUser; password.result = config.shb.hardcodedsecret.jellyfinAdminPassword.result; }; debug = true; @@ -67,7 +85,7 @@ let shb.hardcodedsecret.jellyfinAdminPassword = { request = config.shb.jellyfin.admin.password.request; - settings.content = "admin"; + settings.content = adminPassword; }; environment.systemPackages = [ @@ -95,18 +113,18 @@ let loginButtonNameRegex = "Sign In"; testLoginWith = [ { - username = "jellyfin"; + username = adminUser; password = "badpassword"; nextPageExpect = [ - "expect(page).to_have_title(re.compile('Jellyfin'))" + # "expect(page).to_have_title(re.compile('Jellyfin'))" "expect(page.get_by_text(re.compile('[Ii]nvalid'))).to_be_visible(timeout=10000)" ]; } { - username = "jellyfin"; - password = "admin"; + username = adminUser; + password = adminPassword; nextPageExpect = [ - "expect(page).to_have_title(re.compile('Jellyfin'))" + # "expect(page).to_have_title(re.compile('Jellyfin'))" "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Uu]ser'))).not_to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Pp]assword$'))).not_to_be_visible(timeout=10000)" @@ -181,18 +199,18 @@ let loginButtonNameRegex = "Sign In"; testLoginWith = [ { - username = "jellyfin"; + username = adminUser; password = "badpassword"; nextPageExpect = [ - "expect(page).to_have_title(re.compile('Jellyfin'))" + # "expect(page).to_have_title(re.compile('Jellyfin'))" "expect(page.get_by_text(re.compile('[Ii]nvalid'))).to_be_visible(timeout=10000)" ]; } { - username = "jellyfin"; - password = "admin"; + username = adminUser; + password = adminPassword; nextPageExpect = [ - "expect(page).to_have_title(re.compile('Jellyfin'))" + # "expect(page).to_have_title(re.compile('Jellyfin'))" "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Uu]ser'))).not_to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Pp]assword$'))).not_to_be_visible(timeout=10000)" @@ -202,7 +220,7 @@ let username = "alice"; password = "AlicePassword"; nextPageExpect = [ - "expect(page).to_have_title(re.compile('Jellyfin'))" + # "expect(page).to_have_title(re.compile('Jellyfin'))" # For a reason I can't explain, redirection needs to happen manually. "page.goto('${config.test.proto}://${config.test.fqdn}/web/')" "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=10000)" @@ -214,7 +232,7 @@ let username = "alice"; password = "NotAlicePassword"; nextPageExpect = [ - "expect(page).to_have_title(re.compile('Jellyfin'))" + # "expect(page).to_have_title(re.compile('Jellyfin'))" "expect(page.get_by_text(re.compile('[Ii]nvalid'))).to_be_visible(timeout=10000)" ]; } @@ -222,7 +240,7 @@ let username = "bob"; password = "BobPassword"; nextPageExpect = [ - "expect(page).to_have_title(re.compile('Jellyfin'))" + # "expect(page).to_have_title(re.compile('Jellyfin'))" # For a reason I can't explain, redirection needs to happen manually. "page.goto('${config.test.proto}://${config.test.fqdn}/web/')" "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=10000)" @@ -234,7 +252,7 @@ let username = "bob"; password = "NotBobPassword"; nextPageExpect = [ - "expect(page).to_have_title(re.compile('Jellyfin'))" + # "expect(page).to_have_title(re.compile('Jellyfin'))" "expect(page.get_by_text(re.compile('[Ii]nvalid'))).to_be_visible(timeout=10000)" ]; } @@ -300,7 +318,7 @@ let "page.get_by_text(re.compile('[Aa]ccept')).click()" # For a reason I can't explain, redirection needs to happen manually. "page.goto('${config.test.proto}://${config.test.fqdn}/web/')" - "expect(page).to_have_title(re.compile('Jellyfin'))" + # "expect(page).to_have_title(re.compile('Jellyfin'))" "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Uu]ser'))).not_to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Pp]assword$'))).not_to_be_visible(timeout=10000)" @@ -313,7 +331,7 @@ let # For a reason I can't explain, redirection needs to happen manually. # So for failing auth, we check we're back on the login page. "page.goto('${config.test.proto}://${config.test.fqdn}/web/')" - "expect(page).to_have_title(re.compile('Jellyfin'))" + # "expect(page).to_have_title(re.compile('Jellyfin'))" "expect(page.get_by_label(re.compile('^[Uu]ser'))).to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Pp]assword$'))).to_be_visible(timeout=10000)" ]; @@ -325,7 +343,7 @@ let "page.get_by_text(re.compile('[Aa]ccept')).click()" # For a reason I can't explain, redirection needs to happen manually. "page.goto('${config.test.proto}://${config.test.fqdn}/web/')" - "expect(page).to_have_title(re.compile('Jellyfin'))" + # "expect(page).to_have_title(re.compile('Jellyfin'))" "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Uu]ser'))).not_to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Pp]assword$'))).not_to_be_visible(timeout=10000)" @@ -337,7 +355,7 @@ let nextPageExpect = [ # For a reason I can't explain, redirection needs to happen manually. "page.goto('${config.test.proto}://${config.test.fqdn}/web/')" - "expect(page).to_have_title(re.compile('Jellyfin'))" + # "expect(page).to_have_title(re.compile('Jellyfin'))" "expect(page.get_by_label(re.compile('^[Uu]ser'))).to_be_visible(timeout=10000)" "expect(page.get_by_label(re.compile('^[Pp]assword$'))).to_be_visible(timeout=10000)" ]; @@ -352,6 +370,7 @@ let shb.test.runNixOSTest { name = "jellyfin_${name}"; + interactive.sshBackdoor.enable = true; interactive.nodes.server = { environment.systemPackages = [ pkgs.sqlite