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.
87 lines
2.8 KiB
C#
87 lines
2.8 KiB
C#
using System;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using MediaBrowser.Controller.Library;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Jellyfin.Plugin.WatchedTogether.Services;
|
|
|
|
/// <summary>
|
|
/// Keeps group membership consistent with the set of users that actually exist.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Jellyfin raises no user-deleted event that carries the removed id, so instead of subscribing to
|
|
/// deletions this reconciles configuration against live users at startup. Stale entries are also
|
|
/// skipped at read time by <see cref="GroupService.GetEligibleMembers"/>, so this is about keeping
|
|
/// stored configuration tidy and disabling groups that have fallen below two members.
|
|
/// </remarks>
|
|
public sealed class UserLifecycleService : IHostedService
|
|
{
|
|
private readonly IUserManager _userManager;
|
|
private readonly IGroupService _groupService;
|
|
private readonly ILogger<UserLifecycleService> _logger;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="UserLifecycleService"/> class.
|
|
/// </summary>
|
|
/// <param name="userManager">The user manager.</param>
|
|
/// <param name="groupService">The group service.</param>
|
|
/// <param name="logger">The logger.</param>
|
|
public UserLifecycleService(
|
|
IUserManager userManager,
|
|
IGroupService groupService,
|
|
ILogger<UserLifecycleService> logger)
|
|
{
|
|
_userManager = userManager;
|
|
_groupService = groupService;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
Reconcile();
|
|
}
|
|
#pragma warning disable CA1031 // Reconciliation must never prevent the server from starting.
|
|
catch (Exception ex)
|
|
#pragma warning restore CA1031
|
|
{
|
|
_logger.LogError(ex, "Failed to reconcile Watched Together groups at startup");
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
|
|
|
/// <summary>
|
|
/// Removes references to users that no longer exist.
|
|
/// </summary>
|
|
private void Reconcile()
|
|
{
|
|
var config = Plugin.Instance?.Configuration;
|
|
if (config is null || config.Groups.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var liveIds = _userManager.UsersIds.ToHashSet();
|
|
|
|
var referenced = config.Groups
|
|
.SelectMany(g => g.MemberUserIds.Append(g.SharedUserId))
|
|
.Distinct()
|
|
.ToList();
|
|
|
|
foreach (var id in referenced.Where(id => !liveIds.Contains(id)))
|
|
{
|
|
_logger.LogInformation("Pruning deleted user {UserId} from Watched Together groups", id);
|
|
_groupService.PruneDeletedUser(id);
|
|
}
|
|
}
|
|
}
|