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>
This commit is contained in:
2026-09-11 19:25:16 +02:00
co-authored by Claude Opus 5
parent 17bb9a1e8a
commit bd08629fff
18 changed files with 411 additions and 111 deletions
+38 -17
View File
@@ -164,8 +164,10 @@ Then install **Watched Together** from the catalogue and restart Jellyfin.
### Manual
Download the release `.zip`, extract it into a `WatchedTogether` folder inside your Jellyfin
`plugins` directory, and restart the server.
Each release ships two `.zip` files: one for Jellyfin **10.11** and one for Jellyfin **12** (the
version's last segment says which: `x.y.z.11` or `x.y.z.12`). Download the one matching your
server, extract it into a `WatchedTogether` folder inside your Jellyfin `plugins` directory, and
restart the server. The plugin repository picks the right one for you automatically.
---
@@ -239,18 +241,26 @@ How this plugin bounds what a shared account can reach.
## Compatibility
| | |
| --- | --- |
| Target ABI | Jellyfin **10.11.x** |
| Framework | .NET 9 |
| Jellyfin | Framework | Built against | Package version |
| --- | --- | --- | --- |
| **10.11.x** | .NET 9 | `Jellyfin.Controller` 10.11.5 | `x.y.z.11` |
| **12.x** | .NET 10 | `Jellyfin.Controller` 12.0.0 | `x.y.z.12` |
Verified against the 10.11.5 SDK: `IAuthenticationProvider` + `IRequiresResolvedUser`,
`ICryptoProvider.Verify`, `IUserDataManager.UserDataSaved`, and a 255-character username limit.
One source tree, one build per server generation. Jellyfin 12 turned `IUserManager.Users` and
`UsersIds` into methods, made `ChangePassword` take a user id, dropped `HasPassword` from
`IAuthenticationProvider`, and stopped caching users in memory (every lookup is a detached copy).
Those differences live behind a `JELLYFIN_12` compile constant in
[Compat/UserManagerCompat.cs](Jellyfin.Plugin.WatchedTogether/Compat/UserManagerCompat.cs); the
rest of the plugin is identical on both.
Jellyfin installs the highest manifest version whose `targetAbi` it satisfies, which is why the two
packages of a release carry different version numbers: a 10.11 server only sees the `.11` entry,
while a 12 server sees both and takes the `.12` one.
Auto-creation depends on `UserManager.AuthenticateUser` offering unmatched usernames to every
enabled provider and re-querying the database afterwards ("the authentication provider might have
created it"). That behaviour is present in 10.11.5; if a future release changes it, auto-creation
stops working and dashboard provisioning continues to.
created it"). That behaviour is present in both 10.11.5 and 12.0; if a future release changes it,
auto-creation stops working and dashboard provisioning continues to.
Jellyfin's plugin API changes across minor versions, `IServerEntryPoint` gave way to
`IHostedService` around 10.9, and entity types moved namespaces in 10.11. Expect to rebuild against
@@ -260,26 +270,35 @@ the matching SDK when upgrading the server.
## Building
The plugin targets .NET 9. If your machine does not have that runtime, build in a container:
The solution multi-targets `net9.0` (Jellyfin 10.11) and `net10.0` (Jellyfin 12), so it needs the
.NET 10 SDK, which builds both. The test host rolls the `net9.0` run forward onto the .NET 10
runtime, so a single runtime is enough. If your machine does not have it, build in a container:
```bash
docker run --rm -v "$PWD":/src -w /src mcr.microsoft.com/dotnet/sdk:9.0 \
docker run --rm -v "$PWD":/src -w /src mcr.microsoft.com/dotnet/sdk:10.0 \
dotnet test Jellyfin.Plugin.WatchedTogether.sln -c Release
```
Or natively, with the .NET 9 SDK installed:
Or natively, with the .NET 10 SDK installed:
```bash
dotnet build Jellyfin.Plugin.WatchedTogether.sln -c Release
dotnet test Jellyfin.Plugin.WatchedTogether.sln -c Release
```
To produce an installable plugin zip:
Tests run once per framework; the `net10.0` run has one test fewer because `HasPassword` no longer
exists on Jellyfin 12's provider contract.
To produce installable plugin zips, one per Jellyfin generation:
```bash
jprm plugin build .
scripts/package.sh 10.11 0.0.5.11 # -> artifacts/watched-together_0.0.5.11.zip
scripts/package.sh 12 0.0.5.12 # -> artifacts/watched-together_0.0.5.12.zip
```
The script wraps `jprm plugin build`, stamping `build.yaml` with the right framework and
`targetAbi` for the chosen generation and restoring it afterwards.
### CI
Gitea Actions workflows live in [.gitea/workflows/](.gitea/workflows/):
@@ -287,8 +306,10 @@ Gitea Actions workflows live in [.gitea/workflows/](.gitea/workflows/):
| Workflow | Trigger | Does |
| --- | --- | --- |
| `test.yaml` | push / PR | Debug build and test run, uploads `.trx` results |
| `build.yaml` | push / PR to `master` | Release build, tests, and a date-versioned plugin zip |
| `release.yaml` | tag `v*.*.*` | Builds, creates a Gitea release, and updates `manifest.json` |
| `build.yaml` | push / PR to `master` | Release build, tests, and a date-versioned plugin zip per Jellyfin generation |
| `release.yaml` | tag `vX.Y.Z` | Builds `X.Y.Z.11` and `X.Y.Z.12`, creates a Gitea release with both, and adds both to `manifest.json` |
Release tags are three-part (`v0.0.5`); the fourth segment is reserved for the Jellyfin generation.
All three run in the builder image defined by [Dockerfile.builder](Dockerfile.builder):