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>
The release and nightly workflows stamp a date-based version into
build.yaml (1.0.20260729.42). That build segment exceeds the 16-bit
limit AssemblyVersion and FileVersion require, so it would fail the
compile with CS7034 if it reached them.
Directory.Build.targets is imported after Directory.Build.props and is
not rewritten by CI, so pinning the assembly identity there keeps those
builds working. The manifest version is untouched - Jellyfin identifies
plugins by GUID plus manifest version, not assembly version. Verified by
building with a date-based version injected.
Previously the shared account's libraries were chosen independently of
its members, so a group could see a library that one of its members was
blocked from - joining a group became a way to gain access. That was
especially sharp with auto-created groups, where no admin is in the loop.
A shared account is now granted exactly the libraries every member can
already reach. If one member is blocked from a library, no group
containing them can see it. The account is therefore always a subset of
what each member could reach alone, which is what makes creating groups
at the login screen safe to leave on by default.
Details:
- "Enable all folders" is expanded to concrete library ids before
intersecting, since it cannot otherwise be compared with an explicit
list. Shared accounts are always given an explicit list, never the
all-folders permission, so newly added libraries do not silently widen
an existing group.
- Explicitly blocked folders are subtracted even for members who
otherwise have access to everything.
- Fails closed: an unresolvable member contributes no access rather than
being treated as unrestricted.
- Recomputed when membership changes, and re-applied to every group at
startup so narrowing a member's own access narrows their groups.
Drops the now-meaningless EnableAllFolders/EnabledFolders provisioning
inputs and the DynamicGroupsEnableAllFolders setting. Adds 8 tests
covering the intersection rules.
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.