using System; using System.Collections.Generic; using System.Linq; using FluentValidation.Results; using NzbDrone.Common.Extensions; using NzbDrone.Core.Localization; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Movies; using NzbDrone.Core.Notifications.Discord.Payloads; using NzbDrone.Core.Tags; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Discord { public class Discord : NotificationBase { private readonly IDiscordProxy _proxy; private readonly ITagRepository _tagRepository; private readonly ILocalizationService _localizationService; public Discord(IDiscordProxy proxy, ITagRepository tagRepository, ILocalizationService localizationService) { _proxy = proxy; _tagRepository = tagRepository; _localizationService = localizationService; } public override string Name => "Discord"; public override string Link => "https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks"; public override void OnGrab(GrabMessage message) { var embed = new Embed { Author = new DiscordAuthor { Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author, IconUrl = "https://raw.githubusercontent.com/Radarr/Radarr/develop/Logo/256.png" }, Url = $"https://www.themoviedb.org/movie/{message.Movie.MovieMetadata.Value.TmdbId}", Description = "Movie Grabbed", Title = GetTitle(message.Movie), 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 = message.Movie.MovieMetadata.Value.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Poster)?.RemoteUrl }; } if (Settings.GrabFields.Contains((int)DiscordGrabFieldType.Fanart)) { embed.Image = new DiscordImage { Url = message.Movie.MovieMetadata.Value.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Fanart)?.RemoteUrl }; } foreach (var field in Settings.GrabFields) { var discordField = new DiscordField(); switch ((DiscordGrabFieldType)field) { case DiscordGrabFieldType.Overview: var overview = message.Movie.MovieMetadata.Value.Overview ?? ""; discordField.Name = "Overview"; discordField.Value = overview.Length <= 300 ? overview : $"{overview.AsSpan(0, 300)}..."; break; case DiscordGrabFieldType.Rating: discordField.Name = "Rating"; discordField.Value = message.Movie.MovieMetadata.Value.Ratings.Tmdb?.Value.ToString() ?? string.Empty; break; case DiscordGrabFieldType.Genres: discordField.Name = "Genres"; discordField.Value = message.Movie.MovieMetadata.Value.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.RemoteMovie.ParsedMovieInfo.ReleaseGroup; break; case DiscordGrabFieldType.Size: discordField.Name = "Size"; discordField.Value = BytesToString(message.RemoteMovie.Release.Size); discordField.Inline = true; break; case DiscordGrabFieldType.Release: discordField.Name = "Release"; discordField.Value = string.Format("```{0}```", message.RemoteMovie.Release.Title); break; case DiscordGrabFieldType.Links: discordField.Name = "Links"; discordField.Value = GetLinksString(message.Movie); break; case DiscordGrabFieldType.CustomFormats: discordField.Name = "Custom Formats"; discordField.Value = string.Join("|", message.RemoteMovie.CustomFormats); break; case DiscordGrabFieldType.CustomFormatScore: discordField.Name = "Custom Format Score"; discordField.Value = message.RemoteMovie.CustomFormatScore.ToString(); break; case DiscordGrabFieldType.Indexer: discordField.Name = "Indexer"; discordField.Value = message.RemoteMovie.Release.Indexer; break; case DiscordGrabFieldType.Tags: discordField.Name = "Tags"; discordField.Value = GetTagLabels(message.Movie)?.Join(", ") ?? string.Empty; 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 isUpgrade = message.OldMovieFiles.Count > 0; var embed = new Embed { Author = new DiscordAuthor { Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author, IconUrl = "https://raw.githubusercontent.com/Radarr/Radarr/develop/Logo/256.png" }, Url = $"https://www.themoviedb.org/movie/{message.Movie.MovieMetadata.Value.TmdbId}", Description = isUpgrade ? "Movie Upgraded" : "Movie Imported", Title = GetTitle(message.Movie), Color = isUpgrade ? (int)DiscordColors.Upgrade : (int)DiscordColors.Success, 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 = message.Movie.MovieMetadata.Value.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Poster)?.RemoteUrl }; } if (Settings.ImportFields.Contains((int)DiscordImportFieldType.Fanart)) { embed.Image = new DiscordImage { Url = message.Movie.MovieMetadata.Value.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Fanart)?.RemoteUrl }; } foreach (var field in Settings.ImportFields) { var discordField = new DiscordField(); switch ((DiscordImportFieldType)field) { case DiscordImportFieldType.Overview: var overview = message.Movie.MovieMetadata.Value.Overview ?? ""; discordField.Name = "Overview"; discordField.Value = overview.Length <= 300 ? overview : $"{overview.AsSpan(0, 300)}..."; break; case DiscordImportFieldType.Rating: discordField.Name = "Rating"; discordField.Value = message.Movie.MovieMetadata.Value.Ratings.Tmdb?.Value.ToString() ?? string.Empty; break; case DiscordImportFieldType.Genres: discordField.Name = "Genres"; discordField.Value = message.Movie.MovieMetadata.Value.Genres.Take(5).Join(", "); break; case DiscordImportFieldType.Quality: discordField.Name = "Quality"; discordField.Inline = true; discordField.Value = message.MovieFile.Quality.Quality.Name; break; case DiscordImportFieldType.Codecs: discordField.Name = "Codecs"; discordField.Inline = true; discordField.Value = string.Format("{0} / {1} {2}", MediaInfoFormatter.FormatVideoCodec(message.MovieFile.MediaInfo, null), MediaInfoFormatter.FormatAudioCodec(message.MovieFile.MediaInfo, null), MediaInfoFormatter.FormatAudioChannels(message.MovieFile.MediaInfo)); break; case DiscordImportFieldType.Group: discordField.Name = "Group"; discordField.Value = message.MovieFile.ReleaseGroup; break; case DiscordImportFieldType.Size: discordField.Name = "Size"; discordField.Value = BytesToString(message.MovieFile.Size); discordField.Inline = true; break; case DiscordImportFieldType.Languages: discordField.Name = "Languages"; discordField.Value = message.MovieFile.MediaInfo.AudioLanguages.ConcatToString("/"); break; case DiscordImportFieldType.Subtitles: discordField.Name = "Subtitles"; discordField.Value = message.MovieFile.MediaInfo.Subtitles.ConcatToString("/"); break; case DiscordImportFieldType.Release: discordField.Name = "Release"; discordField.Value = string.Format("```{0}```", message.MovieFile.SceneName); break; case DiscordImportFieldType.Links: discordField.Name = "Links"; discordField.Value = GetLinksString(message.Movie); break; case DiscordImportFieldType.Tags: discordField.Name = "Tags"; discordField.Value = GetTagLabels(message.Movie)?.Join(", ") ?? string.Empty; 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 OnMovieAdded(Movie movie) { var embed = new Embed { Author = new DiscordAuthor { Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author, IconUrl = "https://raw.githubusercontent.com/Radarr/Radarr/develop/Logo/256.png" }, Url = $"https://www.themoviedb.org/movie/{movie.MovieMetadata.Value.TmdbId}", Title = movie.Title, Description = "Movie Added", Color = (int)DiscordColors.Success, Fields = new List { new () { Name = "Links", Value = GetLinksString(movie) } } }; if (Settings.ImportFields.Contains((int)DiscordImportFieldType.Poster)) { embed.Thumbnail = new DiscordImage { Url = movie.MovieMetadata.Value.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Poster)?.Url }; } if (Settings.ImportFields.Contains((int)DiscordImportFieldType.Fanart)) { embed.Image = new DiscordImage { Url = movie.MovieMetadata.Value.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Fanart)?.Url }; } var payload = CreatePayload(null, new List { embed }); _proxy.SendPayload(payload, Settings); } public override void OnMovieRename(Movie movie, List renamedFiles) { var attachments = new List(); foreach (var renamedFile in renamedFiles) { attachments.Add(new Embed { Title = movie.MovieMetadata.Value.Title, Description = renamedFile.PreviousRelativePath + " renamed to " + renamedFile.MovieFile.RelativePath, }); } var payload = CreatePayload("Renamed", attachments); _proxy.SendPayload(payload, Settings); } public override void OnMovieDelete(MovieDeleteMessage deleteMessage) { var movie = deleteMessage.Movie; var embed = new Embed { Author = new DiscordAuthor { Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author, IconUrl = "https://raw.githubusercontent.com/Radarr/Radarr/develop/Logo/256.png" }, Url = $"https://www.themoviedb.org/movie/{movie.MovieMetadata.Value.TmdbId}", Title = movie.Title, Description = deleteMessage.DeletedFilesMessage, Color = (int)DiscordColors.Danger, Fields = new List { new () { Name = "Links", Value = GetLinksString(movie) } } }; if (Settings.ImportFields.Contains((int)DiscordImportFieldType.Poster)) { embed.Thumbnail = new DiscordImage { Url = movie.MovieMetadata.Value.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Poster)?.Url }; } if (Settings.ImportFields.Contains((int)DiscordImportFieldType.Fanart)) { embed.Image = new DiscordImage { Url = movie.MovieMetadata.Value.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Fanart)?.Url }; } var payload = CreatePayload(null, new List { embed }); _proxy.SendPayload(payload, Settings); } public override void OnMovieFileDelete(MovieFileDeleteMessage deleteMessage) { var movie = deleteMessage.Movie; var deletedFile = deleteMessage.MovieFile.Path; var reason = deleteMessage.Reason; var embed = new Embed { Author = new DiscordAuthor { Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author, IconUrl = "https://raw.githubusercontent.com/Radarr/Radarr/develop/Logo/256.png" }, Url = $"https://www.themoviedb.org/movie/{movie.MovieMetadata.Value.TmdbId}", Title = GetTitle(movie), Description = "Movie File Deleted", Color = (int)DiscordColors.Danger, Fields = new List { new () { Name = "Reason", Value = reason.ToString() }, new () { Name = "File name", Value = string.Format("```{0}```", deletedFile) } }, Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), }; var payload = CreatePayload(null, new List { embed }); _proxy.SendPayload(payload, Settings); } public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck) { var embed = new Embed { Author = new DiscordAuthor { Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author, IconUrl = "https://raw.githubusercontent.com/Radarr/Radarr/develop/Logo/256.png" }, Title = healthCheck.Source.Name, 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(null, new List { embed }); _proxy.SendPayload(payload, Settings); } public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck) { var embed = new Embed { Author = new DiscordAuthor { Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author, IconUrl = "https://raw.githubusercontent.com/Radarr/Radarr/develop/Logo/256.png" }, Title = "Health Issue Resolved: " + previousCheck.Source.Name, Description = $"The following issue is now resolved: {previousCheck.Message}", Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), Color = (int)DiscordColors.Success }; var payload = CreatePayload(null, new List { embed }); _proxy.SendPayload(payload, Settings); } public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage) { var embed = new Embed { Author = new DiscordAuthor { Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author, IconUrl = "https://raw.githubusercontent.com/Radarr/Radarr/develop/Logo/256.png" }, Title = APPLICATION_UPDATE_TITLE, Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), Color = (int)DiscordColors.Standard, Fields = new List { new () { Name = "Previous Version", Value = updateMessage.PreviousVersion.ToString() }, new () { Name = "New Version", Value = updateMessage.NewVersion.ToString() } }, }; var payload = CreatePayload(null, new List { embed }); _proxy.SendPayload(payload, Settings); } public override void OnManualInteractionRequired(ManualInteractionRequiredMessage message) { var movie = message.Movie; var embed = new Embed { Author = new DiscordAuthor { Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author, IconUrl = "https://raw.githubusercontent.com/Radarr/Radarr/develop/Logo/256.png" }, Url = $"https://www.themoviedb.org/movie/{movie.MovieMetadata.Value.TmdbId}", Description = "Manual interaction needed", Title = GetTitle(movie), Color = (int)DiscordColors.Standard, Fields = new List(), Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ") }; if (Settings.ManualInteractionFields.Contains((int)DiscordManualInteractionFieldType.Poster)) { embed.Thumbnail = new DiscordImage { Url = movie.MovieMetadata.Value.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Poster)?.RemoteUrl }; } if (Settings.ManualInteractionFields.Contains((int)DiscordManualInteractionFieldType.Fanart)) { embed.Image = new DiscordImage { Url = movie.MovieMetadata.Value.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Fanart)?.RemoteUrl }; } foreach (var field in Settings.ManualInteractionFields) { var discordField = new DiscordField(); switch ((DiscordManualInteractionFieldType)field) { case DiscordManualInteractionFieldType.Overview: var overview = movie.MovieMetadata.Value.Overview ?? ""; discordField.Name = "Overview"; discordField.Value = overview.Length <= 300 ? overview : $"{overview.AsSpan(0, 300)}..."; break; case DiscordManualInteractionFieldType.Rating: discordField.Name = "Rating"; discordField.Value = movie.MovieMetadata.Value.Ratings.Tmdb?.Value.ToString() ?? string.Empty; break; case DiscordManualInteractionFieldType.Genres: discordField.Name = "Genres"; discordField.Value = movie.MovieMetadata.Value.Genres.Take(5).Join(", "); break; case DiscordManualInteractionFieldType.Quality: discordField.Name = "Quality"; discordField.Inline = true; discordField.Value = message.Quality.Quality.Name; break; case DiscordManualInteractionFieldType.Group: discordField.Name = "Group"; discordField.Value = message.RemoteMovie.ParsedMovieInfo.ReleaseGroup; break; case DiscordManualInteractionFieldType.Size: discordField.Name = "Size"; discordField.Value = BytesToString(message.TrackedDownload.DownloadItem.TotalSize); discordField.Inline = true; break; case DiscordManualInteractionFieldType.DownloadTitle: discordField.Name = "Download"; discordField.Value = string.Format("```{0}```", message.TrackedDownload.DownloadItem.Title); break; case DiscordManualInteractionFieldType.Links: discordField.Name = "Links"; discordField.Value = GetLinksString(message.Movie); break; case DiscordManualInteractionFieldType.Tags: discordField.Name = "Tags"; discordField.Value = GetTagLabels(message.Movie)?.Join(", ") ?? string.Empty; 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 ValidationResult Test() { var failures = new List(); failures.AddIfNotNull(TestMessage()); return new ValidationResult(failures); } public ValidationFailure TestMessage() { try { var message = $"Test message from Radarr posted at {DateTime.Now}"; var payload = CreatePayload(message); _proxy.SendPayload(payload, Settings); } catch (DiscordException ex) { return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("NotificationsValidationUnableToSendTestMessage", new Dictionary { { "exceptionMessage", ex.Message } })); } return null; } private DiscordPayload CreatePayload(string message, List embeds = null) { var avatar = Settings.Avatar; var payload = new DiscordPayload { Username = Settings.Username, Content = message, Embeds = embeds }; if (avatar.IsNotNullOrWhiteSpace()) { payload.AvatarUrl = avatar; } if (Settings.Username.IsNotNullOrWhiteSpace()) { payload.Username = Settings.Username; } return payload; } private static 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 static string GetLinksString(Movie movie) { var links = string.Format("[{0}]({1})", "TMDb", $"https://themoviedb.org/movie/{movie.MovieMetadata.Value.TmdbId}"); links += string.Format(" / [{0}]({1})", "Trakt", $"https://trakt.tv/search/tmdb/{movie.MovieMetadata.Value.TmdbId}?id_type=movie"); if (movie.MovieMetadata.Value.ImdbId.IsNotNullOrWhiteSpace()) { links += string.Format(" / [{0}]({1})", "IMDb", $"https://imdb.com/title/{movie.MovieMetadata.Value.ImdbId}/"); } if (movie.MovieMetadata.Value.YouTubeTrailerId.IsNotNullOrWhiteSpace()) { links += string.Format(" / [{0}]({1})", "YouTube", $"https://www.youtube.com/watch?v={movie.MovieMetadata.Value.YouTubeTrailerId}"); } if (movie.MovieMetadata.Value.Website.IsNotNullOrWhiteSpace()) { links += string.Format(" / [{0}]({1})", "Website", movie.MovieMetadata.Value.Website); } return links; } private string GetTitle(Movie movie) { var title = movie.MovieMetadata.Value.Year > 0 ? $"{movie.MovieMetadata.Value.Title} ({movie.MovieMetadata.Value.Year})" : movie.MovieMetadata.Value.Title; return title.Length > 256 ? $"{title.AsSpan(0, 253)}..." : title; } private IEnumerable GetTagLabels(Movie movie) { return movie.Tags?.Select(t => _tagRepository.Get(t)?.Label).OrderBy(t => t).Take(5); } } }