using System;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.WatchedTogether.Services;
///
/// Propagates played state from a shared account to each of its members, one way.
///
public sealed class WatchedStateSyncService : IHostedService, IDisposable
{
private readonly IUserDataManager _userDataManager;
private readonly IUserManager _userManager;
private readonly IGroupService _groupService;
private readonly ILogger _logger;
private bool _disposed;
///
/// Initializes a new instance of the class.
///
/// The user data manager.
/// The user manager.
/// The group service.
/// The logger.
public WatchedStateSyncService(
IUserDataManager userDataManager,
IUserManager userManager,
IGroupService groupService,
ILogger logger)
{
_userDataManager = userDataManager;
_userManager = userManager;
_groupService = groupService;
_logger = logger;
}
///
public Task StartAsync(CancellationToken cancellationToken)
{
_userDataManager.UserDataSaved += OnUserDataSaved;
_logger.LogInformation("Watched Together sync started");
return Task.CompletedTask;
}
///
public Task StopAsync(CancellationToken cancellationToken)
{
_userDataManager.UserDataSaved -= OnUserDataSaved;
_logger.LogInformation("Watched Together sync stopped");
return Task.CompletedTask;
}
///
public void Dispose()
{
if (_disposed)
{
return;
}
_userDataManager.UserDataSaved -= OnUserDataSaved;
_disposed = true;
}
///
/// Mirrors a shared account's played state onto its members.
///
///
/// No loop guard is needed. Writing to a member raises this event again with that member's id,
/// which is not a shared account id, so the handler returns immediately. The
/// Played equality check below suppresses redundant writes on top of that.
///
private void OnUserDataSaved(object? sender, UserDataSaveEventArgs e)
{
if (e?.UserData is null || e.Item is null)
{
return;
}
// UserDataSaved fires constantly during playback (progress ticks); only act on the reasons
// that actually represent a change in watched state.
if (e.SaveReason is not (UserDataSaveReason.PlaybackFinished
or UserDataSaveReason.TogglePlayed
or UserDataSaveReason.Import))
{
return;
}
var group = _groupService.GetGroupForSharedUser(e.UserId);
if (group is null)
{
return;
}
var played = e.UserData.Played;
if (!played && !group.SyncUnwatched)
{
return;
}
foreach (var member in _groupService.GetEligibleMembers(group))
{
try
{
var data = _userDataManager.GetUserData(member, e.Item);
if (data is null || data.Played == played)
{
continue;
}
data.Played = played;
if (group.SyncPlayCount && played && data.PlayCount < 1)
{
data.PlayCount = 1;
}
_userDataManager.SaveUserData(
member,
e.Item,
data,
UserDataSaveReason.TogglePlayed,
CancellationToken.None);
_logger.LogDebug(
"Synced played={Played} for {ItemName} to member {MemberUsername}",
played,
e.Item.Name,
member.Username);
}
#pragma warning disable CA1031 // One member failing must not stop the rest from syncing.
catch (Exception ex)
#pragma warning restore CA1031
{
_logger.LogError(
ex,
"Failed to sync played state for {ItemName} to member {MemberId}",
e.Item.Name,
member.Id);
}
}
}
}