From 8bef9b4da790da54b43708716de8f481bfb1b61a Mon Sep 17 00:00:00 2001 From: Denis Gheorghescu Date: Sat, 19 Jul 2025 17:43:18 +0300 Subject: [PATCH] New:(Pushcut) Improved Notification Details (#10897) --- src/NzbDrone.Core/Localization/Core/en.json | 4 +++ .../Notifications/NotificationMetadataLink.cs | 16 +++++++++ .../NotificationMetadataLinkGenerator.cs | 35 +++++++++++++++++++ .../Notifications/Pushcut/Pushcut.cs | 29 ++++++++++----- .../Notifications/Pushcut/PushcutPayload.cs | 10 ++++++ .../Notifications/Pushcut/PushcutProxy.cs | 25 ++++++++++--- .../Notifications/Pushcut/PushcutSettings.cs | 9 ++++- 7 files changed, 114 insertions(+), 14 deletions(-) create mode 100644 src/NzbDrone.Core/Notifications/NotificationMetadataLink.cs create mode 100644 src/NzbDrone.Core/Notifications/NotificationMetadataLinkGenerator.cs diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 2830292d5e..47d10fa767 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1328,6 +1328,10 @@ "NotificationsPushBulletSettingsDeviceIds": "Device IDs", "NotificationsPushBulletSettingsDeviceIdsHelpText": "List of device IDs (leave blank to send to all devices)", "NotificationsPushcutSettingsApiKeyHelpText": "API Keys can be managed in the Account view of the Pushcut app", + "NotificationsPushcutSettingsIncludePoster": "Include Poster", + "NotificationsPushcutSettingsIncludePosterHelpText": "Include poster with notification", + "NotificationsPushcutSettingsMetadataLinks": "Metadata Links", + "NotificationsPushcutSettingsMetadataLinksHelpText": "Add a links to series metadata when sending notifications", "NotificationsPushcutSettingsNotificationName": "Notification Name", "NotificationsPushcutSettingsNotificationNameHelpText": "Notification name from Notifications tab of the Pushcut app", "NotificationsPushcutSettingsTimeSensitive": "Time Sensitive", diff --git a/src/NzbDrone.Core/Notifications/NotificationMetadataLink.cs b/src/NzbDrone.Core/Notifications/NotificationMetadataLink.cs new file mode 100644 index 0000000000..d7cfad39c4 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/NotificationMetadataLink.cs @@ -0,0 +1,16 @@ +namespace NzbDrone.Core.Notifications +{ + public class NotificationMetadataLink + { + public MetadataLinkType? Type { get; set; } + public string Label { get; set; } + public string Link { get; set; } + + public NotificationMetadataLink(MetadataLinkType? type, string label, string link) + { + Type = type; + Label = label; + Link = link; + } + } +} diff --git a/src/NzbDrone.Core/Notifications/NotificationMetadataLinkGenerator.cs b/src/NzbDrone.Core/Notifications/NotificationMetadataLinkGenerator.cs new file mode 100644 index 0000000000..08e433fd48 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/NotificationMetadataLinkGenerator.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Movies; + +namespace NzbDrone.Core.Notifications; + +public static class NotificationMetadataLinkGenerator +{ + public static List GenerateLinks(Movie movie, IEnumerable metadataLinks) + { + var links = new List(); + + if (movie == null) + { + return links; + } + + foreach (var type in metadataLinks) + { + var linkType = (MetadataLinkType)type; + + if (linkType == MetadataLinkType.Imdb && movie.ImdbId.IsNotNullOrWhiteSpace()) + { + links.Add(new NotificationMetadataLink(MetadataLinkType.Imdb, "IMDb", $"https://www.imdb.com/title/{movie.ImdbId}")); + } + + if (linkType == MetadataLinkType.Tmdb && movie.TmdbId > 0) + { + links.Add(new NotificationMetadataLink(MetadataLinkType.Tmdb, "TMDb", $"https://www.themoviedb.org/movie/{movie.TmdbId}")); + } + } + + return links; + } +} diff --git a/src/NzbDrone.Core/Notifications/Pushcut/Pushcut.cs b/src/NzbDrone.Core/Notifications/Pushcut/Pushcut.cs index e36fe7c46c..1451b4bec2 100644 --- a/src/NzbDrone.Core/Notifications/Pushcut/Pushcut.cs +++ b/src/NzbDrone.Core/Notifications/Pushcut/Pushcut.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using FluentValidation.Results; using NzbDrone.Common.Extensions; +using NzbDrone.Core.MediaCover; using NzbDrone.Core.Movies; namespace NzbDrone.Core.Notifications.Pushcut @@ -30,47 +31,57 @@ namespace NzbDrone.Core.Notifications.Pushcut public override void OnGrab(GrabMessage grabMessage) { - _proxy.SendNotification(MOVIE_GRABBED_TITLE, grabMessage?.Message, Settings); + _proxy.SendNotification(MOVIE_GRABBED_TITLE, grabMessage?.Message, GetPosterUrl(grabMessage.Movie), GetLinks(grabMessage.Movie), Settings); } public override void OnDownload(DownloadMessage downloadMessage) { - _proxy.SendNotification(downloadMessage.OldMovieFiles.Any() ? MOVIE_UPGRADED_TITLE : MOVIE_DOWNLOADED_TITLE, downloadMessage.Message, Settings); + _proxy.SendNotification(downloadMessage.OldMovieFiles.Any() ? MOVIE_UPGRADED_TITLE : MOVIE_DOWNLOADED_TITLE, downloadMessage.Message, GetPosterUrl(downloadMessage.Movie), GetLinks(downloadMessage.Movie), Settings); } public override void OnMovieAdded(Movie movie) { - _proxy.SendNotification(MOVIE_ADDED_TITLE, $"{movie.Title} added to library", Settings); + _proxy.SendNotification(MOVIE_ADDED_TITLE, $"{movie.Title} added to library", GetPosterUrl(movie), GetLinks(movie), Settings); } public override void OnMovieFileDelete(MovieFileDeleteMessage deleteMessage) { - _proxy.SendNotification(MOVIE_FILE_DELETED_TITLE, deleteMessage.Message, Settings); + _proxy.SendNotification(MOVIE_FILE_DELETED_TITLE, deleteMessage.Message, GetPosterUrl(deleteMessage.Movie), GetLinks(deleteMessage.Movie), Settings); } public override void OnMovieDelete(MovieDeleteMessage deleteMessage) { - _proxy.SendNotification(MOVIE_DELETED_TITLE, deleteMessage.Message, Settings); + _proxy.SendNotification(MOVIE_DELETED_TITLE, deleteMessage.Message, GetPosterUrl(deleteMessage.Movie), GetLinks(deleteMessage.Movie), Settings); } public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck) { - _proxy.SendNotification(HEALTH_ISSUE_TITLE_BRANDED, healthCheck.Message, Settings); + _proxy.SendNotification(HEALTH_ISSUE_TITLE_BRANDED, healthCheck.Message, null, new List(), Settings); } public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck) { - _proxy.SendNotification(HEALTH_RESTORED_TITLE_BRANDED, $"The following issue is now resolved: {previousCheck.Message}", Settings); + _proxy.SendNotification(HEALTH_RESTORED_TITLE_BRANDED, $"The following issue is now resolved: {previousCheck.Message}", null, new List(), Settings); } public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage) { - _proxy.SendNotification(APPLICATION_UPDATE_TITLE_BRANDED, updateMessage.Message, Settings); + _proxy.SendNotification(APPLICATION_UPDATE_TITLE_BRANDED, updateMessage.Message, null, new List(), Settings); } public override void OnManualInteractionRequired(ManualInteractionRequiredMessage manualInteractionRequiredMessage) { - _proxy.SendNotification(MANUAL_INTERACTION_REQUIRED_TITLE_BRANDED, manualInteractionRequiredMessage.Message, Settings); + _proxy.SendNotification(MANUAL_INTERACTION_REQUIRED_TITLE_BRANDED, manualInteractionRequiredMessage.Message, null, new List(), Settings); + } + + private string GetPosterUrl(Movie movie) + { + return movie.MovieMetadata.Value.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Poster)?.RemoteUrl; + } + + private List GetLinks(Movie movie) + { + return NotificationMetadataLinkGenerator.GenerateLinks(movie, Settings.MetadataLinks); } } } diff --git a/src/NzbDrone.Core/Notifications/Pushcut/PushcutPayload.cs b/src/NzbDrone.Core/Notifications/Pushcut/PushcutPayload.cs index 41a5d3cafa..b9da607266 100644 --- a/src/NzbDrone.Core/Notifications/Pushcut/PushcutPayload.cs +++ b/src/NzbDrone.Core/Notifications/Pushcut/PushcutPayload.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace NzbDrone.Core.Notifications.Pushcut { public class PushcutPayload @@ -5,5 +7,13 @@ namespace NzbDrone.Core.Notifications.Pushcut public string Title { get; set; } public string Text { get; set; } public bool? IsTimeSensitive { get; set; } + public string Image { get; set; } + public List Actions; + } + + public class PushcutAction + { + public string Name { get; set; } + public string Url { get; set; } } } diff --git a/src/NzbDrone.Core/Notifications/Pushcut/PushcutProxy.cs b/src/NzbDrone.Core/Notifications/Pushcut/PushcutProxy.cs index 9c555c34bd..034d4be3c8 100644 --- a/src/NzbDrone.Core/Notifications/Pushcut/PushcutProxy.cs +++ b/src/NzbDrone.Core/Notifications/Pushcut/PushcutProxy.cs @@ -12,7 +12,7 @@ namespace NzbDrone.Core.Notifications.Pushcut { public interface IPushcutProxy { - void SendNotification(string title, string message, PushcutSettings settings); + void SendNotification(string title, string message, string posterUrl, List links, PushcutSettings settings); ValidationFailure Test(PushcutSettings settings); } @@ -29,20 +29,37 @@ namespace NzbDrone.Core.Notifications.Pushcut _logger = logger; } - public void SendNotification(string title, string message, PushcutSettings settings) + public void SendNotification(string title, string message, string posterUrl, List links, PushcutSettings settings) { + if (settings == null) + { + return; + } + var request = new HttpRequestBuilder("https://api.pushcut.io/v1/notifications/{notificationName}") .SetSegment("notificationName", settings?.NotificationName) .SetHeader("API-Key", settings?.ApiKey) .Accept(HttpAccept.Json) .Build(); + var payload = new PushcutPayload { Title = title, Text = message, - IsTimeSensitive = settings?.TimeSensitive + IsTimeSensitive = settings?.TimeSensitive, + Image = settings.IncludePoster ? posterUrl : null, + Actions = new List() }; + foreach (var link in links) + { + payload.Actions.Add(new PushcutAction + { + Name = link.Label, + Url = link.Link + }); + } + request.Method = HttpMethod.Post; request.Headers.ContentType = "application/json"; request.SetContent(payload.ToJson()); @@ -64,7 +81,7 @@ namespace NzbDrone.Core.Notifications.Pushcut { const string title = "Radarr Test Title"; const string message = "Success! You have properly configured your Pushcut notification settings."; - SendNotification(title, message, settings); + SendNotification(title, message, null, new List(), settings); } catch (PushcutException pushcutException) when (pushcutException.InnerException is HttpException httpException) { diff --git a/src/NzbDrone.Core/Notifications/Pushcut/PushcutSettings.cs b/src/NzbDrone.Core/Notifications/Pushcut/PushcutSettings.cs index 5b94fcef4e..bc5a5ebb1a 100644 --- a/src/NzbDrone.Core/Notifications/Pushcut/PushcutSettings.cs +++ b/src/NzbDrone.Core/Notifications/Pushcut/PushcutSettings.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.Validation; @@ -15,7 +16,7 @@ namespace NzbDrone.Core.Notifications.Pushcut public class PushcutSettings : NotificationSettingsBase { - private static readonly PushcutSettingsValidator Validator = new (); + private static readonly PushcutSettingsValidator Validator = new PushcutSettingsValidator(); [FieldDefinition(0, Label = "NotificationsPushcutSettingsNotificationName", Type = FieldType.Textbox, HelpText = "NotificationsPushcutSettingsNotificationNameHelpText")] public string NotificationName { get; set; } @@ -26,6 +27,12 @@ namespace NzbDrone.Core.Notifications.Pushcut [FieldDefinition(2, Label = "NotificationsPushcutSettingsTimeSensitive", Type = FieldType.Checkbox, HelpText = "NotificationsPushcutSettingsTimeSensitiveHelpText")] public bool TimeSensitive { get; set; } + [FieldDefinition(3, Label = "NotificationsPushcutSettingsIncludePoster", Type = FieldType.Checkbox, HelpText = "NotificationsPushcutSettingsIncludePosterHelpText")] + public bool IncludePoster { get; set; } + + [FieldDefinition(4, Label = "NotificationsPushcutSettingsMetadataLinks", Type = FieldType.Select, SelectOptions = typeof(MetadataLinkType), HelpText = "NotificationsPushcutSettingsMetadataLinksHelpText")] + public IEnumerable MetadataLinks { get; set; } = new List(); + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this));