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

11 KiB
Raw Permalink Blame History

Recently Added Guard

Jellyfin-Plugin (Jellyfin 12.2, .NET 10), das Qualitäts-Upgrades von Radarr und Sonarr aus „Zuletzt hinzugefügt“ fernhält. Dort erscheinen nur noch Filme und Episoden, die wirklich neu sind.

Warum

Radarr und Sonarr ersetzen Filme und Episoden automatisch durch bessere Versionen. Die neue Datei bekommt meist einen neuen Namen, weil Qualität und Release-Infos darin stehen. Jellyfin identifiziert Items über den absoluten Pfad. Für einen neuen Pfad legt es deshalb ein neues Item an, dessen „Hinzugefügt am“ (DateCreated) jetzt ist. Das Upgrade landet unter „Zuletzt hinzugefügt“, als wäre es neu.

Auch am selben Pfad kann das Datum springen. Ändert sich die Datei, setzt Jellyfin DateCreated beim nächsten Metadaten-Refresh auf die Erstellungszeit der Datei.

Funktionsweise

Das Plugin führt ein Ledger mit dem ersten Hinzufügedatum pro logischem Medium, also pro Film oder Episode und nicht pro Datei.

  • Identität über Alias-Keys statt über Pfade:
    • Film: movie:tmdb:{id}, movie:imdb:{id}, movie:tvdb:{id}
    • Episode: ep:tvdb|tmdb|imdb:{Serien-ID}:s{Staffel}e{Episode} (Doppelfolgen: s1e1-2) und immer zusätzlich ep:series-guid:{Jellyfin-Serien-ID}:…. Damit greift der Schutz auch bei Serien ohne Provider-IDs.
    • Ein Eintrag sammelt alle Keys. Ein Treffer über irgendeinen Key genügt, neue Keys werden ergänzt.
  • Neues Medium: Das Plugin legt einen Eintrag an und ändert nichts am Item.
  • Upgrade: Ein Item ist jünger als der Ledger-Eintrag (Toleranz 60 s), und das Medium ist entweder noch vorhanden oder wurde vor weniger als 14 Tagen gelöscht. Dann setzt das Plugin das „Hinzugefügt am“ des Items auf das gespeicherte Datum zurück. Bei aktiviertem NFO-Speichern schreibt Jellyfin dabei auch <dateadded> neu.
  • Rückkehr nach Ablauf der Frist: Taucht ein Medium erst später als 14 Tage nach dem Löschen wieder auf, gilt es als neu.
  • Älteres Datum (z. B. aus einer NFO oder manuell vorverlegt): Der Ledger übernimmt immer das Minimum.
  • Das Plugin wertet ItemAdded, ItemUpdated und ItemRemoved im Hintergrund aus, entprellt und ohne Jellyfin zu blockieren. Überschreibt eine noch laufende Metadaten-Aktualisierung das Datum erneut, setzt das Plugin es wieder zurück.
  • Seed: Beim ersten Start registriert das Plugin alle vorhandenen Filme und Episoden, ohne etwas zu ändern.
  • Scheduled Task „Ledger synchronisieren“ (Kategorie „Recently Added Guard“, täglich um 03:00): Er registriert unbekannte Items, trägt gelöschte aus und entfernt Einträge, die länger als Frist + 1 Tag gelöscht sind. Auch er ändert keine Items.

Das Ledger liegt unter <Jellyfin-Config>/plugins/Jellyfin.Plugin.RecentlyAddedGuard/ledger.json, im Docker-Container also unter /config/plugins/…. Nimm die Datei in dein Backup auf. Löschst du sie und startest Jellyfin neu, läuft der Seed erneut.

Installation

Jellyfin lädt manifest.json und das Plugin-ZIP ohne Anmeldung. Das Repository ist deshalb öffentlich.

  1. Dashboard → Plugins → Repositories → +
  2. Name: Recently Added Guard, URL:
    https://git.menzel.center/menzeljonas/jellyfin-plugin-recently-added-guard/raw/branch/manifest/manifest.json
    
  3. Katalog → Recently Added Guard → Installieren
  4. Jellyfin neu starten. Danach läuft der Seed im Hintergrund. Im Log erscheint Ledger-Seed abgeschlossen ….

Updates erscheinen anschließend wie bei offiziellen Plugins im Katalog bzw. unter „Meine Plugins“.

