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