# 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 `` 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 `/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. ```bash 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_/`). 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. ```bash # 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=` 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: ```bash 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.