diff --git a/src/NzbDrone.Common/Extensions/UrlExtensions.cs b/src/NzbDrone.Common/Extensions/UrlExtensions.cs new file mode 100644 index 000000000..7808feb34 --- /dev/null +++ b/src/NzbDrone.Common/Extensions/UrlExtensions.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Common.Extensions +{ + public static class UrlExtensions + { + public static bool IsValidUrl(this string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return false; + } + + Uri uri; + if (!Uri.TryCreate(path, UriKind.Absolute, out uri)) + { + return false; + } + + if (!uri.IsWellFormedOriginalString()) + { + return false; + } + + return true; + + } + } +} diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index f2b0ac12b..265234252 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -138,6 +138,7 @@ + diff --git a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs index 2d6dc5249..4406fa73f 100644 --- a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs +++ b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs @@ -28,6 +28,7 @@ namespace NzbDrone.Core.Annotations Path, Hidden, Tag, - Action + Action, + Url } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs b/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs new file mode 100644 index 000000000..12f72dd91 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs @@ -0,0 +1,55 @@ + +using System.Collections.Generic; +using FluentValidation.Results; +using NzbDrone.Core.Tv; +using NzbDrone.Common.Extensions; + +namespace NzbDrone.Core.Notifications.Webhook +{ + public class Webhook : NotificationBase + { + private readonly IWebhookService _service; + + public Webhook(IWebhookService service) + { + _service = service; + } + + public override string Link + { + get { return "https://github.com/Sonarr/Sonarr/wiki/Webhook"; } + } + + public override void OnGrab(GrabMessage message) + { + _service.OnGrab(message.Series, message.Episode, message.Quality, Settings); + } + + public override void OnDownload(DownloadMessage message) + { + _service.OnDownload(message.Series, message.EpisodeFile, Settings); + } + + public override void OnRename(Series series) + { + _service.OnRename(series, Settings); + } + + public override string Name + { + get + { + return "Webhook"; + } + } + + public override ValidationResult Test() + { + var failures = new List(); + + failures.AddIfNotNull(_service.Test(Settings)); + + return new ValidationResult(failures); + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisode.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisode.cs new file mode 100644 index 000000000..a7979b726 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookEpisode.cs @@ -0,0 +1,32 @@ +using NzbDrone.Core.Tv; +using System; + +namespace NzbDrone.Core.Notifications.Webhook +{ + public class WebhookEpisode + { + public WebhookEpisode() { } + + public WebhookEpisode(Episode episode) + { + Id = episode.Id; + SeasonNumber = episode.SeasonNumber; + EpisodeNumber = episode.EpisodeNumber; + Title = episode.Title; + AirDate = episode.AirDate; + AirDateUtc = episode.AirDateUtc; + } + + public int Id { get; set; } + public int EpisodeNumber { get; set; } + public int SeasonNumber { get; set; } + public string Title { get; set; } + public string AirDate { get; set; } + public DateTime? AirDateUtc { get; set; } + + public string Quality { get; set; } + public int QualityVersion { get; set; } + public string ReleaseGroup { get; set; } + public string SceneName { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookException.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookException.cs new file mode 100644 index 000000000..b422d8621 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookException.cs @@ -0,0 +1,16 @@ +using System; +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Notifications.Webhook +{ + public class WebhookException : NzbDroneException + { + public WebhookException(string message) : base(message) + { + } + + public WebhookException(string message, Exception innerException, params object[] args) : base(message, innerException, args) + { + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookMethod.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookMethod.cs new file mode 100644 index 000000000..6ca862edb --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookMethod.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Notifications.Webhook +{ + public enum WebhookMethod + { + POST = RestSharp.Method.POST, + PUT = RestSharp.Method.PUT + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookPayload.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookPayload.cs new file mode 100644 index 000000000..41009a695 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookPayload.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.Notifications.Webhook +{ + public class WebhookPayload + { + public string EventType { get; set; } + public WebhookSeries Series { get; set; } + public List Episodes { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs new file mode 100644 index 000000000..222f9eebb --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs @@ -0,0 +1,22 @@ +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Notifications.Webhook +{ + public class WebhookSeries + { + public int Id { get; set; } + public string Title { get; set; } + public string Path { get; set; } + public int TvdbId { get; set; } + + public WebhookSeries() { } + + public WebhookSeries(Series series) + { + Id = series.Id; + Title = series.Title; + Path = series.Path; + TvdbId = series.TvdbId; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookService.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookService.cs new file mode 100644 index 000000000..982f502be --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookService.cs @@ -0,0 +1,120 @@ +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Processes; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Validation; +using NzbDrone.Core.Rest; +using RestSharp; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Parser.Model; +using System.Collections.Generic; + +namespace NzbDrone.Core.Notifications.Webhook +{ + public interface IWebhookService + { + void OnDownload(Series series, EpisodeFile episodeFile, WebhookSettings settings); + void OnRename(Series series, WebhookSettings settings); + void OnGrab(Series series, RemoteEpisode episode, QualityModel quality, WebhookSettings settings); + ValidationFailure Test(WebhookSettings settings); + } + + public class WebhookService : IWebhookService + { + public void OnDownload(Series series, EpisodeFile episodeFile, WebhookSettings settings) + { + var payload = new WebhookPayload + { + EventType = "Download", + Series = new WebhookSeries(series), + Episodes = episodeFile.Episodes.Value.ConvertAll(x => new WebhookEpisode(x) { + Quality = episodeFile.Quality.Quality.Name, + QualityVersion = episodeFile.Quality.Revision.Version, + ReleaseGroup = episodeFile.ReleaseGroup, + SceneName = episodeFile.SceneName + }) + }; + + NotifyWebhook(payload, settings); + } + + public void OnRename(Series series, WebhookSettings settings) + { + var payload = new WebhookPayload + { + EventType = "Rename", + Series = new WebhookSeries(series) + }; + + NotifyWebhook(payload, settings); + } + + public void OnGrab(Series series, RemoteEpisode episode, QualityModel quality, WebhookSettings settings) + { + var payload = new WebhookPayload + { + EventType = "Grab", + Series = new WebhookSeries(series), + Episodes = episode.Episodes.ConvertAll(x => new WebhookEpisode(x) + { + Quality = quality.Quality.Name, + QualityVersion = quality.Revision.Version, + ReleaseGroup = episode.ParsedEpisodeInfo.ReleaseGroup + }) + }; + NotifyWebhook(payload, settings); + } + + public void NotifyWebhook(WebhookPayload body, WebhookSettings settings) + { + try { + var client = RestClientFactory.BuildClient(settings.Url); + var request = new RestRequest((Method) settings.Method); + request.RequestFormat = DataFormat.Json; + request.AddBody(body); + client.ExecuteAndValidate(request); + } + catch (RestException ex) + { + throw new WebhookException("Unable to post to webhook: {0}", ex, ex.Message); + } + } + + public ValidationFailure Test(WebhookSettings settings) + { + try + { + NotifyWebhook( + new WebhookPayload + { + EventType = "Test", + Series = new WebhookSeries() + { + Id = 1, + Title = "Test Title", + Path = "C:\\testpath", + TvdbId = 1234 + }, + Episodes = new List() { + new WebhookEpisode() + { + Id = 123, + EpisodeNumber = 1, + SeasonNumber = 1, + Title = "Test title" + } + } + }, + settings + ); + } + catch (WebhookException ex) + { + return new NzbDroneValidationFailure("Url", ex.Message); + } + + return null; + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookSettings.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookSettings.cs new file mode 100644 index 000000000..f1584e16a --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookSettings.cs @@ -0,0 +1,38 @@ +using System; +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; +using NzbDrone.Core.Validation.Paths; + +namespace NzbDrone.Core.Notifications.Webhook +{ + public class WebhookSettingsValidator : AbstractValidator + { + public WebhookSettingsValidator() + { + RuleFor(c => c.Url).IsValidUrl(); + } + } + + public class WebhookSettings : IProviderConfig + { + private static readonly WebhookSettingsValidator Validator = new WebhookSettingsValidator(); + + public WebhookSettings() + { + Method = Convert.ToInt32(WebhookMethod.POST); + } + + [FieldDefinition(0, Label = "URL", Type = FieldType.Url)] + public string Url { get; set; } + + [FieldDefinition(1, Label = "Method", Type = FieldType.Select, SelectOptions = typeof(WebhookMethod), HelpText = "Which HTTP method to use submit to the Webservice")] + public Int32 Method { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 19949d436..d0b5703d0 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -746,6 +746,14 @@ + + + + + + + + @@ -992,6 +1000,7 @@ + diff --git a/src/NzbDrone.Core/Validation/UrlValidator.cs b/src/NzbDrone.Core/Validation/UrlValidator.cs new file mode 100644 index 000000000..a52c14a90 --- /dev/null +++ b/src/NzbDrone.Core/Validation/UrlValidator.cs @@ -0,0 +1,28 @@ +using FluentValidation; +using FluentValidation.Validators; +using NzbDrone.Common.Extensions; + +namespace NzbDrone.Core.Validation +{ + public static class UrlValidation + { + public static IRuleBuilderOptions IsValidUrl(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.SetValidator(new UrlValidator()); + } + } + + public class UrlValidator : PropertyValidator + { + public UrlValidator() + : base("Invalid Url") + { + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return false; + return context.PropertyValue.ToString().IsValidUrl(); + } + } +} \ No newline at end of file diff --git a/src/UI/Form/FormBuilder.js b/src/UI/Form/FormBuilder.js index d68608dea..53700b614 100644 --- a/src/UI/Form/FormBuilder.js +++ b/src/UI/Form/FormBuilder.js @@ -17,6 +17,10 @@ var _fieldBuilder = function(field) { return _templateRenderer.call(field, 'Form/HiddenTemplate'); } + if (field.type === 'url') { + return _templateRenderer.call(field, 'Form/UrlTemplate'); + } + if (field.type === 'password') { return _templateRenderer.call(field, 'Form/PasswordTemplate'); } @@ -56,4 +60,4 @@ Handlebars.registerHelper('formBuilder', function() { }); return new Handlebars.SafeString(ret); -}); \ No newline at end of file +}); diff --git a/src/UI/Form/UrlTemplate.hbs b/src/UI/Form/UrlTemplate.hbs new file mode 100644 index 000000000..7f41272f1 --- /dev/null +++ b/src/UI/Form/UrlTemplate.hbs @@ -0,0 +1,8 @@ +
+ + +
+ +
+ {{> FormHelpPartial}} +