diff --git a/Jellyfin.Plugin.SmartNotify/Notifiers/SmartNotifyBackgroundService.cs b/Jellyfin.Plugin.SmartNotify/Notifiers/SmartNotifyBackgroundService.cs index d3c4c4d..6bb1077 100644 --- a/Jellyfin.Plugin.SmartNotify/Notifiers/SmartNotifyBackgroundService.cs +++ b/Jellyfin.Plugin.SmartNotify/Notifiers/SmartNotifyBackgroundService.cs @@ -254,7 +254,12 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable return; } - _logger.LogDebug("Processing {Count} pending notifications", pendingNotifications.Count); + _logger.LogInformation( + "Processing {Count} pending notifications (delay={Delay}min, grouping={Grouping}min, cutoff={Cutoff})", + pendingNotifications.Count, + delayMinutes, + groupingWindowMinutes, + cutoff); // Group episodes by series var episodesBySeries = pendingNotifications @@ -262,6 +267,17 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable .GroupBy(n => n.SeriesId!) .ToList(); + // Log unmatched notifications (neither episode with series nor movie) + var unmatchedCount = pendingNotifications.Count + - pendingNotifications.Count(n => n.ItemType == "Episode" && !string.IsNullOrEmpty(n.SeriesId)) + - pendingNotifications.Count(n => n.ItemType == "Movie"); + if (unmatchedCount > 0) + { + _logger.LogWarning( + "{Count} notifications are neither episodes (with SeriesId) nor movies and will be skipped", + unmatchedCount); + } + // Process each series group foreach (var seriesGroup in episodesBySeries) { @@ -271,19 +287,34 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable // Only process if the oldest notification is outside the grouping window if (oldestInGroup > groupingCutoff) { - _logger.LogDebug( - "Waiting for grouping window for series {SeriesId}, oldest: {Oldest}", + _logger.LogInformation( + "Waiting for grouping window for series {SeriesName} ({SeriesId}), oldest queued: {Oldest}, grouping cutoff: {Cutoff}", + seriesGroup.First().SeriesName, seriesGroup.Key, - oldestInGroup); + oldestInGroup, + groupingCutoff); continue; } - await _discordService.SendGroupedEpisodeNotificationAsync( + _logger.LogInformation( + "Sending grouped notification for {SeriesName}: {Count} episodes", + seriesGroup.First().SeriesName, + seriesGroup.Count()); + + var success = await _discordService.SendGroupedEpisodeNotificationAsync( seriesGroup, CancellationToken.None); - var idsToRemove = seriesGroup.Select(n => n.Id).ToList(); - _historyService.RemoveNotifications(idsToRemove); + if (success) + { + var idsToRemove = seriesGroup.Select(n => n.Id).ToList(); + _historyService.RemoveNotifications(idsToRemove); + _logger.LogInformation("Removed {Count} processed episode notifications from queue", idsToRemove.Count); + } + else + { + _logger.LogWarning("Discord send failed for {SeriesName}, keeping notifications in queue for retry", seriesGroup.First().SeriesName); + } } // Process movies @@ -293,8 +324,19 @@ public class SmartNotifyBackgroundService : IHostedService, IDisposable foreach (var movie in movies) { - await _discordService.SendMovieNotificationAsync(movie, CancellationToken.None); - _historyService.RemoveNotifications(new[] { movie.Id }); + _logger.LogInformation("Sending movie notification for: {Name}", movie.Name); + + var success = await _discordService.SendMovieNotificationAsync(movie, CancellationToken.None); + + if (success) + { + _historyService.RemoveNotifications(new[] { movie.Id }); + _logger.LogInformation("Removed processed movie notification from queue: {Name}", movie.Name); + } + else + { + _logger.LogWarning("Discord send failed for movie {Name}, keeping in queue for retry", movie.Name); + } } } catch (Exception ex) diff --git a/Jellyfin.Plugin.SmartNotify/Services/DiscordNotificationService.cs b/Jellyfin.Plugin.SmartNotify/Services/DiscordNotificationService.cs index a8be990..5eaf34d 100644 --- a/Jellyfin.Plugin.SmartNotify/Services/DiscordNotificationService.cs +++ b/Jellyfin.Plugin.SmartNotify/Services/DiscordNotificationService.cs @@ -38,8 +38,8 @@ public class DiscordNotificationService /// /// The notifications to group and send. /// The cancellation token. - /// A task representing the async operation. - public async Task SendGroupedEpisodeNotificationAsync( + /// True if the notification was sent successfully. + public async Task SendGroupedEpisodeNotificationAsync( IEnumerable notifications, CancellationToken cancellationToken) { @@ -47,13 +47,13 @@ public class DiscordNotificationService if (config == null || string.IsNullOrEmpty(config.DiscordWebhookUrl)) { _logger.LogWarning("Discord webhook URL not configured"); - return; + return false; } var notificationList = notifications.ToList(); if (notificationList.Count == 0) { - return; + return false; } var first = notificationList.First(); @@ -70,7 +70,7 @@ public class DiscordNotificationService var title = $"📺 {seriesName}"; var description = $"Neue Episoden hinzugefügt:\n{episodeDescription}"; - await SendDiscordWebhookAsync( + return await SendDiscordWebhookAsync( config, title, description, @@ -153,7 +153,8 @@ public class DiscordNotificationService /// /// The notification. /// The cancellation token. - public async Task SendMovieNotificationAsync( + /// True if the notification was sent successfully. + public async Task SendMovieNotificationAsync( PendingNotification notification, CancellationToken cancellationToken) { @@ -161,7 +162,7 @@ public class DiscordNotificationService if (config == null || string.IsNullOrEmpty(config.DiscordWebhookUrl)) { _logger.LogWarning("Discord webhook URL not configured"); - return; + return false; } var title = $"🎬 {notification.Name}"; @@ -176,7 +177,7 @@ public class DiscordNotificationService description = description.Substring(0, 297) + "..."; } - await SendDiscordWebhookAsync( + return await SendDiscordWebhookAsync( config, title, description, @@ -236,7 +237,8 @@ public class DiscordNotificationService /// /// Sends the actual Discord webhook request, with optional image attachment. /// - private async Task SendDiscordWebhookAsync( + /// True if the webhook was sent successfully. + private async Task SendDiscordWebhookAsync( PluginConfiguration config, string title, string description, @@ -260,7 +262,16 @@ public class DiscordNotificationService if (!string.IsNullOrEmpty(externalLinks)) { - embed["footer"] = new Dictionary { ["text"] = externalLinks }; + var fields = new List> + { + new Dictionary + { + ["name"] = "Links", + ["value"] = externalLinks, + ["inline"] = false + } + }; + embed["fields"] = fields; } embed["timestamp"] = DateTime.UtcNow.ToString("o"); @@ -272,7 +283,7 @@ public class DiscordNotificationService }; var json = JsonSerializer.Serialize(payload); - _logger.LogDebug("Sending Discord webhook: {Json}", json); + _logger.LogInformation("Sending Discord webhook for: {Title}", title); try { @@ -281,6 +292,7 @@ public class DiscordNotificationService if (hasImage) { + _logger.LogInformation("Attaching image from: {Path}", imagePath); // Send as multipart/form-data with image attachment using var form = new MultipartFormDataContent(); form.Add(new StringContent(json, Encoding.UTF8, "application/json"), "payload_json"); @@ -306,15 +318,16 @@ public class DiscordNotificationService "Discord webhook failed with status {Status}: {Body}", response.StatusCode, responseBody); + return false; } - else - { - _logger.LogInformation("Discord notification sent successfully: {Title}", title); - } + + _logger.LogInformation("Discord notification sent successfully: {Title}", title); + return true; } catch (Exception ex) { _logger.LogError(ex, "Failed to send Discord webhook"); + return false; } } }