New: Customizable Discord Notifications

Closes #3985
This commit is contained in:
Qstick 2020-09-20 00:04:57 -04:00 committed by Mark McDowall
parent 09347f79c5
commit 470c9101ae
8 changed files with 332 additions and 27 deletions

View File

@ -1,7 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using FluentValidation.Results; using FluentValidation.Results;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Core.Notifications.Discord.Payloads; using NzbDrone.Core.Notifications.Discord.Payloads;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@ -22,34 +25,197 @@ namespace NzbDrone.Core.Notifications.Discord
public override void OnGrab(GrabMessage message) public override void OnGrab(GrabMessage message)
{ {
var embeds = new List<Embed> var series = message.Series;
var episodes = message.Episode.Episodes;
var embed = new Embed
{ {
new Embed Author = new DiscordAuthor
{ {
Description = message.Message, Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author,
Title = message.Series.Title, IconUrl = "https://raw.githubusercontent.com/Sonarr/Sonarr/phantom-develop/Logo/256.png"
Text = message.Message, },
Color = (int)DiscordColors.Warning Url = $"http://thetvdb.com/?tab=series&id={series.TvdbId}",
} Description = "Episode Grabbed",
Title = GetTitle(series, episodes),
Color = (int)DiscordColors.Standard,
Fields = new List<DiscordField>(),
Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
}; };
var payload = CreatePayload($"Grabbed: {message.Message}", embeds);
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> { embed });
_proxy.SendPayload(payload, Settings); _proxy.SendPayload(payload, Settings);
} }
public override void OnDownload(DownloadMessage message) public override void OnDownload(DownloadMessage message)
{ {
var embeds = new List<Embed> var series = message.Series;
var episodes = message.EpisodeFile.Episodes.Value;
var isUpgrade = message.OldFiles.Count > 0;
var embed = new Embed
{ {
new Embed Author = new DiscordAuthor
{ {
Description = message.Message, Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author,
Title = message.Series.Title, IconUrl = "https://raw.githubusercontent.com/Sonarr/Sonarr/phantom-develop/Logo/256.png"
Text = message.Message, },
Color = (int)DiscordColors.Success 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<DiscordField>(),
Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
}; };
var payload = CreatePayload($"Imported: {message.Message}", embeds);
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> { embed });
_proxy.SendPayload(payload, Settings); _proxy.SendPayload(payload, Settings);
} }
@ -75,13 +241,19 @@ namespace NzbDrone.Core.Notifications.Discord
{ {
new 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"
},
Title = healthCheck.Source.Name, Title = healthCheck.Source.Name,
Text = healthCheck.Message, Description = healthCheck.Message,
Color = healthCheck.Type == HealthCheck.HealthCheckResult.Warning ? (int)DiscordColors.Warning : (int)DiscordColors.Success 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); _proxy.SendPayload(payload, Settings);
} }
@ -136,5 +308,51 @@ namespace NzbDrone.Core.Notifications.Discord
return payload; 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<string>();
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<Episode> 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}";
}
} }
} }

View File

@ -1,9 +1,11 @@
namespace NzbDrone.Core.Notifications.Discord namespace NzbDrone.Core.Notifications.Discord
{ {
public enum DiscordColors public enum DiscordColors
{ {
Danger = 15749200, Danger = 15749200,
Success = 2605644, Success = 2605644,
Warning = 16753920 Warning = 16753920,
Standard = 16761392,
Upgrade = 7105644
} }
} }

View File

@ -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
}
}

View File

@ -1,3 +1,4 @@
using System.Collections.Generic;
using FluentValidation; using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
@ -15,6 +16,13 @@ namespace NzbDrone.Core.Notifications.Discord
public class DiscordSettings : IProviderConfig public class DiscordSettings : IProviderConfig
{ {
public DiscordSettings()
{
//Set Default Fields
GrabFields = new List<int> { 0, 1, 2, 3, 5, 6, 7, 8, 9 };
ImportFields = new List<int> { 0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12 };
}
private static readonly DiscordSettingsValidator Validator = new DiscordSettingsValidator(); private static readonly DiscordSettingsValidator Validator = new DiscordSettingsValidator();
[FieldDefinition(0, Label = "Webhook URL", HelpText = "Discord channel webhook url")] [FieldDefinition(0, Label = "Webhook URL", HelpText = "Discord channel webhook url")]
@ -26,6 +34,14 @@ namespace NzbDrone.Core.Notifications.Discord
[FieldDefinition(2, Label = "Avatar", HelpText = "Change the avatar that is used for messages from this integration", Type = FieldType.Textbox)] [FieldDefinition(2, Label = "Avatar", HelpText = "Change the avatar that is used for messages from this integration", Type = FieldType.Textbox)]
public string Avatar { get; set; } 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<int> 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<int> ImportFields { get; set; }
public NzbDroneValidationResult Validate() public NzbDroneValidationResult Validate()
{ {

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -0,0 +1,7 @@
namespace NzbDrone.Core.Notifications.Discord.Payloads
{
public class DiscordImage
{
public string Url { get; set; }
}
}

View File

@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Notifications.Discord.Payloads namespace NzbDrone.Core.Notifications.Discord.Payloads
{ {
public class Embed public class Embed
@ -6,5 +8,11 @@ namespace NzbDrone.Core.Notifications.Discord.Payloads
public string Title { get; set; } public string Title { get; set; }
public string Text { get; set; } public string Text { get; set; }
public int Color { 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<DiscordField> Fields { get; set; }
} }
} }