using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.RecentlyAddedGuard.Core; /// /// Turns reconciler decisions into actions: restores the date of upgrades (or only logs them in dry-run mode) /// and records corrections. /// public sealed class GuardProcessor { private readonly Reconciler _reconciler; private readonly IItemDateWriter _writer; private readonly CorrectionLog _corrections; private readonly Func _options; private readonly TimeProvider _timeProvider; private readonly ILogger _logger; private readonly ConcurrentDictionary _dryRunReported = new(); /// /// Initializes a new instance of the class. /// /// The reconciler. /// Writes item dates. /// The correction log. /// Provides the current options. /// The clock. /// The logger. public GuardProcessor( Reconciler reconciler, IItemDateWriter writer, CorrectionLog corrections, Func options, TimeProvider timeProvider, ILogger logger) { _reconciler = reconciler ?? throw new ArgumentNullException(nameof(reconciler)); _writer = writer ?? throw new ArgumentNullException(nameof(writer)); _corrections = corrections ?? throw new ArgumentNullException(nameof(corrections)); _options = options ?? throw new ArgumentNullException(nameof(options)); _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// /// Handles an added or updated item. /// /// The item. /// The cancellation token. /// The reconciler's outcome. public async Task ProcessSeenAsync(MediaIdentity identity, CancellationToken cancellationToken) { var result = _reconciler.OnItemSeen(identity); switch (result.Action) { case ReconcileAction.UpgradeDetected: await RestoreDateAsync(result, cancellationToken).ConfigureAwait(false); break; case ReconcileAction.ReturnedAfterGrace: _logger.LogInformation( "{Name} ist nach Ablauf der Frist zurückgekehrt und gilt als neu (DateAdded {Date:u})", identity.DisplayName, result.ItemDate); break; case ReconcileAction.Registered: _logger.LogDebug("Neu registriert: {Name} (DateAdded {Date:u})", identity.DisplayName, result.ItemDate); break; case ReconcileAction.FirstAddedLowered: _logger.LogDebug("Erstes Hinzufügedatum von {Name} auf {Date:u} vorverlegt", identity.DisplayName, result.ItemDate); break; default: break; } return result; } /// /// Handles a removed item. /// /// The removed item. /// The keys the item still resolves to; may be empty. /// true if a ledger entry was affected. public bool ProcessRemoved(Guid itemId, IReadOnlyList keys) => _reconciler.OnItemRemoved(itemId, keys); private async Task RestoreDateAsync(ReconcileResult result, CancellationToken cancellationToken) { var identity = result.Identity; if (_options().DryRun) { // Every metadata refresh of the item reports the upgrade again; log it once per date. if (_dryRunReported.TryGetValue(identity.ItemId, out var reported) && reported == result.ItemDate) { return; } _dryRunReported[identity.ItemId] = result.ItemDate; _logger.LogInformation( "[DryRun] Upgrade erkannt: {Name} – DateAdded {New:u} → {Original:u} (nicht geändert)", identity.DisplayName, result.ItemDate, result.FirstAdded); _corrections.Add(new CorrectionRecord(identity.ItemId, identity.DisplayName, result.ItemDate, result.FirstAdded, UtcNow(), DryRun: true)); return; } if (!await _writer.TrySetDateCreatedAsync(identity.ItemId, result.FirstAdded, cancellationToken).ConfigureAwait(false)) { _logger.LogDebug("Item {ItemId} existiert nicht mehr, keine Korrektur", identity.ItemId); return; } _dryRunReported.TryRemove(identity.ItemId, out _); var record = new CorrectionRecord(identity.ItemId, identity.DisplayName, result.ItemDate, result.FirstAdded, UtcNow(), DryRun: false); if (_corrections.Add(record)) { _logger.LogInformation( "Upgrade erkannt: {Name} – DateAdded {New:u} → {Original:u}", identity.DisplayName, result.ItemDate, result.FirstAdded); } else { _logger.LogDebug( "DateAdded von {Name} wurde von Jellyfin erneut überschrieben und wieder auf {Original:u} gesetzt", identity.DisplayName, result.FirstAdded); } } private DateTime UtcNow() => _timeProvider.GetUtcNow().UtcDateTime; }