217 lines
6.8 KiB
C#
217 lines
6.8 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Captures which Jellyfin user created Live TV timers / series timers.
|
|
/// </summary>
|
|
public class TimerOwnershipHost : IHostedService
|
|
{
|
|
private const string UserIdClaimType = "Jellyfin-UserId";
|
|
|
|
private readonly ILogger<TimerOwnershipHost> _logger;
|
|
private readonly ILiveTvManager _liveTvManager;
|
|
private readonly IUserManager _userManager;
|
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
private readonly OwnershipStore _ownershipStore;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="TimerOwnershipHost"/> class.
|
|
/// </summary>
|
|
public TimerOwnershipHost(
|
|
ILogger<TimerOwnershipHost> logger,
|
|
ILiveTvManager liveTvManager,
|
|
IUserManager userManager,
|
|
IHttpContextAccessor httpContextAccessor,
|
|
OwnershipStore ownershipStore)
|
|
{
|
|
_logger = logger;
|
|
_liveTvManager = liveTvManager;
|
|
_userManager = userManager;
|
|
_httpContextAccessor = httpContextAccessor;
|
|
_ownershipStore = ownershipStore;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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<TimerEventInfo> 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<TimerEventInfo> 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<TimerEventInfo> 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<TimerEventInfo> 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;
|
|
}
|
|
}
|