Keeps Radarr/Sonarr quality upgrades out of "Recently Added" by keeping a ledger of the first date added per logical medium (movie or episode, identified by provider ids and the series) and restoring it on upgrades. - core logic without Jellyfin types (Reconciler, MediaKeys, GuardProcessor) - thread-safe ledger with atomic, debounced persistence - event processing via channel and background worker, seed on first start - scheduled task "Ledger synchronisieren", status API, configuration page - xUnit tests and a Jellyfin 12.2 integration test - Gitea workflows: build/test, release with jprm and manifest branch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
3.7 KiB
C#
117 lines
3.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
|
|
namespace Jellyfin.Plugin.RecentlyAddedGuard.Core;
|
|
|
|
/// <summary>
|
|
/// One restored (or, in dry-run mode, detected) upgrade.
|
|
/// </summary>
|
|
/// <param name="ItemId">The corrected item.</param>
|
|
/// <param name="Name">The display name.</param>
|
|
/// <param name="PreviousDate">The "date added" the upgrade brought along (UTC).</param>
|
|
/// <param name="RestoredDate">The first "date added" from the ledger (UTC).</param>
|
|
/// <param name="CorrectedAt">When the correction happened (UTC).</param>
|
|
/// <param name="DryRun"><c>true</c> if only detected, not applied.</param>
|
|
public sealed record CorrectionRecord(
|
|
Guid ItemId,
|
|
string Name,
|
|
DateTime PreviousDate,
|
|
DateTime RestoredDate,
|
|
DateTime CorrectedAt,
|
|
bool DryRun);
|
|
|
|
/// <summary>
|
|
/// In-memory ring buffer of the most recent corrections, for the configuration page.
|
|
/// </summary>
|
|
public sealed class CorrectionLog
|
|
{
|
|
/// <summary>
|
|
/// The default number of kept corrections.
|
|
/// </summary>
|
|
public const int DefaultCapacity = 100;
|
|
|
|
private readonly Lock _sync = new();
|
|
private readonly List<CorrectionRecord> _records;
|
|
private readonly int _capacity;
|
|
private long _corrections;
|
|
private long _dryRunDetections;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="CorrectionLog"/> class.
|
|
/// </summary>
|
|
/// <param name="capacity">The number of kept corrections.</param>
|
|
public CorrectionLog(int capacity = DefaultCapacity)
|
|
{
|
|
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity);
|
|
_capacity = capacity;
|
|
_records = new List<CorrectionRecord>(capacity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the number of applied corrections since the server started.
|
|
/// </summary>
|
|
public long CorrectionsSinceStart => Interlocked.Read(ref _corrections);
|
|
|
|
/// <summary>
|
|
/// Gets the number of upgrades detected in dry-run mode since the server started.
|
|
/// </summary>
|
|
public long DryRunDetectionsSinceStart => Interlocked.Read(ref _dryRunDetections);
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="record">The correction.</param>
|
|
/// <returns><c>true</c> for a new correction, <c>false</c> for a repeated one.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the kept corrections, newest first.
|
|
/// </summary>
|
|
/// <returns>The corrections.</returns>
|
|
public IReadOnlyList<CorrectionRecord> GetRecent()
|
|
{
|
|
lock (_sync)
|
|
{
|
|
return _records.AsEnumerable().Reverse().ToList();
|
|
}
|
|
}
|
|
}
|