158 Commits
Author SHA1 Message Date
dtourolleandClaude Opus 5.5 408cb26da7 ci: run on any runner, amd64 or arm64
🏗️ Build Plugin / build (push) Successful in 11m40s
🧪 Test Plugin / test (push) Successful in 10m39s
The builder image is now multi-arch (linux/amd64 + linux/arm64). Both
variants were tested by running this repo's own CI build steps in each
before being pushed as :latest. Jobs therefore no longer need an amd64
host: runs-on moves to ubuntu-latest, which the amd64 runner (draco-x86)
and the ARM runner (oracle-a1) both carry, so jobs go to whichever is
free instead of queueing behind long jobs on draco.

The Dockerfile header now gives the buildx command for the multi-arch
image, so a rebuild does not silently go back to amd64-only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 04:12:43 +02:00
Gitea Actions 46c7870131 Update manifest.json for version 0.1.0 2026-09-19 14:30:57 +00:00
dtourolle 64a07f1508 Merge remote-tracking branch 'origin/master'
🏗️ Build Plugin / build (push) Successful in 1m45s
🧪 Test Plugin / test (push) Successful in 55s
2026-09-19 12:31:26 +02:00
dtourolleandClaude Opus 5 5120e7abba Set version to 0.1.0
🚀 Release Plugin / build-and-release (push) Successful in 1m43s
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
v0.1.0
2026-09-19 12:30:50 +02:00
dtourolleandClaude Opus 5 27cd2a3d37 Inherit parental restrictions on shared accounts, with a chosen rating cap
A shared account previously inherited its members' library access but
none of their content restrictions, so a child could log into "alice+kid"
with their own password and get around their own rating cap.

The shared account now gets the strictest member's parental rating,
unrated-item block, blocked tags and allowed tags, recomputed at
creation, on membership change and at startup. An admin can raise the
rating cap on a slider between the strictest and the loosest member;
unrated and tag rules stay strictest-wins.

What makes raising the cap safe is the unlock rule: after a member's
password matches, both users' live policies are compared and the login
is refused if the account is looser than the member on any field. So
raising the cap above the child's rating means the child's password no
longer opens the account, while the parent's still does. The same rule
bounds the slider - past the loosest member nobody could unlock the
account - so a chosen cap is clamped back into range whenever applied.

Allowed tags need care: Jellyfin reads an empty list as "no whitelist",
so an empty intersection of members' whitelists is written as a sentinel
tag no item carries. Access schedules and channels are not inherited yet.

The shared account is never an administrator. Groups created at the
login screen always inherit and are restricted before the first session
exists. The dashboard shows each member's cap, who a chosen cap shuts
out, and the restrictions in effect, and gains a per-group edit form for
the sync options.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 12:30:50 +02:00
Gitea Actions 64668e10ef Update manifest.json for version 0.0.6 2026-09-19 10:23:58 +00:00
dtourolleandClaude Opus 5 0c5fc9487c Set version to 0.0.6
🏗️ Build Plugin / build (push) Successful in 3m20s
🧪 Test Plugin / test (push) Successful in 1m23s
🚀 Release Plugin / build-and-release (push) Successful in 2m4s
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
v0.0.6
2026-09-19 10:36:11 +02:00
dtourolleandClaude Opus 5 8ad727d870 Fix watched-state sync so members' Next Up and Continue Watching follow
Synced rows only ever had the Played flag set. Jellyfin computes Next Up
from LastPlayedDate on the member's own row and Continue Watching from
the resume position, so a member watching through the shared account got
the tick on each episode but their Next Up never advanced. Members are
now written the way BaseItem.MarkPlayed/MarkUnplayed write: date and
position included. Rows the old version ticked without a date are
repaired the next time the item syncs.

PlaybackFinished was also treated as an unwatched toggle. Jellyfin raises
it on every stop, not just completion (and on 10.11 PlaybackStart resets
Played to false first), so a stop halfway through on the shared account
cleared members' own watched state whenever Sync unwatched was on.
Playback-derived reasons (PlaybackFinished, PlaybackProgress,
UpdateUserData) now only ever mirror "watched"; TogglePlayed and Import
remain explicit and mirror either way. PlaybackProgress is acted on so
the tick lands as soon as the completion threshold is crossed, including
for clients that never report a stop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 10:36:11 +02:00
Gitea Actions a2ffbb3efe Update manifest.json for version 0.0.5 2026-09-11 17:32:26 +00:00
dtourolleandClaude Opus 5 5397017000 Set version to 0.0.5
🏗️ Build Plugin / build (push) Successful in 3m50s
🧪 Test Plugin / test (push) Successful in 1m7s
🚀 Release Plugin / build-and-release (push) Successful in 2m6s
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
v0.0.5
2026-09-11 19:25:21 +02:00
dtourolleandClaude Opus 5 bd08629fff Support Jellyfin 12 alongside 10.11
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>
2026-09-11 19:25:16 +02:00
Gitea Actions 17bb9a1e8a Update manifest.json for version 0.0.4.0 2026-08-09 08:59:11 +00:00
dtourolleandClaude Opus 5 44ab98e082 Set version to 0.0.4
🏗️ Build Plugin / build (push) Successful in 36s
🧪 Test Plugin / test (push) Successful in 34s
🚀 Release Plugin / build-and-release (push) Successful in 47s
Dynamic group creation fix on top of 0.0.3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v0.0.4.0
2026-08-09 10:56:36 +02:00
dtourolleandClaude Opus 5 bb8814644c Set the shared account's password before claiming it
Provisioning assigned AuthenticationProviderId and only then called
IUserManager.ChangePassword. Jellyfin dispatches that call to the provider the
user is currently assigned to, so it reached this plugin's own ChangePassword,
which refuses by design. Creating a group by typing "alice+bob" at the login
screen therefore died with NotSupportedException.

