From 470c9101ae825f8ad492e02f62e70d51d0e5b11a Mon Sep 17 00:00:00 2001 From: Qstick Date: Sun, 20 Sep 2020 00:04:57 -0400 Subject: [PATCH] New: Customizable Discord Notifications Closes #3985 --- .../Notifications/Discord/Discord.cs | 268 ++++++++++++++++-- .../Notifications/Discord/DiscordColors.cs | 6 +- .../Notifications/Discord/DiscordFieldType.cs | 33 +++ .../Notifications/Discord/DiscordSettings.cs | 16 ++ .../Discord/Payloads/DiscordAuthor.cs | 12 + .../Discord/Payloads/DiscordField.cs | 9 + .../Discord/Payloads/DiscordImage.cs | 7 + .../Notifications/Discord/Payloads/Embed.cs | 8 + 8 files changed, 332 insertions(+), 27 deletions(-) create mode 100644 src/NzbDrone.Core/Notifications/Discord/DiscordFieldType.cs create mode 100644 src/NzbDrone.Core/Notifications/Discord/Payloads/DiscordAuthor.cs create mode 100644 src/NzbDrone.Core/Notifications/Discord/Payloads/DiscordField.cs create mode 100644 src/NzbDrone.Core/Notifications/Discord/Payloads/DiscordImage.cs diff --git a/src/NzbDrone.Core/Notifications/Discord/Discord.cs b/src/NzbDrone.Core/Notifications/Discord/Discord.cs index 25e6f25b1..c94c6a683 100644 --- a/src/NzbDrone.Core/Notifications/Discord/Discord.cs +++ b/src/NzbDrone.Core/Notifications/Discord/Discord.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; +using System.Linq; using FluentValidation.Results; using NzbDrone.Common.Extensions; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Notifications.Discord.Payloads; using NzbDrone.Core.Tv; using NzbDrone.Core.Validation; @@ -22,34 +25,197 @@ public Discord(IDiscordProxy proxy) public override void OnGrab(GrabMessage message) { - var embeds = new List - { - new Embed - { - Description = message.Message, - Title = message.Series.Title, - Text = message.Message, - Color = (int)DiscordColors.Warning - } - }; - var payload = CreatePayload($"Grabbed: {message.Message}", embeds); + var series = message.Series; + var episodes = message.Episode.Episodes; + + var embed = new Embed + { + Author = new DiscordAuthor + { + Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author, + IconUrl = "https://raw.githubusercontent.com/Sonarr/Sonarr/phantom-develop/Logo/256.png" + }, + Url = $"http://thetvdb.com/?tab=series&id={series.TvdbId}", + Description = "Episode Grabbed", + Title = GetTitle(series, episodes), + Color = (int)DiscordColors.Standard, + Fields = new List(), + Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ") + }; + + if (Settings.GrabFields.Contains((int)DiscordGrabFieldType.Poster)) + { + embed.Thumbnail = new DiscordImage + { + Url = series.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Poster)?.Url + }; + } + + if (Settings.GrabFields.Contains((int)DiscordGrabFieldType.Fanart)) + { + embed.Image = new DiscordImage + { + Url = series.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Fanart)?.Url + }; + } + + foreach (var field in Settings.GrabFields) + { + var discordField = new DiscordField(); + + switch ((DiscordGrabFieldType)field) + { + case DiscordGrabFieldType.Overview: + var overview = episodes.First().Overview; + discordField.Name = "Overview"; + discordField.Value = overview.Length <= 300 ? overview : overview.Substring(0, 300) + "..."; + break; + case DiscordGrabFieldType.Rating: + discordField.Name = "Rating"; + discordField.Value = episodes.First().Ratings.Value.ToString(); + break; + case DiscordGrabFieldType.Genres: + discordField.Name = "Genres"; + discordField.Value = series.Genres.Take(5).Join(", "); + break; + case DiscordGrabFieldType.Quality: + discordField.Name = "Quality"; + discordField.Inline = true; + discordField.Value = message.Quality.Quality.Name; + break; + case DiscordGrabFieldType.Group: + discordField.Name = "Group"; + discordField.Value = message.Episode.ParsedEpisodeInfo.ReleaseGroup; + break; + case DiscordGrabFieldType.Size: + discordField.Name = "Size"; + discordField.Value = BytesToString(message.Episode.Release.Size); + discordField.Inline = true; + break; + case DiscordGrabFieldType.Release: + discordField.Name = "Release"; + discordField.Value = string.Format("```{0}```", message.Episode.Release.Title); + break; + case DiscordGrabFieldType.Links: + discordField.Name = "Links"; + discordField.Value = GetLinksString(series); + break; + } + + if (discordField.Name.IsNotNullOrWhiteSpace() && discordField.Value.IsNotNullOrWhiteSpace()) + { + embed.Fields.Add(discordField); + } + } + + var payload = CreatePayload(null, new List { embed }); _proxy.SendPayload(payload, Settings); } public override void OnDownload(DownloadMessage message) { - var embeds = new List - { - new Embed - { - Description = message.Message, - Title = message.Series.Title, - Text = message.Message, - Color = (int)DiscordColors.Success - } - }; - var payload = CreatePayload($"Imported: {message.Message}", embeds); + var series = message.Series; + var episodes = message.EpisodeFile.Episodes.Value; + var isUpgrade = message.OldFiles.Count > 0; + + var embed = new Embed + { + Author = new DiscordAuthor + { + Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author, + IconUrl = "https://raw.githubusercontent.com/Sonarr/Sonarr/phantom-develop/Logo/256.png" + }, + Url = $"http://thetvdb.com/?tab=series&id={series.TvdbId}", + Description = isUpgrade ? "Episode Upgraded" : "Episode Imported", + Title = GetTitle(series, episodes), + Color = isUpgrade ? (int)DiscordColors.Upgrade : (int)DiscordColors.Standard, + Fields = new List(), + Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ") + }; + + if (Settings.ImportFields.Contains((int)DiscordImportFieldType.Poster)) + { + embed.Thumbnail = new DiscordImage + { + Url = series.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Poster).Url + }; + } + + if (Settings.ImportFields.Contains((int)DiscordImportFieldType.Fanart)) + { + embed.Image = new DiscordImage + { + Url = series.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Fanart).Url + }; + } + + foreach (var field in Settings.ImportFields) + { + var discordField = new DiscordField(); + + switch ((DiscordImportFieldType)field) + { + case DiscordImportFieldType.Overview: + var overview = episodes.First().Overview; + discordField.Name = "Overview"; + discordField.Value = overview.Length <= 300 ? overview : overview.Substring(0, 300) + "..."; + break; + case DiscordImportFieldType.Rating: + discordField.Name = "Rating"; + discordField.Value = episodes.First().Ratings.Value.ToString(); + break; + case DiscordImportFieldType.Genres: + discordField.Name = "Genres"; + discordField.Value = series.Genres.Take(5).Join(", "); + break; + case DiscordImportFieldType.Quality: + discordField.Name = "Quality"; + discordField.Inline = true; + discordField.Value = message.EpisodeFile.Quality.Quality.Name; + break; + case DiscordImportFieldType.Codecs: + discordField.Name = "Codecs"; + discordField.Inline = true; + discordField.Value = string.Format("{0} / {1} {2}", + MediaInfoFormatter.FormatVideoCodec(message.EpisodeFile.MediaInfo, null), + MediaInfoFormatter.FormatAudioCodec(message.EpisodeFile.MediaInfo, null), + MediaInfoFormatter.FormatAudioChannels(message.EpisodeFile.MediaInfo)); + break; + case DiscordImportFieldType.Group: + discordField.Name = "Group"; + discordField.Value = message.EpisodeFile.ReleaseGroup; + break; + case DiscordImportFieldType.Size: + discordField.Name = "Size"; + discordField.Value = BytesToString(message.EpisodeFile.Size); + discordField.Inline = true; + break; + case DiscordImportFieldType.Languages: + discordField.Name = "Languages"; + discordField.Value = message.EpisodeFile.MediaInfo.AudioLanguages; + break; + case DiscordImportFieldType.Subtitles: + discordField.Name = "Subtitles"; + discordField.Value = message.EpisodeFile.MediaInfo.Subtitles; + break; + case DiscordImportFieldType.Release: + discordField.Name = "Release"; + discordField.Value = message.EpisodeFile.SceneName; + break; + case DiscordImportFieldType.Links: + discordField.Name = "Links"; + discordField.Value = GetLinksString(series); + break; + } + + if (discordField.Name.IsNotNullOrWhiteSpace() && discordField.Value.IsNotNullOrWhiteSpace()) + { + embed.Fields.Add(discordField); + } + } + + var payload = CreatePayload(null, new List { embed }); _proxy.SendPayload(payload, Settings); } @@ -75,13 +241,19 @@ public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck) { new Embed { + Author = new DiscordAuthor + { + Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author, + IconUrl = "https://raw.githubusercontent.com/Sonarr/Sonarr/phantom-develop/Logo/256.png" + }, Title = healthCheck.Source.Name, - Text = healthCheck.Message, - Color = healthCheck.Type == HealthCheck.HealthCheckResult.Warning ? (int)DiscordColors.Warning : (int)DiscordColors.Success + Description = healthCheck.Message, + Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + Color = healthCheck.Type == HealthCheck.HealthCheckResult.Warning ? (int)DiscordColors.Warning : (int)DiscordColors.Danger } }; - var payload = CreatePayload("Health Issue", attachments); + var payload = CreatePayload(null, attachments); _proxy.SendPayload(payload, Settings); } @@ -136,5 +308,51 @@ private DiscordPayload CreatePayload(string message, List embeds = null) return payload; } + + private string BytesToString(long byteCount) + { + string[] suf = { "B", "KB", "MB", "GB", "TB", "PB", "EB" }; //Longs run out around EB + if (byteCount == 0) + { + return "0 " + suf[0]; + } + + var bytes = Math.Abs(byteCount); + var place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024))); + var num = Math.Round(bytes / Math.Pow(1024, place), 1); + return string.Format("{0} {1}", (Math.Sign(byteCount) * num).ToString(), suf[place]); + } + + private string GetLinksString(Series series) + { + var links = new List(); + + links.Add($"[The TVDB](https://thetvdb.com/?tab=series&id={series.TvdbId})"); + links.Add($"[Trakt](https://trakt.tv/search/tvdb/{series.TvdbId}?id_type=show)"); + + if (series.ImdbId.IsNotNullOrWhiteSpace()) + { + links.Add($"[IMDB](https://imdb.com/title/{series.ImdbId}/)"); + } + + return string.Join(" / ", links); + } + + private string GetTitle(Series series, List episodes) + { + if (series.SeriesType == SeriesTypes.Daily) + { + var episode = episodes.First(); + + return $"{series.Title} - {episode.AirDate} - {episode.Title}"; + } + + var episodeNumbers = string.Concat(episodes.Select(e => e.EpisodeNumber) + .Select(i => string.Format("x{0:00}", i))); + + var episodeTitles = string.Join(" + ", episodes.Select(e => e.Title)); + + return $"{series.Title} - {episodes.First().SeasonNumber}{episodeNumbers} - {episodeTitles}"; + } } } diff --git a/src/NzbDrone.Core/Notifications/Discord/DiscordColors.cs b/src/NzbDrone.Core/Notifications/Discord/DiscordColors.cs index 16590aade..71507d53a 100644 --- a/src/NzbDrone.Core/Notifications/Discord/DiscordColors.cs +++ b/src/NzbDrone.Core/Notifications/Discord/DiscordColors.cs @@ -1,9 +1,11 @@ -namespace NzbDrone.Core.Notifications.Discord +namespace NzbDrone.Core.Notifications.Discord { public enum DiscordColors { Danger = 15749200, Success = 2605644, - Warning = 16753920 + Warning = 16753920, + Standard = 16761392, + Upgrade = 7105644 } } diff --git a/src/NzbDrone.Core/Notifications/Discord/DiscordFieldType.cs b/src/NzbDrone.Core/Notifications/Discord/DiscordFieldType.cs new file mode 100644 index 000000000..188a8e820 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Discord/DiscordFieldType.cs @@ -0,0 +1,33 @@ +namespace NzbDrone.Core.Notifications.Discord +{ + public enum DiscordGrabFieldType + { + Overview, + Rating, + Genres, + Quality, + Group, + Size, + Links, + Release, + Poster, + Fanart + } + + public enum DiscordImportFieldType + { + Overview, + Rating, + Genres, + Quality, + Codecs, + Group, + Size, + Languages, + Subtitles, + Links, + Release, + Poster, + Fanart + } +} diff --git a/src/NzbDrone.Core/Notifications/Discord/DiscordSettings.cs b/src/NzbDrone.Core/Notifications/Discord/DiscordSettings.cs index fac3d4567..c1df42ec2 100644 --- a/src/NzbDrone.Core/Notifications/Discord/DiscordSettings.cs +++ b/src/NzbDrone.Core/Notifications/Discord/DiscordSettings.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; @@ -15,6 +16,13 @@ public DiscordSettingsValidator() public class DiscordSettings : IProviderConfig { + public DiscordSettings() + { + //Set Default Fields + GrabFields = new List { 0, 1, 2, 3, 5, 6, 7, 8, 9 }; + ImportFields = new List { 0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12 }; + } + private static readonly DiscordSettingsValidator Validator = new DiscordSettingsValidator(); [FieldDefinition(0, Label = "Webhook URL", HelpText = "Discord channel webhook url")] @@ -26,6 +34,14 @@ public class DiscordSettings : IProviderConfig [FieldDefinition(2, Label = "Avatar", HelpText = "Change the avatar that is used for messages from this integration", Type = FieldType.Textbox)] public string Avatar { get; set; } + [FieldDefinition(3, Label = "Host", Advanced = true, HelpText = "Override the Host that shows for this notification, Blank is machine name", Type = FieldType.Textbox)] + public string Author { get; set; } + + [FieldDefinition(4, Label = "On Grab Fields", Advanced = true, SelectOptions = typeof(DiscordGrabFieldType), HelpText = "Change the fields that are passed in for this 'on grab' notification", Type = FieldType.TagSelect)] + public IEnumerable GrabFields { get; set; } + + [FieldDefinition(5, Label = "On Import Fields", Advanced = true, SelectOptions = typeof(DiscordImportFieldType), HelpText = "Change the fields that are passed for this 'on import' notification", Type = FieldType.TagSelect)] + public IEnumerable ImportFields { get; set; } public NzbDroneValidationResult Validate() { diff --git a/src/NzbDrone.Core/Notifications/Discord/Payloads/DiscordAuthor.cs b/src/NzbDrone.Core/Notifications/Discord/Payloads/DiscordAuthor.cs new file mode 100644 index 000000000..df0a68005 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Discord/Payloads/DiscordAuthor.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace NzbDrone.Core.Notifications.Discord.Payloads +{ + public class DiscordAuthor + { + public string Name { get; set; } + + [JsonProperty("icon_url")] + public string IconUrl { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Discord/Payloads/DiscordField.cs b/src/NzbDrone.Core/Notifications/Discord/Payloads/DiscordField.cs new file mode 100644 index 000000000..6e41ad422 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Discord/Payloads/DiscordField.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Notifications.Discord.Payloads +{ + public class DiscordField + { + public string Name { get; set; } + public string Value { get; set; } + public bool Inline { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Discord/Payloads/DiscordImage.cs b/src/NzbDrone.Core/Notifications/Discord/Payloads/DiscordImage.cs new file mode 100644 index 000000000..bd64dd6f6 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Discord/Payloads/DiscordImage.cs @@ -0,0 +1,7 @@ +namespace NzbDrone.Core.Notifications.Discord.Payloads +{ + public class DiscordImage + { + public string Url { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Discord/Payloads/Embed.cs b/src/NzbDrone.Core/Notifications/Discord/Payloads/Embed.cs index 50e27914b..1c1d1bdb4 100644 --- a/src/NzbDrone.Core/Notifications/Discord/Payloads/Embed.cs +++ b/src/NzbDrone.Core/Notifications/Discord/Payloads/Embed.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace NzbDrone.Core.Notifications.Discord.Payloads { public class Embed @@ -6,5 +8,11 @@ public class Embed public string Title { get; set; } public string Text { get; set; } public int Color { get; set; } + public string Url { get; set; } + public DiscordAuthor Author { get; set; } + public DiscordImage Thumbnail { get; set; } + public DiscordImage Image { get; set; } + public string Timestamp { get; set; } + public List Fields { get; set; } } }