Wichtig: Deinstalliere „DateAdded Advanced“ (oder ähnliche Plugins), damit nicht zwei Plugins DateCreated verändern.

Konfiguration

Dashboard → Plugins → Recently Added Guard:

Einstellung Standard Bedeutung
Frist in Tagen (GraceDays) 14 Taucht ein gelöschtes Medium innerhalb dieser Frist wieder auf, gilt es als Upgrade und bekommt das alte Datum. Danach gilt es als neu. Radarr/Sonarr ersetzen Dateien meist innerhalb von Sekunden; die Frist deckt auch längere Lücken ab, etwa wenn der Download des Upgrades erst später fertig wird.
Toleranz in Sekunden (ToleranceSeconds) 60 Kleinere Abweichungen des Datums werden ignoriert.
Filme schützen (EnableMovies) an
Episoden schützen (EnableEpisodes) an
Testmodus (DryRun) aus Upgrades nur protokollieren, kein Datum ändern.

Die Seite zeigt außerdem die Ledger-Statistik, die Korrekturen seit dem Start, eine Tabelle der letzten 100 Korrekturen (Name, altes Datum, neues Datum, Zeitpunkt) und einen Button „Ledger synchronisieren“.

Die Daten kommen aus GET /RecentlyAddedGuard/Status. POST /RecentlyAddedGuard/Sync startet den Scheduled Task. Beide Endpunkte erfordern Administratorrechte.

Korrekturen erscheinen im Jellyfin-Log auf Level Information:

Upgrade erkannt: Matrix (1999) – DateAdded 2026-10-21 21:02:15Z → 2024-01-01 00:00:00Z

Grenzen

  • Bereits falsch datierte Items werden nicht korrigiert. Das Plugin schützt nur vor zukünftigen Upgrades. Geplant für v2 ist ein Abgleich mit der Import-Historie von Radarr/Sonarr. Die Schnittstelle IExternalDateSource ist dafür vorbereitet.
  • Filme ohne TMDb-ID im Ordnernamen haben beim Hinzufügen noch keine Provider-ID. Sie werden erst nach dem Metadatenabruf erkannt und können bis dahin kurz unter „Zuletzt hinzugefügt“ auftauchen. Abhilfe: In Radarr als Ordnerformat z. B. {Movie CleanTitle} ({Release Year}) {tmdb-{TmdbId}} verwenden. Jellyfin liest die ID dann direkt aus dem Pfad. Episoden sind über die Serie sofort zuordenbar.
  • „Neues Item“-Benachrichtigungen anderer Plugins (z. B. Webhook) feuern weiterhin, weil Jellyfin das Item tatsächlich neu anlegt.
  • Manuelles Ändern von „Hinzugefügt am“ im Metadaten-Editor: Vorverlegen senkt das gespeicherte Datum. Ein Nachverlegen setzt das Plugin wieder zurück.
  • Mehrere Dateien desselben Mediums (alternative Versionen, z. B. eine 4K-Kopie in einer eigenen Bibliothek) teilen sich absichtlich einen Eintrag. Eine später hinzugefügte zweite Version erscheint deshalb nicht als neu.
  • Ändert sich die Staffel- oder Episodennummer einer Datei, gilt sie als anderes Medium.

Entwicklung

Voraussetzung: .NET SDK 10 oder Docker.

dotnet build -c Release   # Warnungen sind im Release-Build Fehler
dotnet test

# ohne lokales SDK
docker run --rm -v "$PWD":/src -w /src mcr.microsoft.com/dotnet/sdk:10.0 dotnet test -c Release

Aufbau:

Pfad Inhalt
Core/ Kernlogik ohne Jellyfin-Typen: Reconciler (Abgleich), MediaKeys, GuardProcessor (Korrektur/DryRun), CorrectionLog. Dazu IdentityResolver (Jellyfin-Item → MediaIdentity)
Ledger/ LedgerStore: Persistenz, Indizes nach Key und Item-ID, thread-safe, atomares und entprelltes Speichern
Services/ GuardHostedService (Events → Channel → Worker, Seed), LedgerSynchronizer, LibraryItemDateWriter
Tasks/ Scheduled Task „Ledger synchronisieren“
Api/ GuardController (/RecentlyAddedGuard/Status, /RecentlyAddedGuard/Sync)
Configuration/ Konfiguration und eingebettete Konfigurationsseite