Set the placeholder password first, while the freshly created account is still
on Jellyfin's default provider, then claim it.

The new end-to-end tests wire the real provisioning, group and authentication
services together rather than mocking IProvisioningService, and cover a group
created on demand being unlocked afterwards by either member's password. With
the old ordering restored, six of them fail with the original exception.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:56:36 +02:00
Gitea Actions 69f7a87cef Update manifest.json for version 0.0.3.0 2026-08-09 08:49:28 +00:00
dtourolleandClaude Opus 5 fd08d7ea1a Set version to 0.0.3
🏗️ Build Plugin / build (push) Successful in 1m43s
🧪 Test Plugin / test (push) Successful in 37s
🚀 Release Plugin / build-and-release (push) Successful in 45s
Startup-crash fix on top of 0.0.2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v0.0.3.0
2026-08-09 10:46:09 +02:00
dtourolleandClaude Opus 5 da779514fe Resolve the group services lazily in the authentication provider
Jellyfin's UserManager constructor-injects every IAuthenticationProvider, so
building IUserManager forced SharedAccountAuthenticationProvider to be built
first. That provider eagerly required IGroupService and IDynamicGroupService,
both of which need IUserManager, and the container refused to start the server
with "a circular dependency was detected".

Take the two group services as Lazy<T> and dereference them at authentication
time instead. Nobody can log in before the host is up, so the deferred lookup
is always safe. Microsoft's container has no built-in Lazy<T> support, hence
the explicit factory registrations.

The accompanying test builds the service graph through a stand-in that mimics
UserManager's constructor shape and validates it on build, so a reintroduced
cycle fails in CI rather than at server startup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:46:09 +02:00
Gitea Actions dfab79e92a Update manifest.json for version 0.0.2 2026-07-31 07:45:09 +00:00
dtourolle f430d288ea Set version to 0.0.2
🏗️ Build Plugin / build (push) Successful in 34s
🧪 Test Plugin / test (push) Successful in 35s
🚀 Release Plugin / build-and-release (push) Successful in 46s
Order-independent group resolution on top of 0.0.1.
v0.0.2
2026-07-31 09:43:12 +02:00
dtourolle 5c8430f207 Treat member order as insignificant when resolving a group
🏗️ Build Plugin / build (push) Successful in 38s
🧪 Test Plugin / test (push) Successful in 34s
"jane+john" and "john+jane" name the same group, but they did not behave
that way. Jellyfin only routes a login here when no account matches the
typed name, so logging in with the reversed spelling of an existing group
found nothing and quietly created a second shared account for the same
two people - each with its own watched state.

Group identity is now order-independent:

- Member names are sorted alphabetically when building an account name,
  so a given set of members always produces the same name.
- Before creating anything, the login path looks for an existing group
  whose members are exactly the named set, compared as a set rather than
  a sequence, and logs into that account if it finds one.
- Stored member lists are kept in the same canonical order on create and
  update, so a group's stored order does not depend on the order an
  admin happened to select members in.

Passing no name through to provisioning lets it generate the canonical
name, rather than preserving whatever order was typed.

