fix: double notifications
All checks were successful
Create Release PR / Create Release PR (push) Successful in 18s

This commit is contained in:
2026-03-05 16:40:03 +01:00
parent 562bfbec54
commit 9863778d8b
3 changed files with 70 additions and 17 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.claude
buildedplugin
MEMORY.md

View File

@@ -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<int>();
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)

View File

@@ -319,34 +319,74 @@ public class ItemHistoryService : IDisposable
}
/// <summary>
/// Re-checks if a pending notification is actually a quality upgrade.
/// Result of revalidating a pending notification at send time.
/// </summary>
public enum RevalidationResult
{
/// <summary>Item is genuinely new content.</summary>
New,
/// <summary>Item is a known item that was reorganized (path/metadata change, old ID gone).</summary>
Reorganized,
/// <summary>Item is a quality upgrade (same content, old file still exists).</summary>
Upgrade
}
/// <summary>
/// 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.
/// </summary>
/// <param name="jellyfinItemId">The Jellyfin item ID string.</param>
/// <param name="item">The resolved library item (may have updated metadata).</param>
/// <returns>True if this item is a late-detected upgrade that should be suppressed.</returns>
public bool RevalidatePendingItem(string jellyfinItemId, BaseItem? item)
/// <param name="libraryManager">The library manager to check if old items still exist.</param>
/// <returns>The revalidation result indicating if this is new, reorganized, or an upgrade.</returns>
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;
}
/// <summary>