From 9863778d8bcc0faa25c6bc20866430736d401a10 Mon Sep 17 00:00:00 2001 From: TDPI Date: Thu, 5 Mar 2026 16:40:03 +0100 Subject: [PATCH] fix: double notifications --- .gitignore | 3 +- .../Notifiers/SmartNotifyBackgroundService.cs | 26 ++++++--- .../Services/ItemHistoryService.cs | 58 ++++++++++++++++--- 3 files changed, 70 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 230d71e..4a64c4f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .claude -buildedplugin \ No newline at end of file +buildedplugin +MEMORY.md \ No newline at end of file diff --git a/Jellyfin.Plugin.SmartNotify/Notifiers/SmartNotifyBackgroundService.cs b/Jellyfin.Plugin.SmartNotify/Notifiers/SmartNotifyBackgroundService.cs index a80cfc5..2bc5512 100644 --- a/Jellyfin.Plugin.SmartNotify/Notifiers/SmartNotifyBackgroundService.cs +++ b/Jellyfin.Plugin.SmartNotify/Notifiers/SmartNotifyBackgroundService.cs @@ -512,11 +512,11 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable RefreshNotificationMetadata(notification); } - // Late upgrade detection: re-check now that metadata is fully populated. + // Late known-item detection: re-check now that metadata is fully populated. // At queue time, metadata (ProviderIds, Series, Season/Episode) may not have - // been available, causing GenerateContentKey() to return null and upgrades + // been available, causing GenerateContentKey() to return null and known items // to go undetected. By now (after delay + grouping window), metadata is ready. - if (config.SuppressUpgrades) + // This catches reorganized items (path/metadata changes) and quality upgrades. { var suppressedIds = new List(); foreach (var notification in pendingNotifications) @@ -524,11 +524,22 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable if (Guid.TryParse(notification.JellyfinItemId, out var revalidateId)) { var revalidateItem = _libraryManager.GetItemById(revalidateId); - if (_historyService.RevalidatePendingItem(notification.JellyfinItemId, revalidateItem)) + var result = _historyService.RevalidatePendingItem(notification.JellyfinItemId, revalidateItem, _libraryManager); + + if (result == ItemHistoryService.RevalidationResult.Reorganized) { + // Always suppress reorganized items (same content, path/ID changed) suppressedIds.Add(notification.Id); _logger.LogInformation( - "Late suppression: {Name} detected as upgrade at send time", + "Suppressed {Name}: recognized as reorganized known item at send time", + notification.Name); + } + else if (result == ItemHistoryService.RevalidationResult.Upgrade && config.SuppressUpgrades) + { + // Only suppress upgrades when configured to do so + suppressedIds.Add(notification.Id); + _logger.LogInformation( + "Suppressed {Name}: quality upgrade detected at send time", notification.Name); } } @@ -539,8 +550,9 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable _historyService.RemoveNotifications(suppressedIds); pendingNotifications.RemoveAll(n => suppressedIds.Contains(n.Id)); _logger.LogInformation( - "Suppressed {Count} upgrade notifications at send time", - suppressedIds.Count); + "Suppressed {Count} notifications at send time ({Reason})", + suppressedIds.Count, + "reorganized/upgrade"); } if (pendingNotifications.Count == 0) diff --git a/Jellyfin.Plugin.SmartNotify/Services/ItemHistoryService.cs b/Jellyfin.Plugin.SmartNotify/Services/ItemHistoryService.cs index d7b9873..147d76a 100644 --- a/Jellyfin.Plugin.SmartNotify/Services/ItemHistoryService.cs +++ b/Jellyfin.Plugin.SmartNotify/Services/ItemHistoryService.cs @@ -319,34 +319,74 @@ public class ItemHistoryService : IDisposable } /// - /// Re-checks if a pending notification is actually a quality upgrade. + /// Result of revalidating a pending notification at send time. + /// + public enum RevalidationResult + { + /// Item is genuinely new content. + New, + + /// Item is a known item that was reorganized (path/metadata change, old ID gone). + Reorganized, + + /// Item is a quality upgrade (same content, old file still exists). + Upgrade + } + + /// + /// Re-checks if a pending notification is actually a known item (reorganized or upgraded). /// Called at send time when metadata is fully populated. + /// At queue time, items often have empty ProviderIds and no season/episode numbers, + /// so the content key couldn't be generated. Now that metadata is available, we can + /// properly identify known items. /// /// The Jellyfin item ID string. /// The resolved library item (may have updated metadata). - /// True if this item is a late-detected upgrade that should be suppressed. - public bool RevalidatePendingItem(string jellyfinItemId, BaseItem? item) + /// The library manager to check if old items still exist. + /// The revalidation result indicating if this is new, reorganized, or an upgrade. + public RevalidationResult RevalidatePendingItem(string jellyfinItemId, BaseItem? item, MediaBrowser.Controller.Library.ILibraryManager libraryManager) { if (item == null) { - return false; + return RevalidationResult.New; } var contentKey = GenerateContentKey(item); if (contentKey == null) { - return false; + return RevalidationResult.New; + } + + // Update unresolved content key on the item's own record + var ownRecord = _knownItems.FindOne(x => x.JellyfinItemId == jellyfinItemId); + if (ownRecord != null && ownRecord.ContentKey.StartsWith("unresolved|", StringComparison.Ordinal)) + { + ownRecord.ContentKey = contentKey; + _knownItems.Update(ownRecord); + _logger.LogDebug("Resolved content key for {Name}: {Key}", item.Name, contentKey); } // Check if this content was already known under a different item ID var existing = _knownItems.FindOne(x => x.ContentKey == contentKey && x.JellyfinItemId != jellyfinItemId); if (existing != null) { + // Determine if the old item still exists in the library + var oldItemExists = false; + if (Guid.TryParse(existing.JellyfinItemId, out var oldId)) + { + oldItemExists = libraryManager.GetItemById(oldId) != null; + } + + var resultType = oldItemExists + ? RevalidationResult.Upgrade + : RevalidationResult.Reorganized; + _logger.LogInformation( - "Late upgrade detection for {Name}: content key {Key} already known (first seen: {FirstSeen})", + "Late detection for {Name}: content key {Key} already known (first seen: {FirstSeen}, result: {Result})", item.Name, contentKey, - existing.FirstSeen); + existing.FirstSeen, + resultType); // Update the existing record to point to the new item existing.JellyfinItemId = jellyfinItemId; @@ -360,12 +400,12 @@ public class ItemHistoryService : IDisposable _knownItems.Delete(duplicate.Id); } - return true; + return resultType; } // Ensure the item is properly recorded (might have been missed at queue time due to missing metadata) RecordItem(item); - return false; + return RevalidationResult.New; } ///