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)
|
if (item is Episode episode)
|
||||||
{
|
{
|
||||||
notification.SeriesName = episode.SeriesName;
|
// episode.SeriesName / SeriesId are often null with Shokofin VFS,
|
||||||
notification.SeriesId = episode.SeriesId.ToString();
|
// 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.SeasonNumber = episode.ParentIndexNumber;
|
||||||
notification.EpisodeNumber = episode.IndexNumber;
|
notification.EpisodeNumber = episode.IndexNumber;
|
||||||
notification.Year = episode.ProductionYear;
|
notification.Year = episode.ProductionYear;
|
||||||
|
|
||||||
// Use series provider IDs for external links — episode provider IDs
|
// Use series provider IDs for external links — episode provider IDs
|
||||||
// (e.g. AniDB episode ID) lead to wrong URLs when used with /anime/ paths
|
// (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)
|
if (series?.ProviderIds != null && series.ProviderIds.Count > 0)
|
||||||
{
|
{
|
||||||
notification.ProviderIdsJson = JsonSerializer.Serialize(series.ProviderIds);
|
notification.ProviderIdsJson = JsonSerializer.Serialize(series.ProviderIds);
|
||||||
@@ -426,15 +432,22 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable
|
|||||||
|
|
||||||
if (string.IsNullOrEmpty(notification.SeriesName) || notification.SeriesName == "Unknown Series")
|
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;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(notification.SeriesId) || notification.SeriesId == Guid.Empty.ToString())
|
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;
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -568,16 +581,43 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable
|
|||||||
.GroupBy(n => n.SeriesId!)
|
.GroupBy(n => n.SeriesId!)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// Handle episodes that still have no series info (send individually as fallback)
|
// Episodes without SeriesName or SeriesId must NEVER be sent.
|
||||||
var orphanEpisodes = pendingNotifications
|
// Wait up to 30 minutes for metadata to resolve, then drop.
|
||||||
.Where(n => n.ItemType == "Episode" && (string.IsNullOrEmpty(n.SeriesId) || n.SeriesId == emptyGuid))
|
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();
|
.ToList();
|
||||||
|
|
||||||
if (orphanEpisodes.Count > 0)
|
if (incompleteEpisodes.Count > 0)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(
|
// Split into: still waiting vs. timed out
|
||||||
"{Count} episode notifications have no series info even after refresh",
|
var timedOut = incompleteEpisodes.Where(n => n.QueuedAt < maxMetadataWait).ToList();
|
||||||
orphanEpisodes.Count);
|
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
|
// 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
|
// Process movies
|
||||||
var movies = pendingNotifications
|
var movies = pendingNotifications
|
||||||
.Where(n => n.ItemType == "Movie")
|
.Where(n => n.ItemType == "Movie")
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public class DiscordNotificationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var first = notificationList.First();
|
var first = notificationList.First();
|
||||||
var seriesName = first.SeriesName ?? "Unknown Series";
|
var seriesName = first.SeriesName!;
|
||||||
|
|
||||||
// Group by season
|
// Group by season
|
||||||
var bySeason = notificationList
|
var bySeason = notificationList
|
||||||
|
|||||||
118
README.md
118
README.md
@@ -1,115 +1,65 @@
|
|||||||
# SmartNotify - Jellyfin Plugin
|
# 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
|
- **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"*.
|
||||||
SmartNotify erkennt automatisch:
|
- **Discord-Embeds** — Benachrichtigungen mit Bild, Beschreibung und direkten Links zu IMDb, TMDb, AniDB, AniList und TVDB.
|
||||||
- **Wirklich neue Inhalte** → Benachrichtigung wird gesendet
|
- **Shokofin/VFS-kompatibel** — Die Erkennung basiert auf Provider-IDs statt Jellyfin-internen IDs, die sich bei VFS ständig ändern.
|
||||||
- **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
|
|
||||||
|
|
||||||
## Installation
|
## 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/`
|
https://git.tdpi.dev/TDPI/jellyfin-plugin-smartnotify/raw/branch/main/manifest.json
|
||||||
3. Jellyfin neustarten
|
```
|
||||||
|
|
||||||
## Konfiguration
|
Danach im **Katalog** SmartNotify installieren und Jellyfin neustarten.
|
||||||
|
|
||||||
Nach der Installation im Jellyfin Dashboard:
|
### Manuell
|
||||||
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
|
|
||||||
|
|
||||||
### 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 |
|
| Einstellung | Standard | Beschreibung |
|
||||||
|-------------|----------|--------------|
|
|-------------|----------|--------------|
|
||||||
| Discord Webhook URL | - | Die Webhook-URL deines Discord-Kanals |
|
| Verzögerung | 5 min | Wartezeit damit Jellyfin Metadaten laden kann |
|
||||||
| Server URL | - | Öffentliche Jellyfin-URL (für Bilder) |
|
| Gruppierungsfenster | 30 min | Wie lange auf weitere Episoden gewartet wird bevor gesendet wird |
|
||||||
| Verzögerung | 5 min | Wartezeit für Metadaten-Updates |
|
| Upgrades unterdrücken | An | Keine Benachrichtigung wenn nur die Qualität besser wird |
|
||||||
| Gruppierungsfenster | 30 min | Zeitfenster für Episoden-Gruppierung |
|
| Film-Benachrichtigungen | An | Benachrichtigungen für neue Filme |
|
||||||
| Upgrades unterdrücken | ✓ | Keine Benachrichtigung bei Ersetzungen |
|
| 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
|
**Mehrere Episoden:**
|
||||||
```
|
|
||||||
📺 Demon Slayer
|
|
||||||
Neue Episoden hinzugefügt:
|
|
||||||
Staffel 1: Episode 5
|
|
||||||
```
|
|
||||||
|
|
||||||
### Mehrere Episoden (gruppiert)
|
|
||||||
```
|
```
|
||||||
📺 Attack on Titan
|
📺 Attack on Titan
|
||||||
Neue Episoden hinzugefügt:
|
Neue Episoden hinzugefügt:
|
||||||
Staffel 4: Episode 1-12
|
Staffel 4: Episode 1-12
|
||||||
```
|
```
|
||||||
|
|
||||||
### Episoden mit Lücken
|
**Neuer Film:**
|
||||||
```
|
|
||||||
📺 One Piece
|
|
||||||
Neue Episoden hinzugefügt:
|
|
||||||
Staffel 1: Episode 1-4, 6, 8-12
|
|
||||||
```
|
|
||||||
|
|
||||||
### Film
|
|
||||||
```
|
```
|
||||||
🎬 Your Name (2016)
|
🎬 Your Name (2016)
|
||||||
Kimi no Na wa - Ein Junge und ein Mädchen...
|
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
|
## Lizenz
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
Reference in New Issue
Block a user