Files
Jonas MenzelandClaude Opus 5.5 d431486095
build / build (push) Successful in 1m10s
release / release (push) Failing after 1m36s
feat: Recently Added Guard plugin for Jellyfin 12.2
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>
2026-10-21 23:10:02 +02:00

89 lines
3.6 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
namespace Jellyfin.Plugin.RecentlyAddedGuard.Core;
/// <summary>
/// Builds the alias keys that identify a logical medium independent of its file path.
/// </summary>
/// <remarks>
/// Keys are compared case-insensitively by the ledger, so provider ids are only trimmed.
/// </remarks>
public static class MediaKeys
{
/// <summary>
/// Builds the keys of a movie from its provider ids.
/// </summary>
/// <param name="tmdbId">The TMDb id, if known.</param>
/// <param name="imdbId">The IMDb id, if known.</param>
/// <param name="tvdbId">The TVDb id, if known.</param>
/// <returns>The keys; empty if the movie has no provider id yet.</returns>
public static IReadOnlyList<string> ForMovie(string? tmdbId, string? imdbId, string? tvdbId)
{
var keys = new List<string>(3);
AddIfPresent(keys, "movie:tmdb:", tmdbId, string.Empty);
AddIfPresent(keys, "movie:imdb:", imdbId, string.Empty);
AddIfPresent(keys, "movie:tvdb:", tvdbId, string.Empty);
return keys;
}
/// <summary>
/// Builds the episode part of an episode key, e.g. <c>s1e5</c> or <c>s1e5-6</c> for a double episode.
/// </summary>
/// <param name="seasonNumber">The season number (<c>ParentIndexNumber</c>).</param>
/// <param name="episodeNumber">The episode number (<c>IndexNumber</c>).</param>
/// <param name="episodeNumberEnd">The last episode number of a multi-episode file (<c>IndexNumberEnd</c>).</param>
/// <returns>The episode part, or <c>null</c> if season or episode number is missing.</returns>
public static string? EpisodePart(int? seasonNumber, int? episodeNumber, int? episodeNumberEnd)
{
if (seasonNumber is not { } season || episodeNumber is not { } episode)
{
return null;
}
return episodeNumberEnd is { } end && end > episode
? string.Create(CultureInfo.InvariantCulture, $"s{season}e{episode}-{end}")
: string.Create(CultureInfo.InvariantCulture, $"s{season}e{episode}");
}
/// <summary>
/// Builds the keys of an episode.
/// </summary>
/// <param name="episodePart">The episode part from <see cref="EpisodePart"/>.</param>
/// <param name="seriesTvdbId">The series' TVDb id, if known.</param>
/// <param name="seriesTmdbId">The series' TMDb id, if known.</param>
/// <param name="seriesImdbId">The series' IMDb id, if known.</param>
/// <param name="seriesId">The Jellyfin id of the series; <see cref="Guid.Empty"/> if unknown.</param>
/// <returns>The keys; contains the series-guid key whenever the series id is known.</returns>
public static IReadOnlyList<string> ForEpisode(
string episodePart,
string? seriesTvdbId,
string? seriesTmdbId,
string? seriesImdbId,
Guid seriesId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(episodePart);
var suffix = ":" + episodePart;
var keys = new List<string>(4);
AddIfPresent(keys, "ep:tvdb:", seriesTvdbId, suffix);
AddIfPresent(keys, "ep:tmdb:", seriesTmdbId, suffix);
AddIfPresent(keys, "ep:imdb:", seriesImdbId, suffix);
if (seriesId != Guid.Empty)
{
keys.Add("ep:series-guid:" + seriesId.ToString("N", CultureInfo.InvariantCulture) + suffix);
}
return keys;
}
private static void AddIfPresent(List<string> keys, string prefix, string? id, string suffix)
{
if (!string.IsNullOrWhiteSpace(id))
{
keys.Add(prefix + id.Trim() + suffix);
}
}
}