New: Add Webhook support to sonarr

Add Form type url (type=url input field)
Add isValidUrl input type validation

Only allow absolute urls when checking if a url is valid

String => string as per comments that sonarr is standarizing on the lowercase primative

Remove this before function calls

Refactored everything so OnGrab is supported

Don't double submit the webhook

Wrappers around Series, EpisodeFile, Episode so the entire data structure isn't exposed

Add Braces as per style guide

Series.ID and Series.TvdbId should be integers

Reorder webhook payload as per style guide

Upgrade to use ongrab as json instead of string

Add method selection to webhook settings

include episode directly in download event

QualityVersion should be an int and not a string (don't convert it int=>string)

Remove the list of episodes

Add season number to episode data structure

Code Review Fixes:

* Remove episodefile from payload, move everything to episode
* Change episode to a list

convert to var as per code review / style guide

Down with internals

Everything now uses webhookpayload. None of that payload.Message stuff

{"EventType":"Test","Series":{"Id":1,"Title":"Test Title","Path":"C:\\testpath","TvdbId":1234},"Episodes":[{"Id":123,"EpisodeNumber":1,"SeasonNumber":1,"Title":"Test title","AirDate":null,"AirDateUtc":null,"Quality":null,"QualityVersion":0,"ReleaseGroup":null,"SceneName":null}]}

Remove logger and processProvider

Remove unused constructor
pull/4/head
Gavin Mogan 9 years ago committed by Keivan Beigi
parent 187064101c
commit c5b25bcfee

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

@ -138,6 +138,7 @@
<Compile Include="Extensions\Int64Extensions.cs" />
<Compile Include="Extensions\ObjectExtensions.cs" />
<Compile Include="Extensions\StreamExtensions.cs" />
<Compile Include="Extensions\UrlExtensions.cs" />
<Compile Include="Extensions\XmlExtentions.cs" />
<Compile Include="HashUtil.cs" />
<Compile Include="Http\CurlHttpClient.cs" />

@ -28,6 +28,7 @@ namespace NzbDrone.Core.Annotations
Path,
Hidden,
Tag,
Action
Action,
Url
}
}

@ -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<WebhookSettings>
{
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<ValidationFailure>();
failures.AddIfNotNull(_service.Test(Settings));
return new ValidationResult(failures);
}
}
}

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

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

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

@ -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<WebhookEpisode> Episodes { get; set; }
}
}

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

@ -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<WebhookEpisode>() {
new WebhookEpisode()
{
Id = 123,
EpisodeNumber = 1,
SeasonNumber = 1,
Title = "Test title"
}
}
},
settings
);
}
catch (WebhookException ex)
{
return new NzbDroneValidationFailure("Url", ex.Message);
}
return null;
}
}
}

@ -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<WebhookSettings>
{
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));
}
}
}

@ -746,6 +746,14 @@
<Compile Include="Notifications\Synology\SynologyIndexerProxy.cs" />
<Compile Include="Notifications\Synology\SynologyIndexerSettings.cs" />
<Compile Include="Notifications\Twitter\TwitterException.cs" />
<Compile Include="Notifications\Webhook\WebhookEpisode.cs" />
<Compile Include="Notifications\Webhook\WebhookException.cs" />
<Compile Include="Notifications\Webhook\WebhookMethod.cs" />
<Compile Include="Notifications\Webhook\WebhookPayload.cs" />
<Compile Include="Notifications\Webhook\WebhookSeries.cs" />
<Compile Include="Notifications\Webhook\WebhookService.cs" />
<Compile Include="Notifications\Webhook\WebhookSettings.cs" />
<Compile Include="Notifications\Webhook\Webhook.cs" />
<Compile Include="Organizer\NamingConfigRepository.cs" />
<Compile Include="Notifications\Twitter\Twitter.cs" />
<Compile Include="Notifications\Twitter\TwitterService.cs" />
@ -992,6 +1000,7 @@
<Compile Include="Validation\Paths\SeriesExistsValidator.cs" />
<Compile Include="Validation\Paths\SeriesPathValidator.cs" />
<Compile Include="Validation\RuleBuilderExtensions.cs" />
<Compile Include="Validation\UrlValidator.cs" />
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.0,Profile=Client">

@ -0,0 +1,28 @@
using FluentValidation;
using FluentValidation.Validators;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Core.Validation
{
public static class UrlValidation
{
public static IRuleBuilderOptions<T, string> IsValidUrl<T>(this IRuleBuilder<T, string> 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();
}
}
}

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

@ -0,0 +1,8 @@
<div class="form-group {{#if advanced}}advanced-setting{{/if}}">
<label class="col-sm-3 control-label">{{label}}</label>
<div class="col-sm-5">
<input type="url" name="fields.{{order}}.value" validation-name="{{name}}" spellcheck="false" class="form-control"/>
</div>
{{> FormHelpPartial}}
</div>
Loading…
Cancel
Save