Kompiliert wird gegen die NuGet-Pakete Jellyfin.Controller/Jellyfin.Model 12.2.0 (ExcludeAssets="runtime"), das ZIP enthält nur die Plugin-DLL. targetAbi ist 12.2.0.0.

Integrationstest mit Jellyfin 12.2

integration-test/it.py startet eine Wegwerf-Instanz von jellyfin/jellyfin:12.2 auf 127.0.0.1:18096. Das Plugin installiert es so, wie es der Katalog tut (plugins/Recently Added Guard_<Version>/). Danach erzeugt das Skript per jellyfin-ffmpeg eine Mini-Bibliothek und prüft:

  1. Upgrade mit neuem Dateinamen (Film mit {tmdb-…}, Episode): Das alte Datum bleibt erhalten. Ein echt neuer Film steht oben in GET /Items/Latest.
  2. Datei am selben Pfad ersetzt: Das Datum wird zurückgesetzt.
  3. Film ohne ID im Ordnernamen: Die Korrektur erfolgt nach dem Metadatenabruf.
  4. DryRun ändert nichts, eine Rückkehr nach Ablauf der Frist gilt als neu.
  5. Status-API, Konfigurationsseite, Scheduled Task, Persistenz über einen Neustart.
# Plugin-ZIP bauen (wie die CI) und testen
docker run --rm -v "$PWD":/src:ro -v "$PWD/artifacts":/out mcr.microsoft.com/dotnet/sdk:10.0 bash -c '
  cp -r /src /work && cd /work && rm -rf */bin */obj
  apt-get update -qq && apt-get install -y -qq python3-venv >/dev/null
  python3 -m venv /opt/jprm && /opt/jprm/bin/pip install -q jprm==1.1.0
  /opt/jprm/bin/jprm plugin build . --output /out --version 1.0.0.0'
python3 integration-test/it.py artifacts/recently-added-guard_1.0.0.0.zip

Von Hand geht es so:

  1. Jellyfin 12.2 per Docker starten (docker run -p 8096:8096 -v ./config:/config -v ./media:/media jellyfin/jellyfin:12.2). Das ZIP nach config/plugins/Recently Added Guard_1.0.0.0/ entpacken.
  2. Eine Filmbibliothek mit media/movies/The Matrix (1999) {tmdb-603}/The Matrix (1999) WEBDL-720p.mkv anlegen und scannen.
  3. Im Metadaten-Editor „Hinzugefügt am“ auf ein altes Datum setzen. Danach einen anderen Film hinzufügen.
  4. Die Datei in … Bluray-1080p.mkv umbenennen bzw. ersetzen und die Bibliothek scannen.
  5. Mit GET /Items/Latest?userId=…&parentId=<Bibliothek> prüfen: Matrix steht nicht vorne, sein DateCreated ist das alte Datum. Im Log steht Upgrade erkannt: ….

Release und CI

Workflow Auslöser Wirkung
.gitea/workflows/build.yaml Push (außer Branch manifest), Pull Request dotnet restore, build -c Release (Warnungen = Fehler), test im Container mcr.microsoft.com/dotnet/sdk:10.0
.gitea/workflows/release.yaml Tag v* Tests → ZIP mit jprm → Gitea-Release mit ZIP → manifest.json auf Branch manifest → Prüfung von anonymem Abruf und MD5

So veröffentlichst du ein Release:

git tag -a v1.0.1.0 -m "Kurzer Changelog für Jellyfin"
git push origin v1.0.1.0
  • Die Version muss vierteilig sein (vA.B.C.D). Die CI schreibt sie in Assembly und meta.json.
  • Der Changelog kommt aus der Nachricht des annotierten Tags. Bei einem leichtgewichtigen Tag bilden die Commit-Betreffs seit dem letzten Tag den Changelog.
  • Die CI nutzt nur das automatische GITEA_TOKEN (permissions: contents: write), ein eigenes Secret ist nicht nötig. Commits mit diesem Token starten in Gitea keine weiteren Workflows.
  • manifest.json liegt ausschließlich auf dem Branch manifest und wird nur von der CI gepflegt. Die neue Version kommt nach vorne, alte Versionen bleiben erhalten (checksum = MD5 des ZIP, targetAbi = 12.2.0.0).
  • Die Plugin-GUID 6fdfedef-adde-4ac3-a873-62a057c95574 darf sich nie ändern.