Members are also now checked in the order they were typed, stopping at
the first match, so whoever puts their own name first is verified first.
Verification is a deliberately slow hash comparison, so the ordering is
worth having; it is only a preference, and any member's password still
unlocks the group.
2026-07-31 09:35:14 +02:00
Gitea Actions cb95a317d0 Update manifest.json for version 0.0.1 2026-07-29 22:17:29 +00:00
dtourolle a2780915bb Install nodejs in the builder image and fix a flaky intersection test
🏗️ Build Plugin / build (push) Successful in 1m10s
🧪 Test Plugin / test (push) Successful in 35s
🚀 Release Plugin / build-and-release (push) Failing after 33s
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%.
v0.0.1
2026-07-30 00:10:34 +02:00
dtourolle 92c7dcb871 Set version to 0.0.1 and pin assembly identity for CI builds
🏗️ Build Plugin / build (push) Successful in 37s
🚀 Release Plugin / build-and-release (push) Failing after 30s
🧪 Test Plugin / test (push) Failing after 33s
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.
2026-07-29 00:17:10 +02:00
dtourolle b4134dd744 Grant shared accounts the intersection of member library access
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.
2026-07-29 00:15:32 +02:00
dtourolle 7be07d16a2 Implement Watched Together shared viewing accounts
🏗️ Build Plugin / build (push) Has been cancelled
🧪 Test Plugin / test (push) Has been cancelled
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.
2026-07-29 00:00:13 +02:00
T-GanderandGitHub 7a9dbdafcc Update README for .NET SDK version and setup instructions (#85) 2025-11-23 11:26:03 -07:00
T-GanderandGitHub 6277846394 Fixed plugin template to build and copy correctly, allowing settings button to show and the plugin to register. (#84) 2025-11-23 06:19:48 -07:00
Bond-009andGitHub d544b71939 Merge pull request #79 from jakobkukla/patch-1
Fix vscode build task for NET 8
2025-03-11 09:33:59 +01:00
Jakob KuklaandGitHub 12988e127f Fix vscode build task for .NET 8 2025-03-04 00:55:32 +01:00
Anthony LavadoandGitHub e9312af2c2 Merge pull request #72 from jellyfin/renovate/jellyfin.controller-10.x 2024-10-05 11:42:18 -04:00
renovate[bot]andGitHub 4291891dfd Update dependency Jellyfin.Controller to 10.9.11 2024-10-05 15:20:24 +00:00
Anthony LavadoandGitHub e9c9f334cd Merge pull request #74 from Shadowghost/upgrade-template 2024-10-05 11:20:08 -04:00
Shadowghost 3afa1fc407 Upgrade template to 10.9.x and .NET 8 2024-10-05 17:06:23 +02:00
Anthony LavadoandGitHub 2c1143b49f Merge pull request #65 from mrnoname1000/master 2024-10-05 11:01:52 -04:00
Anthony LavadoandGitHub 74da0d2568 Merge pull request #54 from carif/linux 2024-10-05 11:01:16 -04:00
ErikandGitHub 2ddd08221d Updated to specify that you need to place the plugin DLL into a subfolder (#71) 2024-05-19 08:08:00 -06:00
Bond-009andGitHub 5f4229e0fd Merge pull request #66 from ReenigneArcher/patch-1
chore(deps): bump Jellyfin.Controller and Jellyfin.Model to 10.8.13
2024-01-12 22:38:00 +01:00
ReenigneArcherandGitHub 4c4b1e5a71 chore(deps): bump Jellyfin.Controller and Jellyfin.Model to 10.8.13 2023-12-12 23:14:20 -05:00
mrnoname1000andGitHub 7fe332f29c README.md: Fix sh UUID generator
And make call to head POSIX-compliant
2023-10-19 23:35:44 +00:00
Bond-009andGitHub 9052fe20be Merge pull request #64 from jellyfin/renovate/stylecop.analyzers-1.x
chore(deps): update dependency stylecop.analyzers to v1.2.0-beta.507
2023-08-16 18:50:53 +02:00
renovate[bot]andGitHub 3c3776cc60 chore(deps): update dependency stylecop.analyzers to v1.2.0-beta.507 2023-07-03 05:49:33 +00:00
Johannes KauffmannandGitHub 0cb471baa2 Fix typo in interface name (#61) 2023-06-30 18:23:54 -06:00
Anthony LavadoandGitHub b282d676f4 Merge pull request #60 from zjeffer/patch-1
(chore: README.md) change codeblock language to `jsonc`
2023-05-05 21:32:28 -04:00
Tuur VanhoutteandGitHub 6495c17c7f (chore: README.md) change codeblock language to jsonc
The markdown codeblocks in the README were using `json`, which has no support for comments, making them highlighted in red. `jsonc` does have support, correctly rendering the codeblocks.
2023-04-30 23:47:45 +02:00
ViperiniusandGitHub 89500d8f8e Fix typo in class name for input label (#59) 2023-02-19 15:37:37 -07:00
Cody RobiberoandGitHub d3a60b5f71 Merge pull request #43 from SenorSmartyPants/FixSave
Don't navigate to another page, display saved settings instead
2022-08-13 14:45:52 -06:00
Patrick Farwick 4de36a8abc Add required extensions to Visual Studio Code 2022-07-07 16:48:24 +02:00
Patrick Farwick 66bac259a8 Add linux support for vscode dev-setup
The ${env:XDG_DATA_HOME} variable is not used, as this variable resolves
to the current working directory within Visual Studio Code's development
container. By manually specifying the file path, it is possible to use
Visual Studio Code's development container.

The "mkdir" command uses the "-p" flag. This flag creates the parent
directories, as well. Should they already exists, the command moves
down to the next folder to create without throwing an error.

The "-r" parameter of the "cp" is required to recursively copy all
files and directories within the "publish" directory.
2022-07-07 16:47:17 +02:00
Joshua M. BonifaceandGitHub c71657dfce Merge pull request #53 from oddstr13/pr-10.8-1
Target Jellyfin 10.8
2022-06-24 18:12:06 -04:00
Odd Stråbø ccc02d3ce0 Target Jellyfin 10.8 2022-06-24 16:49:01 +02:00