using System; using System.Collections.Generic; using System.Linq; using System.Threading; namespace Jellyfin.Plugin.RecentlyAddedGuard.Core; /// /// One restored (or, in dry-run mode, detected) upgrade. /// /// The corrected item. /// The display name. /// The "date added" the upgrade brought along (UTC). /// The first "date added" from the ledger (UTC). /// When the correction happened (UTC). /// true if only detected, not applied. public sealed record CorrectionRecord( Guid ItemId, string Name, DateTime PreviousDate, DateTime RestoredDate, DateTime CorrectedAt, bool DryRun); /// /// In-memory ring buffer of the most recent corrections, for the configuration page. /// public sealed class CorrectionLog { /// /// The default number of kept corrections. /// public const int DefaultCapacity = 100; private readonly Lock _sync = new(); private readonly List _records; private readonly int _capacity; private long _corrections; private long _dryRunDetections; /// /// Initializes a new instance of the class. /// /// The number of kept corrections. public CorrectionLog(int capacity = DefaultCapacity) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity); _capacity = capacity; _records = new List(capacity); } /// /// Gets the number of applied corrections since the server started. /// public long CorrectionsSinceStart => Interlocked.Read(ref _corrections); /// /// Gets the number of upgrades detected in dry-run mode since the server started. /// public long DryRunDetectionsSinceStart => Interlocked.Read(ref _dryRunDetections); /// /// Records a correction, dropping the oldest one when full. Restoring the same date of the same item again /// (Jellyfin saved the item with its new date once more, e.g. from a metadata refresh that was already running) /// replaces the earlier record and is not counted twice. /// /// The correction. /// true for a new correction, false for a repeated one. public bool Add(CorrectionRecord record) { ArgumentNullException.ThrowIfNull(record); lock (_sync) { var existing = _records.FindIndex(r => r.ItemId == record.ItemId && r.RestoredDate == record.RestoredDate && r.DryRun == record.DryRun); if (existing >= 0) { _records.RemoveAt(existing); _records.Add(record); return false; } if (_records.Count == _capacity) { _records.RemoveAt(0); } _records.Add(record); } if (record.DryRun) { Interlocked.Increment(ref _dryRunDetections); } else { Interlocked.Increment(ref _corrections); } return true; } /// /// Returns the kept corrections, newest first. /// /// The corrections. public IReadOnlyList GetRecent() { lock (_sync) { return _records.AsEnumerable().Reverse().ToList(); } } }