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;
}
}