Jellyfin 12 moved to .NET 10 and changed the IUserManager surface the
plugin relies on: Users/UsersIds became GetUsers()/GetUsersIds(),
ChangePassword takes a user id, HasPassword left the provider contract,
and the user cache is gone, so every lookup is a detached copy.
The plugin now multi-targets net9.0 (against 10.11.5) and net10.0
(against 12.0.0). The differences sit behind a JELLYFIN_12 constant in
Compat/UserManagerCompat.cs, whose ChangePasswordAsync also carries the
stored hash back onto the caller's instance: on 12 the UpdateUserAsync
that claims the account would otherwise write the stale null password
back over the one provisioning just set.
Each release ships one package per generation, with the fourth version
segment naming the target (x.y.z.11 and x.y.z.12) so a 12 server picks
the 12 package over the 10.11 one. scripts/package.sh wraps jprm for a
single generation and the workflows call it twice. The builder image
moves to the .NET 10 SDK, which builds both targets; the net9.0 test run
rolls forward onto the .NET 10 runtime.
CA1873 is a .NET 10 analyzer that flags the same log calls CA1848 does;
it is set to Info, as in the upstream Jellyfin 12 tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two CI fixes.
The builder image lacked nodejs, so every job died at the first step with
"exec: node: executable file not found in $PATH" (exit 127).
actions/checkout and actions/cache are JavaScript actions: the runner
execs node inside the job container to run them, so the image needs it
even though the build itself does not. Verified by exec'ing node with no
shell, which is how the runner invokes it, and by pulling the pushed
image back from the registry.
TwoExplicitLists_IntersectToTheCommonLibraries compared a sorted actual
against an unsorted hardcoded expected, so it only passed when the
randomly generated library GUIDs happened to sort that way - it failed
about half of all runs and would have made CI intermittently red. The
intersection is a set, so it now asserts on membership and count rather
than ordering. Confirmed with 10 consecutive clean runs, up from ~50%.
Replaces the plugin template with a working plugin that lets several
users share one viewing account while keeping their individual watched
lists accurate.
Three pieces:
- Auto-creating groups. Logging in as "alice+bob" with any named
member's own password provisions the shared account and signs you in.
Verified against 10.11.5: AuthenticateUser offers unmatched usernames
to every enabled provider and re-queries afterwards, which is the hook
this relies on. Gated on a real member password so knowing two
usernames is not enough to create an account.
- Multi-password authentication. IRequiresResolvedUser hands us the
resolved shared account; each member's live stored hash is checked via
ICryptoProvider.Verify. Deliberately avoids re-entering
UserManager.AuthenticateUser, which would trip every member's
failed-attempt counter whenever a different member's password matched.
- One-way played-state sync. Shared account to members only, filtered to
PlaybackFinished/TogglePlayed/Import so playback progress ticks are
ignored. No loop guard needed: member writes carry a non-shared id.
Membership is stored as user IDs rather than re-parsed from the username,
so shared accounts can be renamed freely. The +/name collision resolves
itself because Jellyfin only consults the plugin when no local user
matches the typed name.
Targets Jellyfin 10.11.x / net9.0. Adds Gitea CI (test, build, release),
a builder image, and 34 tests covering the auth and sync rules.