fix: double notifications
All checks were successful
Create Release PR / Create Release PR (push) Successful in 18s
All checks were successful
Create Release PR / Create Release PR (push) Successful in 18s
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
.claude
|
.claude
|
||||||
buildedplugin
|
buildedplugin
|
||||||
|
MEMORY.md
|
||||||
@@ -512,11 +512,11 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable
|
|||||||
RefreshNotificationMetadata(notification);
|
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
|
// 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.
|
// 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>();
|
var suppressedIds = new List<int>();
|
||||||
foreach (var notification in pendingNotifications)
|
foreach (var notification in pendingNotifications)
|
||||||
@@ -524,11 +524,22 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable
|
|||||||
if (Guid.TryParse(notification.JellyfinItemId, out var revalidateId))
|
if (Guid.TryParse(notification.JellyfinItemId, out var revalidateId))
|
||||||
{
|
{
|
||||||
var revalidateItem = _libraryManager.GetItemById(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);
|
suppressedIds.Add(notification.Id);
|
||||||
_logger.LogInformation(
|
_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);
|
notification.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -539,8 +550,9 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable
|
|||||||
_historyService.RemoveNotifications(suppressedIds);
|
_historyService.RemoveNotifications(suppressedIds);
|
||||||
pendingNotifications.RemoveAll(n => suppressedIds.Contains(n.Id));
|
pendingNotifications.RemoveAll(n => suppressedIds.Contains(n.Id));
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Suppressed {Count} upgrade notifications at send time",
|
"Suppressed {Count} notifications at send time ({Reason})",
|
||||||
suppressedIds.Count);
|
suppressedIds.Count,
|
||||||
|
"reorganized/upgrade");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pendingNotifications.Count == 0)
|
if (pendingNotifications.Count == 0)
|
||||||
|
|||||||
@@ -319,34 +319,74 @@ public class ItemHistoryService : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// 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>
|
/// </summary>
|
||||||
/// <param name="jellyfinItemId">The Jellyfin item ID string.</param>
|
/// <param name="jellyfinItemId">The Jellyfin item ID string.</param>
|
||||||
/// <param name="item">The resolved library item (may have updated metadata).</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>
|
/// <param name="libraryManager">The library manager to check if old items still exist.</param>
|
||||||
public bool RevalidatePendingItem(string jellyfinItemId, BaseItem? item)
|
/// <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)
|
if (item == null)
|
||||||
{
|
{
|
||||||
return false;
|
return RevalidationResult.New;
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentKey = GenerateContentKey(item);
|
var contentKey = GenerateContentKey(item);
|
||||||
if (contentKey == null)
|
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
|
// Check if this content was already known under a different item ID
|
||||||
var existing = _knownItems.FindOne(x => x.ContentKey == contentKey && x.JellyfinItemId != jellyfinItemId);
|
var existing = _knownItems.FindOne(x => x.ContentKey == contentKey && x.JellyfinItemId != jellyfinItemId);
|
||||||
if (existing != null)
|
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(
|
_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,
|
item.Name,
|
||||||
contentKey,
|
contentKey,
|
||||||
existing.FirstSeen);
|
existing.FirstSeen,
|
||||||
|
resultType);
|
||||||
|
|
||||||
// Update the existing record to point to the new item
|
// Update the existing record to point to the new item
|
||||||
existing.JellyfinItemId = jellyfinItemId;
|
existing.JellyfinItemId = jellyfinItemId;
|
||||||
@@ -360,12 +400,12 @@ public class ItemHistoryService : IDisposable
|
|||||||
_knownItems.Delete(duplicate.Id);
|
_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)
|
// Ensure the item is properly recorded (might have been missed at queue time due to missing metadata)
|
||||||
RecordItem(item);
|
RecordItem(item);
|
||||||
return false;
|
return RevalidationResult.New;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user