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>
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ätzlichep: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.
- Film:
- 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,ItemUpdatedundItemRemovedim 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.
- Dashboard → Plugins → Repositories → +
- Name:
Recently Added Guard, URL:https://git.menzel.center/menzeljonas/jellyfin-plugin-recently-added-guard/raw/branch/manifest/manifest.json - Katalog → Recently Added Guard → Installieren
- 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
DateCreatedverä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
IExternalDateSourceist 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:
- Upgrade mit neuem Dateinamen (Film mit
{tmdb-…}, Episode): Das alte Datum bleibt erhalten. Ein echt neuer Film steht oben inGET /Items/Latest. - Datei am selben Pfad ersetzt: Das Datum wird zurückgesetzt.
- Film ohne ID im Ordnernamen: Die Korrektur erfolgt nach dem Metadatenabruf.
- DryRun ändert nichts, eine Rückkehr nach Ablauf der Frist gilt als neu.
- 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:
- Jellyfin 12.2 per Docker starten (
docker run -p 8096:8096 -v ./config:/config -v ./media:/media jellyfin/jellyfin:12.2). Das ZIP nachconfig/plugins/Recently Added Guard_1.0.0.0/entpacken. - Eine Filmbibliothek mit
media/movies/The Matrix (1999) {tmdb-603}/The Matrix (1999) WEBDL-720p.mkvanlegen und scannen. - Im Metadaten-Editor „Hinzugefügt am“ auf ein altes Datum setzen. Danach einen anderen Film hinzufügen.
- Die Datei in
… Bluray-1080p.mkvumbenennen bzw. ersetzen und die Bibliothek scannen. - Mit
GET /Items/Latest?userId=…&parentId=<Bibliothek>prüfen: Matrix steht nicht vorne, seinDateCreatedist das alte Datum. Im Log stehtUpgrade 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 undmeta.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.jsonliegt ausschließlich auf dem Branchmanifestund 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-62a057c95574darf sich nie ändern.