using System; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.LiveTv; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.PersonalRecordings.Services; /// /// Captures which Jellyfin user created Live TV timers / series timers. /// public class TimerOwnershipHost : IHostedService { private const string UserIdClaimType = "Jellyfin-UserId"; private readonly ILogger _logger; private readonly ILiveTvManager _liveTvManager; private readonly IUserManager _userManager; private readonly IHttpContextAccessor _httpContextAccessor; private readonly OwnershipStore _ownershipStore; /// /// Initializes a new instance of the class. /// public TimerOwnershipHost( ILogger logger, ILiveTvManager liveTvManager, IUserManager userManager, IHttpContextAccessor httpContextAccessor, OwnershipStore ownershipStore) { _logger = logger; _liveTvManager = liveTvManager; _userManager = userManager; _httpContextAccessor = httpContextAccessor; _ownershipStore = ownershipStore; } /// public Task StartAsync(CancellationToken cancellationToken) { _liveTvManager.TimerCreated += OnTimerCreated; _liveTvManager.SeriesTimerCreated += OnSeriesTimerCreated; _liveTvManager.TimerCancelled += OnTimerCancelled; _liveTvManager.SeriesTimerCancelled += OnSeriesTimerCancelled; _logger.LogInformation("Personal Recordings: listening for Live TV timer events"); return Task.CompletedTask; } /// public Task StopAsync(CancellationToken cancellationToken) { _liveTvManager.TimerCreated -= OnTimerCreated; _liveTvManager.SeriesTimerCreated -= OnSeriesTimerCreated; _liveTvManager.TimerCancelled -= OnTimerCancelled; _liveTvManager.SeriesTimerCancelled -= OnSeriesTimerCancelled; return Task.CompletedTask; } private async void OnTimerCreated(object? sender, GenericEventArgs e) { try { var timerId = e.Argument.Id; if (string.IsNullOrWhiteSpace(timerId)) { return; } TimerInfoDto? timer = null; try { timer = await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { _logger.LogDebug(ex, "Could not load timer {TimerId} after create", timerId); } var seriesTimerId = timer?.SeriesTimerId; if (TryResolveUser(out var userId, out var username)) { _ownershipStore.SetTimerOwner( timerId, userId, username, seriesTimerId, timer?.Name, timer?.StartDate); return; } if (!string.IsNullOrWhiteSpace(seriesTimerId) && _ownershipStore.TryGetSeriesOwner(seriesTimerId, out var seriesOwner)) { _ownershipStore.SetTimerOwner( timerId, seriesOwner.UserId, seriesOwner.Username, seriesTimerId, timer?.Name ?? seriesOwner.Name, timer?.StartDate); return; } _logger.LogWarning( "Timer {TimerId} created without resolvable user context; recording will not be moved", timerId); } catch (Exception ex) { _logger.LogError(ex, "Error handling TimerCreated"); } } private async void OnSeriesTimerCreated(object? sender, GenericEventArgs e) { try { var seriesTimerId = e.Argument.Id; if (string.IsNullOrWhiteSpace(seriesTimerId)) { return; } SeriesTimerInfoDto? seriesTimer = null; try { seriesTimer = await _liveTvManager.GetSeriesTimer(seriesTimerId, CancellationToken.None) .ConfigureAwait(false); } catch (Exception ex) { _logger.LogDebug(ex, "Could not load series timer {SeriesTimerId} after create", seriesTimerId); } if (!TryResolveUser(out var userId, out var username)) { _logger.LogWarning( "Series timer {SeriesTimerId} created without resolvable user context", seriesTimerId); return; } _ownershipStore.SetSeriesOwner(seriesTimerId, userId, username, seriesTimer?.Name); } catch (Exception ex) { _logger.LogError(ex, "Error handling SeriesTimerCreated"); } } private void OnTimerCancelled(object? sender, GenericEventArgs e) { try { if (!string.IsNullOrWhiteSpace(e.Argument.Id)) { _ownershipStore.RemoveTimer(e.Argument.Id); } } catch (Exception ex) { _logger.LogError(ex, "Error handling TimerCancelled"); } } private void OnSeriesTimerCancelled(object? sender, GenericEventArgs e) { try { if (!string.IsNullOrWhiteSpace(e.Argument.Id)) { _ownershipStore.RemoveSeriesTimer(e.Argument.Id); } } catch (Exception ex) { _logger.LogError(ex, "Error handling SeriesTimerCancelled"); } } private bool TryResolveUser(out Guid userId, out string username) { userId = default; username = string.Empty; var principal = _httpContextAccessor.HttpContext?.User; if (principal is null) { return false; } var claim = principal.FindFirst(UserIdClaimType)?.Value ?? principal.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (string.IsNullOrWhiteSpace(claim) || !Guid.TryParse(claim, out userId) || userId == Guid.Empty) { return false; } var user = _userManager.GetUserById(userId); if (user is null || string.IsNullOrWhiteSpace(user.Username)) { return false; } username = user.Username; return true; } }