fix: unknown series
All checks were successful
Create Release PR / Create Release PR (push) Successful in 59s
All checks were successful
Create Release PR / Create Release PR (push) Successful in 59s
This commit is contained in:
@@ -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);
|
||||
@@ -426,15 +432,22 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable
|
||||
|
||||
if (string.IsNullOrEmpty(notification.SeriesName) || notification.SeriesName == "Unknown Series")
|
||||
{
|
||||
notification.SeriesName = episode.SeriesName;
|
||||
// episode.SeriesName is often null (especially with Shokofin VFS),
|
||||
// but the Series navigation property usually has the correct name
|
||||
notification.SeriesName = episode.SeriesName ?? episode.Series?.Name;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -568,16 +581,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 +659,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")
|
||||
|
||||
@@ -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
118
README.md
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user