From f57c1a1955b0ea140cbc6a8486ea326acf93c394 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Francois=20Coutau=20B=C3=A9garie?=
<74976008+Franciskid@users.noreply.github.com>
Date: Fri, 11 Sep 2026 14:49:55 +0200
Subject: [PATCH] Keep redemption working on Jellyfin 10.11.9 and later (#20)
Jellyfin 10.11.9 changed IUserManager.ChangePassword to take the user id
instead of the User. A call compiled against either signature throws
MissingMethodException on the other, so look up whichever one the server
has and call it through a delegate. Bumps to 1.0.5.0.
---
.../Jellyfin.Plugin.ShareLinks.csproj | 6 +-
.../Services/JellyfinGuestUserService.cs | 2 +-
.../Services/UserManagerCompat.cs | 63 +++++++++++++++++++
Jellyfin.Plugin.ShareLinks/meta.json | 4 +-
README.md | 2 +-
5 files changed, 70 insertions(+), 7 deletions(-)
create mode 100644 Jellyfin.Plugin.ShareLinks/Services/UserManagerCompat.cs
diff --git a/Jellyfin.Plugin.ShareLinks/Jellyfin.Plugin.ShareLinks.csproj b/Jellyfin.Plugin.ShareLinks/Jellyfin.Plugin.ShareLinks.csproj
index c9d0aa5..73e954d 100644
--- a/Jellyfin.Plugin.ShareLinks/Jellyfin.Plugin.ShareLinks.csproj
+++ b/Jellyfin.Plugin.ShareLinks/Jellyfin.Plugin.ShareLinks.csproj
@@ -6,9 +6,9 @@
latest
Jellyfin.Plugin.ShareLinks
Jellyfin.Plugin.ShareLinks
- 1.0.4.0
- 1.0.4.0
- 1.0.4.0
+ 1.0.5.0
+ 1.0.5.0
+ 1.0.5.0
true
false
disable
diff --git a/Jellyfin.Plugin.ShareLinks/Services/JellyfinGuestUserService.cs b/Jellyfin.Plugin.ShareLinks/Services/JellyfinGuestUserService.cs
index 2537c05..9928113 100644
--- a/Jellyfin.Plugin.ShareLinks/Services/JellyfinGuestUserService.cs
+++ b/Jellyfin.Plugin.ShareLinks/Services/JellyfinGuestUserService.cs
@@ -90,7 +90,7 @@ public sealed class JellyfinGuestUserService
// policy hands the account to GuestAuthenticationProvider, which refuses every
// interactive sign-in - but it means the account is never reachable with a blank
// password either.
- await _userManager.ChangePassword(user, password).ConfigureAwait(false);
+ await _userManager.ChangePasswordAsync(user, password).ConfigureAwait(false);
await ApplyPolicyAsync(user, record, disabled: false).ConfigureAwait(false);
user = _userManager.GetUserById(user.Id) ?? user;
diff --git a/Jellyfin.Plugin.ShareLinks/Services/UserManagerCompat.cs b/Jellyfin.Plugin.ShareLinks/Services/UserManagerCompat.cs
new file mode 100644
index 0000000..be199ec
--- /dev/null
+++ b/Jellyfin.Plugin.ShareLinks/Services/UserManagerCompat.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Reflection;
+using System.Threading.Tasks;
+using Jellyfin.Database.Implementations.Entities;
+using MediaBrowser.Controller.Library;
+
+namespace Jellyfin.Plugin.ShareLinks.Services;
+
+///
+/// Binds IUserManager.ChangePassword at runtime instead of at compile time.
+///
+internal static class UserManagerCompat
+{
+ // Jellyfin 10.11.9 changed ChangePassword's parameter from the User itself to its Guid id.
+ // The plugin compiles against 10.11.0 but has to load on servers both before and after that
+ // change, and a call compiled against either signature throws MissingMethodException on a
+ // server that only has the other one. Resolving the method once with reflection and calling
+ // it through a delegate lets the plugin bind to whichever overload the running server has.
+ private static readonly Lazy> ChangePasswordInvoker =
+ new(ResolveChangePassword);
+
+ ///
+ /// Changes a user's password, binding to whichever IUserManager.ChangePassword
+ /// overload the running Jellyfin server exposes.
+ ///
+ /// The user manager to invoke.
+ /// The user whose password is being changed.
+ /// The new password.
+ /// A task that completes when the password has been changed.
+ public static Task ChangePasswordAsync(this IUserManager userManager, User user, string newPassword)
+ {
+ ArgumentNullException.ThrowIfNull(userManager);
+ ArgumentNullException.ThrowIfNull(user);
+
+ return ChangePasswordInvoker.Value(userManager, user, newPassword);
+ }
+
+ /// Resolves the available ChangePassword overload into a single callable shape.
+ private static Func ResolveChangePassword()
+ {
+ // Prefer the current (10.11.9+) overload first.
+ var guidOverload = typeof(IUserManager).GetMethod(
+ nameof(IUserManager.ChangePassword),
+ new[] { typeof(Guid), typeof(string) });
+ if (guidOverload is not null)
+ {
+ var call = guidOverload.CreateDelegate>();
+ return (manager, user, password) => call(manager, user.Id, password);
+ }
+
+ // Fall back to the older (10.11.0 - 10.11.8) overload.
+ var userOverload = typeof(IUserManager).GetMethod(
+ nameof(IUserManager.ChangePassword),
+ new[] { typeof(User), typeof(string) });
+ if (userOverload is not null)
+ {
+ return userOverload.CreateDelegate>();
+ }
+
+ return (_, _, _) => throw new InvalidOperationException(
+ "This Jellyfin version has neither IUserManager.ChangePassword(Guid, string) nor ChangePassword(User, string).");
+ }
+}
diff --git a/Jellyfin.Plugin.ShareLinks/meta.json b/Jellyfin.Plugin.ShareLinks/meta.json
index ccecb46..ba224b3 100644
--- a/Jellyfin.Plugin.ShareLinks/meta.json
+++ b/Jellyfin.Plugin.ShareLinks/meta.json
@@ -1,12 +1,12 @@
{
"guid": "68540b76-ee74-436d-85ff-2abc884bbea6",
"name": "ShareLinks",
- "version": "1.0.4.0",
+ "version": "1.0.5.0",
"targetAbi": "10.11.0.0",
"framework": "net9.0",
"owner": "Franciskid",
"overview": "Secure expiring guest-share links for Jellyfin items.",
"description": "Adds secure, expiring share links for Jellyfin items with JSON-backed storage, token hashing, and cleanup scaffolding.",
"category": "General",
- "timestamp": "2026-07-26T20:15:00.0000000Z"
+ "timestamp": "2026-09-11T12:00:00.0000000Z"
}
diff --git a/README.md b/README.md
index 92e68f6..021516a 100644
--- a/README.md
+++ b/README.md
@@ -181,7 +181,7 @@ it in `token-secret.key`.
## Compatibility
-Jellyfin **10.11** (`targetAbi 10.11.0.0`), .NET 9. Tested on 10.11.8. The web-client injection
+Jellyfin **10.11** (`targetAbi 10.11.0.0`), .NET 9. Tested on 10.11.8 and 10.11.11. The web-client injection
targets the shipped client, in English and in French.
## Workflow