14 Commits

Author SHA1 Message Date
Gitea Actions
cb0dfe2c21 docs: update changelog for v0.1.2
Some checks failed
Create Release PR / Create Release PR (push) Has been cancelled
Build and Publish Plugin / Build Plugin + Update Manifest (release) Successful in 44s
2026-04-04 09:39:03 +00:00
eaf6ea91e1 Merge pull request 'chore(main): release 0.1.2' (#25) from release-please--branches--main into main
Some checks failed
Create Release PR / Create Release PR (push) Has been cancelled
Reviewed-on: #25
2026-04-04 11:38:56 +02:00
Gitea Actions
180a998be1 chore(main): release 0.1.2
All checks were successful
Create Release / Publish Release (pull_request) Successful in 9s
2026-04-04 09:36:28 +00:00
6fd2638414 Merge branch 'main' of https://git.tdpi.dev/TDPI/jellyfin-plugin-smartnotify
All checks were successful
Create Release PR / Create Release PR (push) Successful in 19s
2026-04-04 11:36:02 +02:00
0e10e3c089 fix: refresh series name before notification 2026-04-04 11:35:59 +02:00
Gitea Actions
e729c7b8d5 chore: update manifest for v0.1.1
All checks were successful
Create Release PR / Create Release PR (push) Successful in 4s
2026-04-03 17:32:41 +00:00
Gitea Actions
fb8161f976 docs: update changelog for v0.1.1
Some checks failed
Create Release PR / Create Release PR (push) Has been cancelled
Build and Publish Plugin / Build Plugin + Update Manifest (release) Successful in 1m42s
2026-04-03 17:30:57 +00:00
0f5ce9726b Merge pull request 'chore(main): release 0.1.1' (#24) from release-please--branches--main into main
Some checks failed
Create Release PR / Create Release PR (push) Has been cancelled
Reviewed-on: #24
2026-04-03 19:30:45 +02:00
Gitea Actions
8d93c17bde chore(main): release 0.1.1
All checks were successful
Create Release / Publish Release (pull_request) Successful in 15s
2026-04-03 15:41:18 +00:00
b1444094ad fix: unknown series
All checks were successful
Create Release PR / Create Release PR (push) Successful in 59s
2026-04-03 17:40:17 +02:00
cc2d02983f Merge branch 'main' of https://git.tdpi.dev/TDPI/jellyfin-plugin-smartnotify
All checks were successful
Create Release PR / Create Release PR (push) Successful in 10s
2026-03-05 17:22:17 +01:00
aa26dbb40b chore: workflow fix 2026-03-05 17:22:16 +01:00
5954974add chore: removed old versions
All checks were successful
Create Release PR / Create Release PR (push) Successful in 11s
2026-03-05 17:20:36 +01:00
Gitea Actions
52b3588933 chore: update manifest for v0.1.0
All checks were successful
Create Release PR / Create Release PR (push) Has been skipped
2026-03-05 16:15:12 +00:00
8 changed files with 155 additions and 172 deletions

View File

@@ -11,24 +11,36 @@ permissions:
jobs:
release-pr:
name: Create Release PR
if: "!contains(gitea.event.head_commit.message, 'chore(main): release') && !contains(gitea.event.head_commit.message, 'chore: update manifest')"
runs-on: ubuntu-latest
steps:
- name: Skip if release/manifest commit
run: |
COMMIT_MSG="${{ gitea.event.head_commit.message }}"
if echo "$COMMIT_MSG" | grep -qE 'chore\(main\): release|chore: update manifest'; then
echo "Skipping release/manifest commit"
exit 0
fi
echo "SHOULD_RUN=true" >> $GITHUB_ENV
- uses: actions/checkout@v4
if: env.SHOULD_RUN == 'true'
with:
fetch-depth: 0
token: ${{ secrets.GT_TOKEN }}
- name: Setup Python
if: env.SHOULD_RUN == 'true'
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
if: env.SHOULD_RUN == 'true'
run: |
pip install gitpython packaging
- name: Create Release PR
if: env.SHOULD_RUN == 'true'
env:
GIT_TOKEN: ${{ secrets.GT_TOKEN }}
REPO: "TDPI/jellyfin-plugin-smartnotify"

View File

@@ -1,3 +1,3 @@
{
".": "0.1.0"
".": "0.1.2"
}

View File

@@ -1,5 +1,25 @@
# Changelog
## 0.1.2 (2026-04-04)
## 0.1.2 (2026-04-04)
### Bug Fixes
* Problem behoben das der Serienname vor der Benachrichtigung nicht aktualisiert wurde.
## 0.1.1 (2026-04-03)
## 0.1.1 (2026-04-03)
### Bug Fixes
* fix: unknown series
### Chores
* chore: workflow fix
* chore: removed old versions
* chore: update manifest for v0.1.0
## 0.1.0 (2026-03-05)
## 0.1.0 (2026-03-05)

View File

@@ -3,8 +3,8 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>Jellyfin.Plugin.SmartNotify</RootNamespace>
<AssemblyVersion>0.1.0.0</AssemblyVersion>
<FileVersion>0.1.0.0</FileVersion>
<AssemblyVersion>0.1.2.0</AssemblyVersion>
<FileVersion>0.1.2.0</FileVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>

View File

@@ -318,17 +318,23 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable
if (item is Episode episode)
{
notification.SeriesName = episode.SeriesName;
notification.SeriesId = episode.SeriesId.ToString();
// episode.SeriesName / SeriesId are often null with Shokofin VFS,
// but the Series navigation property usually works
var seriesObj = episode.Series;
notification.SeriesName = episode.SeriesName ?? seriesObj?.Name;
notification.SeriesId = (episode.SeriesId != Guid.Empty
? episode.SeriesId
: seriesObj?.Id ?? Guid.Empty).ToString();
notification.SeasonNumber = episode.ParentIndexNumber;
notification.EpisodeNumber = episode.IndexNumber;
notification.Year = episode.ProductionYear;
// Use series provider IDs for external links — episode provider IDs
// (e.g. AniDB episode ID) lead to wrong URLs when used with /anime/ paths
if (episode.SeriesId != Guid.Empty)
var resolvedSeriesId = notification.SeriesId;
if (resolvedSeriesId != Guid.Empty.ToString() && Guid.TryParse(resolvedSeriesId, out var seriesGuid))
{
var series = _libraryManager.GetItemById(episode.SeriesId);
var series = seriesObj ?? _libraryManager.GetItemById(seriesGuid);
if (series?.ProviderIds != null && series.ProviderIds.Count > 0)
{
notification.ProviderIdsJson = JsonSerializer.Serialize(series.ProviderIds);
@@ -424,17 +430,31 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable
var changed = false;
if (string.IsNullOrEmpty(notification.SeriesName) || notification.SeriesName == "Unknown Series")
// Always refresh SeriesName from the library — at queue time the name
// may have been the Sonarr folder name (before metadata refresh).
{
notification.SeriesName = episode.SeriesName;
changed = true;
var freshSeriesName = episode.SeriesName ?? episode.Series?.Name;
if (!string.IsNullOrEmpty(freshSeriesName) && freshSeriesName != notification.SeriesName)
{
_logger.LogInformation(
"[DEBUG Refresh] SeriesName changed: '{Old}' -> '{New}'",
notification.SeriesName,
freshSeriesName);
notification.SeriesName = freshSeriesName;
changed = true;
}
}
if (string.IsNullOrEmpty(notification.SeriesId) || notification.SeriesId == Guid.Empty.ToString())
{
if (episode.SeriesId != Guid.Empty)
// Same fallback: SeriesId property may be empty, but Series object may exist
var resolvedSeriesId = episode.SeriesId != Guid.Empty
? episode.SeriesId
: episode.Series?.Id ?? Guid.Empty;
if (resolvedSeriesId != Guid.Empty)
{
notification.SeriesId = episode.SeriesId.ToString();
notification.SeriesId = resolvedSeriesId.ToString();
changed = true;
}
}
@@ -451,6 +471,17 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable
changed = true;
}
// Always refresh the item name — it may have been a placeholder at queue time
if (!string.IsNullOrEmpty(episode.Name) && episode.Name != notification.Name)
{
_logger.LogInformation(
"[DEBUG Refresh] Name changed: '{Old}' -> '{New}'",
notification.Name,
episode.Name);
notification.Name = episode.Name;
changed = true;
}
// Refresh image if missing
if (string.IsNullOrEmpty(notification.ImagePath) && episode.SeriesId != Guid.Empty)
{
@@ -568,16 +599,43 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable
.GroupBy(n => n.SeriesId!)
.ToList();
// Handle episodes that still have no series info (send individually as fallback)
var orphanEpisodes = pendingNotifications
.Where(n => n.ItemType == "Episode" && (string.IsNullOrEmpty(n.SeriesId) || n.SeriesId == emptyGuid))
// Episodes without SeriesName or SeriesId must NEVER be sent.
// Wait up to 30 minutes for metadata to resolve, then drop.
var maxMetadataWait = DateTime.UtcNow.AddMinutes(-30);
var incompleteEpisodes = pendingNotifications
.Where(n => n.ItemType == "Episode"
&& (string.IsNullOrEmpty(n.SeriesName)
|| string.IsNullOrEmpty(n.SeriesId)
|| n.SeriesId == emptyGuid))
.ToList();
if (orphanEpisodes.Count > 0)
if (incompleteEpisodes.Count > 0)
{
_logger.LogWarning(
"{Count} episode notifications have no series info even after refresh",
orphanEpisodes.Count);
// Split into: still waiting vs. timed out
var timedOut = incompleteEpisodes.Where(n => n.QueuedAt < maxMetadataWait).ToList();
var stillWaiting = incompleteEpisodes.Where(n => n.QueuedAt >= maxMetadataWait).ToList();
if (timedOut.Count > 0)
{
var dropIds = timedOut.Select(n => n.Id).ToList();
_historyService.RemoveNotifications(dropIds);
pendingNotifications.RemoveAll(n => dropIds.Contains(n.Id));
_logger.LogWarning(
"Dropped {Count} episode notifications after 30min without series metadata: {Names}",
timedOut.Count,
string.Join(", ", timedOut.Select(n => n.Name)));
}
if (stillWaiting.Count > 0)
{
// Remove from this processing cycle, keep in queue for next attempt
var waitIds = stillWaiting.Select(n => n.Id).ToHashSet();
pendingNotifications.RemoveAll(n => waitIds.Contains(n.Id));
_logger.LogInformation(
"Deferring {Count} episode notifications, waiting for series metadata: {Names}",
stillWaiting.Count,
string.Join(", ", stillWaiting.Select(n => n.Name)));
}
}
// Process each series group
@@ -619,31 +677,6 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable
}
}
// Process orphan episodes (no series info - send individually)
foreach (var orphan in orphanEpisodes)
{
var oldAge = DateTime.UtcNow.AddMinutes(-groupingWindowMinutes);
if (orphan.QueuedAt > oldAge)
{
continue;
}
_logger.LogInformation("Sending individual episode notification for: {Name} (no series info)", orphan.Name);
var success = await _discordService.SendGroupedEpisodeNotificationAsync(
new[] { orphan },
CancellationToken.None);
if (success)
{
_historyService.RemoveNotifications(new[] { orphan.Id });
}
else
{
_logger.LogWarning("Discord send failed for orphan episode {Name}, keeping in queue for retry", orphan.Name);
}
}
// Process movies
var movies = pendingNotifications
.Where(n => n.ItemType == "Movie")

View File

@@ -57,7 +57,7 @@ public class DiscordNotificationService
}
var first = notificationList.First();
var seriesName = first.SeriesName ?? "Unknown Series";
var seriesName = first.SeriesName!;
// Group by season
var bySeason = notificationList

118
README.md
View File

@@ -1,115 +1,65 @@
# SmartNotify - Jellyfin Plugin
**Intelligente Discord-Benachrichtigungen für Jellyfin 10.11+**
**Discord-Benachrichtigungen für neue Filme und Episoden in Jellyfin 10.11+**
## Das Problem
Kennt das Problem: Du tauschst eine Serie gegen bessere Qualität aus und Jellyfin meldet jede Folge als "neu". SmartNotify erkennt den Unterschied zwischen wirklich neuen Inhalten und Qualitäts-Upgrades — und nervt dich nicht mit Spam.
Wenn du eine Serie gegen eine bessere Qualität austauschst, schreit Jellyfin "NEUE EPISODE!" - obwohl es nur ein Upgrade ist. Das nervt.
## Was kann es?
## Die Lösung
SmartNotify erkennt automatisch:
- **Wirklich neue Inhalte** → Benachrichtigung wird gesendet
- **Qualitäts-Upgrades** → Keine Benachrichtigung (oder optional eigener Typ)
Zusätzlich gruppiert SmartNotify Episoden intelligent:
- Statt 12 einzelner Nachrichten: **"Staffel 1: Episode 1-12 hinzugefügt"**
- Bei Lücken: **"Episode 1-4, 6, 8-12"**
## Features
- ✅ Erkennt Qualitäts-Upgrades automatisch
- ✅ Intelligente Episoden-Gruppierung
- ✅ Unterstützt AniDB, AniList, TMDB, TVDB, IMDB
- ✅ Konfigurierbare Verzögerung (für Metadaten-Updates)
- ✅ Konfigurierbare Gruppierungsfenster
- ✅ Discord Webhook Support
- ✅ Deutsche Oberfläche
## Wie funktioniert die Erkennung?
SmartNotify führt eine lokale Datenbank mit allen bekannten Inhalten:
1. **Neues Item kommt rein**
2. SmartNotify generiert einen "Content Key" basierend auf:
- Bei Episoden: Serie (Provider-IDs) + Staffel + Episode
- Bei Filmen: Provider-IDs (AniDB, TMDB, etc.)
3. **Prüfung 1:** Existiert dieser Content Key schon in der DB? → Upgrade!
4. **Prüfung 2:** Gibt es gerade ein anderes Item mit gleichem Content Key? → Duplikat (neues File vor Löschung des alten)
5. Nur wenn beide Prüfungen negativ sind → Benachrichtigung
- **Upgrade-Erkennung** — Erkennt ob eine Datei neu ist oder nur ein Qualitäts-Upgrade. Keine falschen Benachrichtigungen mehr beim Austausch von Dateien.
- **Episoden-Gruppierung** — Statt 12 einzelner Nachrichten bekommst du eine: *"Staffel 1: Episode 1-12"*. Bei Lücken entsprechend: *"Episode 1-4, 6, 8-12"*.
- **Discord-Embeds** — Benachrichtigungen mit Bild, Beschreibung und direkten Links zu IMDb, TMDb, AniDB, AniList und TVDB.
- **Shokofin/VFS-kompatibel** — Die Erkennung basiert auf Provider-IDs statt Jellyfin-internen IDs, die sich bei VFS ständig ändern.
## Installation
## Manuelle Installation
In Jellyfin unter **Dashboard** > **Plugins** > **Repositories** die folgende URL als Repository hinzufügen:
1. Plugin von Releases herunterladen
2. ZIP entpacken in `plugins/SmartNotify/`
3. Jellyfin neustarten
```
https://git.tdpi.dev/TDPI/jellyfin-plugin-smartnotify/raw/branch/main/manifest.json
```
## Konfiguration
Danach im **Katalog** SmartNotify installieren und Jellyfin neustarten.
Nach der Installation im Jellyfin Dashboard:
1. **Dashboard****Plugins****SmartNotify**
2. Discord Webhook URL eintragen
3. Server URL eintragen (für Bilder in Discord)
4. Optional: Verzögerung und Gruppierungsfenster anpassen
### Manuell
### Einstellungen
Plugin-ZIP von den [Releases](https://git.tdpi.dev/TDPI/jellyfin-plugin-smartnotify/releases) herunterladen, nach `plugins/SmartNotify/` entpacken und Jellyfin neustarten.
## Einrichtung
Nach der Installation unter **Dashboard** > **Plugins** > **SmartNotify**:
1. Discord Webhook URL eintragen
2. Server URL eintragen (wird für Bilder in Discord gebraucht)
3. Fertig — Standardeinstellungen passen für die meisten Setups
### Optionale Einstellungen
| Einstellung | Standard | Beschreibung |
|-------------|----------|--------------|
| Discord Webhook URL | - | Die Webhook-URL deines Discord-Kanals |
| Server URL | - | Öffentliche Jellyfin-URL (für Bilder) |
| Verzögerung | 5 min | Wartezeit für Metadaten-Updates |
| Gruppierungsfenster | 30 min | Zeitfenster für Episoden-Gruppierung |
| Upgrades unterdrücken | ✓ | Keine Benachrichtigung bei Ersetzungen |
| Verzögerung | 5 min | Wartezeit damit Jellyfin Metadaten laden kann |
| Gruppierungsfenster | 30 min | Wie lange auf weitere Episoden gewartet wird bevor gesendet wird |
| Upgrades unterdrücken | An | Keine Benachrichtigung wenn nur die Qualität besser wird |
| Film-Benachrichtigungen | An | Benachrichtigungen für neue Filme |
| Episoden-Benachrichtigungen | An | Benachrichtigungen für neue Episoden |
| Bot-Name | Jellyfin SmartNotify | Anzeigename in Discord |
| Embed-Farbe | Blau | Farbe des Discord-Embeds |
## Beispiel-Benachrichtigungen
## Beispiele
### Einzelne Episode
```
📺 Demon Slayer
Neue Episoden hinzugefügt:
Staffel 1: Episode 5
```
### Mehrere Episoden (gruppiert)
**Mehrere Episoden:**
```
📺 Attack on Titan
Neue Episoden hinzugefügt:
Staffel 4: Episode 1-12
```
### Episoden mit Lücken
```
📺 One Piece
Neue Episoden hinzugefügt:
Staffel 1: Episode 1-4, 6, 8-12
```
### Film
**Neuer Film:**
```
🎬 Your Name (2016)
Kimi no Na wa - Ein Junge und ein Mädchen...
```
## Bekannte Einschränkungen
- Provider-IDs müssen vorhanden sein (AniDB, TMDB, etc.)
- Bei Items ohne Provider-IDs wird auf Name+Jahr zurückgefallen
- Die Datenbank wächst mit der Zeit (kann bei Bedarf gelöscht werden)
## Installation via Repository
In Jellyfin:
1. **Dashboard****Plugins****Repositories****+**
2. URL einfügen:
```
https://git.tdpi.dev/TDPI/jellyfin-plugin-smartnotify/raw/branch/main/manifest.json
```
3. **Katalog** → SmartNotify installieren
4. Jellyfin neustarten
## Lizenz
MIT

View File

@@ -9,52 +9,20 @@
"imageUrl": "",
"versions": [
{
"version": "0.0.19.0",
"changelog": "### Bug Fixes\n\n* fix: debuging\n\n### Chores\n\n* chore: update manifest for v0.0.18",
"version": "0.1.1.0",
"changelog": "## 0.1.1 (2026-04-03)\n\n### Bug Fixes\n\n* fix: unknown series\n\n### Chores\n\n* chore: workflow fix\n* chore: removed old versions\n* chore: update manifest for v0.1.0",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.tdpi.dev/TDPI/jellyfin-plugin-smartnotify/releases/download/v0.0.19/smartnotify_0.0.19.zip",
"checksum": "a79dec5dc65282ff8bb5f130931f9480",
"timestamp": "2026-03-04T17:22:49Z"
"sourceUrl": "https://git.tdpi.dev/TDPI/jellyfin-plugin-smartnotify/releases/download/v0.1.1/smartnotify_0.1.1.zip",
"checksum": "459569cbae49ba569291011ac9a54202",
"timestamp": "2026-04-03T17:32:41Z"
},
{
"version": "0.0.18.0",
"changelog": "### Bug Fixes\n\n* fix: build error",
"version": "0.1.0.0",
"changelog": "## 0.1.0 (2026-03-05)\n\n**Features**\n\nIntelligente Discord-Benachrichtigungen\n\nAutomatische Benachrichtigungen bei neuen Filmen und Episoden via Discord Webhook\nSchöne Discord-Embeds mit Thumbnail, Beschreibung und Links zu externen Datenbanken (IMDb, TMDb, AniDB, AniList, TVDB)\n\n**Smarte Episoden-Gruppierung**\n\nEpisoden werden intelligent gebündelt statt einzeln gemeldet — z.B. \"Staffel 1: Episode 1-12\" statt 12 einzelne Nachrichten\nKonfigurierbares Zeitfenster für die Gruppierung\n\n**Qualitäts-Upgrade-Erkennung**\n\nErkennt automatisch ob eine Datei neu ist oder nur ein Qualitäts-Upgrade einer bestehenden Datei\nKeine Spam-Benachrichtigungen mehr beim Ersetzen von Dateien durch bessere Versionen\nStabile Erkennung über Provider-IDs (AniDB, TMDb etc.) — funktioniert auch mit Shokofin/VFS\n\n**Robuste Metadaten-Verarbeitung**\n\nVerzögerte Verarbeitung damit Jellyfin Zeit hat Metadaten zu laden\nDreistufige Validierung: beim Hinzufügen, beim Einreihen und beim Senden\nAutomatische Unterdrückung von reorganisierten Items (Pfad-/Metadata-Änderungen)\n\n**Konfigurierbar**\n\nBenachrichtigungen für Filme und Episoden einzeln aktivierbar\nUpgrade-Unterdrückung optional\nAnpassbare Verzögerung, Gruppierungsfenster, Bot-Name und Embed-Farbe",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.tdpi.dev/TDPI/jellyfin-plugin-smartnotify/releases/download/v0.0.18/smartnotify_0.0.18.zip",
"checksum": "4b5857ce309974f8e64ab81885d9b67d",
"timestamp": "2026-03-03T13:53:53Z"
},
{
"version": "0.0.16.0",
"changelog": "### Bug Fixes\n\n* fix: ist das wirklich ein Fix und kein defix?\n\n### Chores\n\n* chore: update manifest for v0.0.15",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.tdpi.dev/TDPI/jellyfin-plugin-smartnotify/releases/download/v0.0.16/smartnotify_0.0.16.zip",
"checksum": "fbe2b3ce339c92204961df605bfe276b",
"timestamp": "2026-03-02T18:58:41Z"
},
{
"version": "0.0.15.0",
"changelog": "### Bug Fixes\n\n* fix: timestamps!\n\n### Chores\n\n* chore: update manifest for v0.0.14",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.tdpi.dev/TDPI/jellyfin-plugin-smartnotify/releases/download/v0.0.15/smartnotify_0.0.15.zip",
"checksum": "c3dc638240b5688de030f77eccaf9c50",
"timestamp": "2026-03-01T17:49:47Z"
},
{
"version": "0.0.14.0",
"changelog": "### Bug Fixes\n\n* fix: claude hat bugs im Kopf\n\n### Chores\n\n* chore: update manifest for v0.0.13",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.tdpi.dev/TDPI/jellyfin-plugin-smartnotify/releases/download/v0.0.14/smartnotify_0.0.14.zip",
"checksum": "428e0cc3a00c873381dc2f2f198f3907",
"timestamp": "2026-03-01T17:36:26Z"
},
{
"version": "0.0.13.0",
"changelog": "### Bug Fixes\n\n* fix: removed drecks versionen\n* fix: Bilder und dumme min TImings!\n\n### Chores\n\n* chore: update manifest for v0.0.12",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.tdpi.dev/TDPI/jellyfin-plugin-smartnotify/releases/download/v0.0.13/smartnotify_0.0.13.zip",
"checksum": "2d303e8dc214a58e038f516076840d5b",
"timestamp": "2026-03-01T17:21:50Z"
"sourceUrl": "https://git.tdpi.dev/TDPI/jellyfin-plugin-smartnotify/releases/download/v0.1.0/smartnotify_0.1.0.zip",
"checksum": "2db7d09e4fd669a5cd25a6649397ea98",
"timestamp": "2026-03-05T16:15:11Z"
}
]
}