mirror of
https://github.com/Radarr/Radarr.git
synced 2024-11-04 10:02:40 +01:00
Notifications can be tested
Notification ImplementationType was added for showing in UI (Humanized/Title cased of Implementation)
This commit is contained in:
parent
1f4cf0034e
commit
8cac7ed1cd
@ -8,6 +8,7 @@ namespace NzbDrone.Api.Notifications
|
||||
public class NotificationResource : RestResource
|
||||
{
|
||||
public String Name { get; set; }
|
||||
public String ImplementationName { get; set; }
|
||||
public Boolean OnGrab { get; set; }
|
||||
public Boolean OnDownload { get; set; }
|
||||
public List<Field> Fields { get; set; }
|
||||
|
@ -42,8 +42,7 @@ public void GetSectionKeys_should_return_single_section_key_when_only_one_show_s
|
||||
Mocker.GetMock<IHttpProvider>().Setup(s => s.DownloadStream("http://localhost:32400/library/sections", null))
|
||||
.Returns(stream);
|
||||
|
||||
|
||||
var result = Mocker.Resolve<PlexProvider>().GetSectionKeys("localhost:32400");
|
||||
var result = Mocker.Resolve<PlexService>().GetSectionKeys(new PlexServerSettings { Host = "localhost", Port = 32400 });
|
||||
|
||||
|
||||
result.Should().HaveCount(1);
|
||||
@ -62,7 +61,7 @@ public void GetSectionKeys_should_return_single_section_key_when_only_one_show_s
|
||||
.Returns(stream);
|
||||
|
||||
|
||||
var result = Mocker.Resolve<PlexProvider>().GetSectionKeys("localhost:32400");
|
||||
var result = Mocker.Resolve<PlexService>().GetSectionKeys(new PlexServerSettings { Host = "localhost", Port = 32400 });
|
||||
|
||||
|
||||
result.Should().HaveCount(1);
|
||||
@ -81,7 +80,7 @@ public void GetSectionKeys_should_return_multiple_section_keys_when_there_are_mu
|
||||
.Returns(stream);
|
||||
|
||||
|
||||
var result = Mocker.Resolve<PlexProvider>().GetSectionKeys("localhost:32400");
|
||||
var result = Mocker.Resolve<PlexService>().GetSectionKeys(new PlexServerSettings { Host = "localhost", Port = 32400 });
|
||||
|
||||
|
||||
result.Should().HaveCount(2);
|
||||
@ -101,7 +100,7 @@ public void UpdateSection_should_update_section()
|
||||
.Returns(response);
|
||||
|
||||
|
||||
Mocker.Resolve<PlexProvider>().UpdateSection("localhost:32400", 5);
|
||||
Mocker.Resolve<PlexService>().UpdateSection(new PlexServerSettings { Host = "localhost", Port = 32400 }, 5);
|
||||
|
||||
|
||||
|
||||
@ -121,7 +120,7 @@ public void Notify_should_send_notification()
|
||||
.Returns("ok");
|
||||
|
||||
|
||||
Mocker.Resolve<PlexProvider>().Notify(_clientSettings, header, message);
|
||||
Mocker.Resolve<PlexService>().Notify(_clientSettings, header, message);
|
||||
|
||||
|
||||
fakeHttp.Verify(v => v.DownloadString(expectedUrl), Times.Once());
|
||||
@ -142,7 +141,7 @@ public void Notify_should_send_notification_with_credentials_when_configured()
|
||||
.Returns("ok");
|
||||
|
||||
|
||||
Mocker.Resolve<PlexProvider>().Notify(_clientSettings, header, message);
|
||||
Mocker.Resolve<PlexService>().Notify(_clientSettings, header, message);
|
||||
|
||||
|
||||
fakeHttp.Verify(v => v.DownloadString(expectedUrl, "plex", "plex"), Times.Once());
|
||||
|
@ -9,130 +9,40 @@ namespace NzbDrone.Core.Test.NotificationTests
|
||||
{
|
||||
[Explicit]
|
||||
[TestFixture]
|
||||
public class ProwlProviderTest : CoreTest
|
||||
public class ProwlProviderTest : CoreTest<ProwlService>
|
||||
{
|
||||
private const string _apiKey = "c3bdc0f48168f72d546cc6872925b160f5cbffc1";
|
||||
private const string _apiKey2 = "46a710a46b111b0b8633819b0d8a1e0272a3affa";
|
||||
private const string _apiKey = "66e9f688b512152eb2688f0486ae542c76e564a2";
|
||||
|
||||
private const string _badApiKey = "1234567890abcdefghijklmnopqrstuvwxyz1234";
|
||||
|
||||
[Test]
|
||||
public void Verify_should_return_true_for_a_valid_apiKey()
|
||||
public void Verify_should_not_throw_for_a_valid_apiKey()
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
var result = Mocker.Resolve<ProwlProvider>().Verify(_apiKey);
|
||||
|
||||
|
||||
result.Should().BeTrue();
|
||||
Subject.Verify(_apiKey);
|
||||
ExceptionVerification.ExpectedWarns(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Verify_should_return_false_for_an_invalid_apiKey()
|
||||
public void Verify_should_throw_for_an_invalid_apiKey()
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
var result = Mocker.Resolve<ProwlProvider>().Verify(_badApiKey);
|
||||
|
||||
Assert.Throws<InvalidApiKeyException>(() => Subject.Verify(_badApiKey));
|
||||
|
||||
ExceptionVerification.ExpectedWarns(1);
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SendNotification_should_return_true_for_a_valid_apiKey()
|
||||
public void SendNotification_should_not_throw_for_a_valid_apiKey()
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
var result = Mocker.Resolve<ProwlProvider>().SendNotification("NzbDrone Test", "This is a test message from NzbDrone", _apiKey);
|
||||
|
||||
|
||||
result.Should().BeTrue();
|
||||
Subject.SendNotification("NzbDrone Test", "This is a test message from NzbDrone", _apiKey);
|
||||
ExceptionVerification.ExpectedWarns(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SendNotification_should_return_false_for_an_invalid_apiKey()
|
||||
public void SendNotification_should_log_a_warning_for_an_invalid_apiKey()
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
var result = Mocker.Resolve<ProwlProvider>().SendNotification("NzbDrone Test", "This is a test message from NzbDrone", _badApiKey);
|
||||
|
||||
Subject.SendNotification("NzbDrone Test", "This is a test message from NzbDrone", _badApiKey);
|
||||
|
||||
ExceptionVerification.ExpectedWarns(1);
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SendNotification_should_alert_with_high_priority()
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
var result = Mocker.Resolve<ProwlProvider>().SendNotification("NzbDrone Test", "This is a test message from NzbDrone (High)", _apiKey, NotificationPriority.High);
|
||||
|
||||
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SendNotification_should_alert_with_VeryLow_priority()
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
var result = Mocker.Resolve<ProwlProvider>().SendNotification("NzbDrone Test", "This is a test message from NzbDrone (VeryLow)", _apiKey, NotificationPriority.VeryLow);
|
||||
|
||||
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SendNotification_should_have_a_call_back_url()
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
var result = Mocker.Resolve<ProwlProvider>().SendNotification("NzbDrone Test", "This is a test message from NzbDrone", _apiKey, NotificationPriority.Normal, "http://www.nzbdrone.com");
|
||||
|
||||
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SendNotification_should_return_true_for_two_valid_apiKey()
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
var result = Mocker.Resolve<ProwlProvider>().SendNotification("NzbDrone Test", "This is a test message from NzbDrone", _apiKey + ", " + _apiKey2);
|
||||
|
||||
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SendNotification_should_return_true_for_valid_apiKey_with_bad_apiKey()
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
var result = Mocker.Resolve<ProwlProvider>().SendNotification("NzbDrone Test", "This is a test message from NzbDrone", _apiKey + ", " + _badApiKey);
|
||||
|
||||
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
@ -185,7 +185,6 @@
|
||||
<Compile Include="DecisionEngineTests\QualityUpgradableSpecificationFixture.cs" />
|
||||
<Compile Include="ProviderTests\DiskProviderTests\FreeDiskSpaceTest.cs" />
|
||||
<Compile Include="NotificationTests\ProwlProviderTest.cs" />
|
||||
<Compile Include="NotificationTests\GrowlProviderTest.cs" />
|
||||
<Compile Include="ProviderTests\DiskProviderTests\ExtractArchiveFixture.cs" />
|
||||
<Compile Include="ProviderTests\PostDownloadProviderTests\DropFolderImportServiceFixture.cs" />
|
||||
<Compile Include="SeriesStatsTests\SeriesStatisticsFixture.cs" />
|
||||
|
@ -5,9 +5,9 @@ namespace NzbDrone.Core.Notifications.Email
|
||||
{
|
||||
public class Email : NotificationBase<EmailSettings>
|
||||
{
|
||||
private readonly EmailProvider _smtpProvider;
|
||||
private readonly IEmailService _smtpProvider;
|
||||
|
||||
public Email(EmailProvider smtpProvider)
|
||||
public Email(IEmailService smtpProvider)
|
||||
{
|
||||
_smtpProvider = smtpProvider;
|
||||
}
|
||||
@ -17,6 +17,11 @@ public override string Name
|
||||
get { return "Email"; }
|
||||
}
|
||||
|
||||
public override string ImplementationName
|
||||
{
|
||||
get { return "Email"; }
|
||||
}
|
||||
|
||||
public override void OnGrab(string message)
|
||||
{
|
||||
const string subject = "NzbDrone [TV] - Grabbed";
|
||||
|
@ -2,19 +2,26 @@
|
||||
using System.Net;
|
||||
using System.Net.Mail;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Messaging;
|
||||
using Omu.ValueInjecter;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Email
|
||||
{
|
||||
public class EmailProvider
|
||||
public interface IEmailService
|
||||
{
|
||||
void SendEmail(EmailSettings settings, string subject, string body, bool htmlBody = false);
|
||||
}
|
||||
|
||||
public class EmailService : IEmailService, IExecute<TestEmailCommand>
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public EmailProvider(Logger logger)
|
||||
public EmailService(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public virtual void SendEmail(EmailSettings settings, string subject, string body, bool htmlBody = false)
|
||||
public void SendEmail(EmailSettings settings, string subject, string body, bool htmlBody = false)
|
||||
{
|
||||
var email = new MailMessage();
|
||||
email.From = new MailAddress(settings.From);
|
||||
@ -32,7 +39,7 @@ public virtual void SendEmail(EmailSettings settings, string subject, string bod
|
||||
|
||||
try
|
||||
{
|
||||
Send(email, settings.Server, settings.Port, settings.UseSsl, credentials);
|
||||
Send(email, settings.Server, settings.Port, settings.Ssl, credentials);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@ -41,7 +48,7 @@ public virtual void SendEmail(EmailSettings settings, string subject, string bod
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Send(MailMessage email, string server, int port, bool ssl, NetworkCredential credentials)
|
||||
private void Send(MailMessage email, string server, int port, bool ssl, NetworkCredential credentials)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -60,5 +67,15 @@ public virtual void Send(MailMessage email, string server, int port, bool ssl, N
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Execute(TestEmailCommand message)
|
||||
{
|
||||
var settings = new EmailSettings();
|
||||
settings.InjectFrom(message);
|
||||
|
||||
var body = "Success! You have properly configured your email notification settings";
|
||||
|
||||
SendEmail(settings, "NzbDrone - Test Notification", body);
|
||||
}
|
||||
}
|
||||
}
|
@ -11,16 +11,16 @@ public class EmailSettings : INotifcationSettings
|
||||
[FieldDefinition(1, Label = "Port")]
|
||||
public Int32 Port { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Use SSL", HelpText = "Does your Email server use SSL?")]
|
||||
public Boolean UseSsl { get; set; }
|
||||
[FieldDefinition(2, Label = "SSL", Type = FieldType.Checkbox)]
|
||||
public Boolean Ssl { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Username")]
|
||||
public String Username { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Password")]
|
||||
[FieldDefinition(4, Label = "Password", Type = FieldType.Password)]
|
||||
public String Password { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Sender Address")]
|
||||
[FieldDefinition(5, Label = "From Address")]
|
||||
public String From { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "Recipient Address")]
|
||||
|
15
NzbDrone.Core/Notifications/Email/TestEmailCommand.cs
Normal file
15
NzbDrone.Core/Notifications/Email/TestEmailCommand.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using NzbDrone.Common.Messaging;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Email
|
||||
{
|
||||
public class TestEmailCommand : ICommand
|
||||
{
|
||||
public string Server { get; set; }
|
||||
public int Port { get; set; }
|
||||
public bool Ssl { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string From { get; set; }
|
||||
public string To { get; set; }
|
||||
}
|
||||
}
|
@ -7,9 +7,9 @@ namespace NzbDrone.Core.Notifications.Growl
|
||||
{
|
||||
public class Growl : NotificationBase<GrowlSettings>
|
||||
{
|
||||
private readonly GrowlProvider _growlProvider;
|
||||
private readonly IGrowlService _growlProvider;
|
||||
|
||||
public Growl(GrowlProvider growlProvider)
|
||||
public Growl(IGrowlService growlProvider)
|
||||
{
|
||||
_growlProvider = growlProvider;
|
||||
}
|
||||
@ -19,6 +19,11 @@ public override string Name
|
||||
get { return "Growl"; }
|
||||
}
|
||||
|
||||
public override string ImplementationName
|
||||
{
|
||||
get { return "Growl"; }
|
||||
}
|
||||
|
||||
public override void OnGrab(string message)
|
||||
{
|
||||
const string title = "Episode Grabbed";
|
||||
|
@ -3,43 +3,33 @@
|
||||
using System.Linq;
|
||||
using Growl.Connector;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Messaging;
|
||||
using GrowlNotification = Growl.Connector.Notification;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Growl
|
||||
{
|
||||
public class GrowlProvider
|
||||
public interface IGrowlService
|
||||
{
|
||||
void SendNotification(string title, string message, string notificationTypeName, string hostname, int port, string password);
|
||||
}
|
||||
|
||||
public class GrowlService : IGrowlService, IExecute<TestGrowlCommand>
|
||||
{
|
||||
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
private readonly Application _growlApplication = new Application("NzbDrone");
|
||||
private GrowlConnector _growlConnector;
|
||||
private List<NotificationType> _notificationTypes;
|
||||
private readonly List<NotificationType> _notificationTypes;
|
||||
|
||||
public GrowlProvider()
|
||||
public GrowlService()
|
||||
{
|
||||
_notificationTypes = GetNotificationTypes();
|
||||
_growlApplication.Icon = "https://github.com/NzbDrone/NzbDrone/raw/master/NzbDrone.Core/NzbDrone.jpg";
|
||||
}
|
||||
|
||||
public virtual void Register(string hostname, int port, string password)
|
||||
{
|
||||
Logger.Trace("Registering NzbDrone with Growl host: {0}:{1}", hostname, port);
|
||||
_growlConnector = new GrowlConnector(password, hostname, port);
|
||||
_growlConnector.Register(_growlApplication, _notificationTypes.ToArray());
|
||||
}
|
||||
|
||||
public virtual void TestNotification(string hostname, int port, string password)
|
||||
{
|
||||
const string title = "Test Notification";
|
||||
const string message = "This is a test message from NzbDrone";
|
||||
|
||||
SendNotification(title, message, "TEST", hostname, port, password);
|
||||
}
|
||||
|
||||
public virtual void SendNotification(string title, string message, string notificationTypeName, string hostname, int port, string password)
|
||||
public void SendNotification(string title, string message, string notificationTypeName, string hostname, int port, string password)
|
||||
{
|
||||
var notificationType = _notificationTypes.Single(n => n.Name == notificationTypeName);
|
||||
|
||||
var notification = new GrowlNotification("NzbDrone", notificationType.Name, DateTime.Now.Ticks.ToString(), title, message);
|
||||
|
||||
_growlConnector = new GrowlConnector(password, hostname, port);
|
||||
@ -48,6 +38,13 @@ public virtual void SendNotification(string title, string message, string notifi
|
||||
_growlConnector.Notify(notification);
|
||||
}
|
||||
|
||||
private void Register(string host, int port, string password)
|
||||
{
|
||||
Logger.Trace("Registering NzbDrone with Growl host: {0}:{1}", host, port);
|
||||
_growlConnector = new GrowlConnector(password, host, port);
|
||||
_growlConnector.Register(_growlApplication, _notificationTypes.ToArray());
|
||||
}
|
||||
|
||||
private List<NotificationType> GetNotificationTypes()
|
||||
{
|
||||
var notificationTypes = new List<NotificationType>();
|
||||
@ -57,5 +54,15 @@ private List<NotificationType> GetNotificationTypes()
|
||||
|
||||
return notificationTypes;
|
||||
}
|
||||
|
||||
public void Execute(TestGrowlCommand message)
|
||||
{
|
||||
Register(message.Host, message.Port, message.Password);
|
||||
|
||||
const string title = "Test Notification";
|
||||
const string body = "This is a test message from NzbDrone";
|
||||
|
||||
SendNotification(title, body, "TEST", message.Host, message.Port, message.Password);
|
||||
}
|
||||
}
|
||||
}
|
11
NzbDrone.Core/Notifications/Growl/TestGrowlCommand.cs
Normal file
11
NzbDrone.Core/Notifications/Growl/TestGrowlCommand.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using NzbDrone.Common.Messaging;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Growl
|
||||
{
|
||||
public class TestGrowlCommand : ICommand
|
||||
{
|
||||
public string Host { get; set; }
|
||||
public int Port { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ namespace NzbDrone.Core.Notifications
|
||||
public interface INotification
|
||||
{
|
||||
string Name { get; }
|
||||
string ImplementationName { get; }
|
||||
|
||||
NotificationDefinition InstanceDefinition { get; set; }
|
||||
|
||||
|
@ -9,6 +9,7 @@ public class Notification
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string ImplementationName { get; set; }
|
||||
public bool OnGrab { get; set; }
|
||||
public bool OnDownload { get; set; }
|
||||
public INotifcationSettings Settings { get; set; }
|
||||
|
@ -8,6 +8,7 @@ namespace NzbDrone.Core.Notifications
|
||||
public abstract class NotificationBase<TSetting> : INotification where TSetting : class, INotifcationSettings, new()
|
||||
{
|
||||
public abstract string Name { get; }
|
||||
public abstract string ImplementationName { get; }
|
||||
|
||||
public NotificationDefinition InstanceDefinition { get; set; }
|
||||
|
||||
|
@ -69,7 +69,7 @@ public List<Notification> Schema()
|
||||
var newNotification = new Notification();
|
||||
newNotification.Instance = (INotification)_container.Resolve(type);
|
||||
newNotification.Id = i;
|
||||
newNotification.Name = notification.Name;
|
||||
newNotification.ImplementationName = notification.ImplementationName;
|
||||
|
||||
var instanceType = newNotification.Instance.GetType();
|
||||
var baseGenArgs = instanceType.BaseType.GetGenericArguments();
|
||||
@ -120,6 +120,7 @@ private Notification ToNotification(NotificationDefinition definition)
|
||||
notification.Instance = GetInstance(definition);
|
||||
notification.Name = definition.Name;
|
||||
notification.Implementation = definition.Implementation;
|
||||
notification.ImplementationName = notification.Instance.ImplementationName;
|
||||
notification.Settings = ((dynamic)notification.Instance).ImportSettingsFromJson(definition.Settings);
|
||||
|
||||
return notification;
|
||||
|
@ -6,9 +6,9 @@ namespace NzbDrone.Core.Notifications.Plex
|
||||
{
|
||||
public class PlexClient : NotificationBase<PlexClientSettings>
|
||||
{
|
||||
private readonly PlexProvider _plexProvider;
|
||||
private readonly IPlexService _plexProvider;
|
||||
|
||||
public PlexClient(PlexProvider plexProvider)
|
||||
public PlexClient(IPlexService plexProvider)
|
||||
{
|
||||
_plexProvider = plexProvider;
|
||||
}
|
||||
@ -18,6 +18,11 @@ public override string Name
|
||||
get { return "Plex Client"; }
|
||||
}
|
||||
|
||||
public override string ImplementationName
|
||||
{
|
||||
get { return "Plex Client"; }
|
||||
}
|
||||
|
||||
public override void OnGrab(string message)
|
||||
{
|
||||
const string header = "NzbDrone [TV] - Grabbed";
|
||||
|
@ -1,88 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Plex
|
||||
{
|
||||
public class PlexProvider
|
||||
{
|
||||
private readonly IHttpProvider _httpProvider;
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
public PlexProvider(IHttpProvider httpProvider)
|
||||
{
|
||||
_httpProvider = httpProvider;
|
||||
}
|
||||
|
||||
public virtual void Notify(PlexClientSettings settings, string header, string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
var command = String.Format("ExecBuiltIn(Notification({0}, {1}))", header, message);
|
||||
SendCommand(settings.Host, settings.Port, command, settings.Username, settings.Password);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
logger.WarnException("Failed to send notification to Plex Client: " + settings.Host, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void UpdateLibrary(string host)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Trace("Sending Update Request to Plex Server");
|
||||
var sections = GetSectionKeys(host);
|
||||
sections.ForEach(s => UpdateSection(host, s));
|
||||
}
|
||||
|
||||
catch(Exception ex)
|
||||
{
|
||||
logger.WarnException("Failed to Update Plex host: " + host, ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public List<int> GetSectionKeys(string host)
|
||||
{
|
||||
logger.Trace("Getting sections from Plex host: {0}", host);
|
||||
var url = String.Format("http://{0}/library/sections", host);
|
||||
var xmlStream = _httpProvider.DownloadStream(url, null);
|
||||
var xDoc = XDocument.Load(xmlStream);
|
||||
var mediaContainer = xDoc.Descendants("MediaContainer").FirstOrDefault();
|
||||
var directories = mediaContainer.Descendants("Directory").Where(x => x.Attribute("type").Value == "show");
|
||||
|
||||
return directories.Select(d => Int32.Parse(d.Attribute("key").Value)).ToList();
|
||||
}
|
||||
|
||||
public void UpdateSection(string host, int key)
|
||||
{
|
||||
logger.Trace("Updating Plex host: {0}, Section: {1}", host, key);
|
||||
var url = String.Format("http://{0}/library/sections/{1}/refresh", host, key);
|
||||
_httpProvider.DownloadString(url);
|
||||
}
|
||||
|
||||
public virtual string SendCommand(string host, int port, string command, string username, string password)
|
||||
{
|
||||
var url = String.Format("http://{0}:{1}/xbmcCmds/xbmcHttp?command={2}", host, port, command);
|
||||
|
||||
if (!String.IsNullOrEmpty(username))
|
||||
{
|
||||
return _httpProvider.DownloadString(url, username, password);
|
||||
}
|
||||
|
||||
return _httpProvider.DownloadString(url);
|
||||
}
|
||||
|
||||
public virtual void TestNotification(string host, int port, string username, string password)
|
||||
{
|
||||
logger.Trace("Sending Test Notifcation to XBMC Host: {0}", host);
|
||||
var command = String.Format("ExecBuiltIn(Notification({0}, {1}))", "Test Notification", "Success! Notifications are setup correctly");
|
||||
SendCommand(host, port, command, username, password);
|
||||
}
|
||||
}
|
||||
}
|
@ -6,9 +6,9 @@ namespace NzbDrone.Core.Notifications.Plex
|
||||
{
|
||||
public class PlexServer : NotificationBase<PlexServerSettings>
|
||||
{
|
||||
private readonly PlexProvider _plexProvider;
|
||||
private readonly IPlexService _plexProvider;
|
||||
|
||||
public PlexServer(PlexProvider plexProvider)
|
||||
public PlexServer(IPlexService plexProvider)
|
||||
{
|
||||
_plexProvider = plexProvider;
|
||||
}
|
||||
@ -18,6 +18,11 @@ public override string Name
|
||||
get { return "Plex Server"; }
|
||||
}
|
||||
|
||||
public override string ImplementationName
|
||||
{
|
||||
get { return "Plex Server"; }
|
||||
}
|
||||
|
||||
public override void OnGrab(string message)
|
||||
{
|
||||
}
|
||||
@ -36,7 +41,7 @@ private void UpdateIfEnabled()
|
||||
{
|
||||
if (Settings.UpdateLibrary)
|
||||
{
|
||||
_plexProvider.UpdateLibrary(Settings.Host);
|
||||
_plexProvider.UpdateLibrary(Settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,10 @@ public class PlexServerSettings : INotifcationSettings
|
||||
[FieldDefinition(0, Label = "Host", HelpText = "Plex Server Host (IP or Hostname)")]
|
||||
public String Host { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Update Library", HelpText = "Update Library on Download/Rename")]
|
||||
[FieldDefinition(1, Label = "Port")]
|
||||
public Int32 Port { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Update Library")]
|
||||
public Boolean UpdateLibrary { get; set; }
|
||||
|
||||
public bool IsValid
|
||||
|
110
NzbDrone.Core/Notifications/Plex/PlexService.cs
Normal file
110
NzbDrone.Core/Notifications/Plex/PlexService.cs
Normal file
@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.Messaging;
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Plex
|
||||
{
|
||||
public interface IPlexService
|
||||
{
|
||||
void Notify(PlexClientSettings settings, string header, string message);
|
||||
void UpdateLibrary(PlexServerSettings settings);
|
||||
}
|
||||
|
||||
public class PlexService : IPlexService, IExecute<TestPlexClientCommand>, IExecute<TestPlexServerCommand>
|
||||
{
|
||||
private readonly IHttpProvider _httpProvider;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public PlexService(IHttpProvider httpProvider, Logger logger)
|
||||
{
|
||||
_httpProvider = httpProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Notify(PlexClientSettings settings, string header, string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
var command = String.Format("ExecBuiltIn(Notification({0}, {1}))", header, message);
|
||||
SendCommand(settings.Host, settings.Port, command, settings.Username, settings.Password);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
_logger.WarnException("Failed to send notification to Plex Client: " + settings.Host, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateLibrary(PlexServerSettings settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Trace("Sending Update Request to Plex Server");
|
||||
var sections = GetSectionKeys(settings);
|
||||
sections.ForEach(s => UpdateSection(settings, s));
|
||||
}
|
||||
|
||||
catch(Exception ex)
|
||||
{
|
||||
_logger.WarnException("Failed to Update Plex host: " + settings.Host, ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public List<int> GetSectionKeys(PlexServerSettings settings)
|
||||
{
|
||||
_logger.Trace("Getting sections from Plex host: {0}", settings.Host);
|
||||
var url = String.Format("http://{0}:{1}/library/sections", settings.Host, settings.Port);
|
||||
var xmlStream = _httpProvider.DownloadStream(url, null);
|
||||
var xDoc = XDocument.Load(xmlStream);
|
||||
var mediaContainer = xDoc.Descendants("MediaContainer").FirstOrDefault();
|
||||
var directories = mediaContainer.Descendants("Directory").Where(x => x.Attribute("type").Value == "show");
|
||||
|
||||
return directories.Select(d => Int32.Parse(d.Attribute("key").Value)).ToList();
|
||||
}
|
||||
|
||||
public void UpdateSection(PlexServerSettings settings, int key)
|
||||
{
|
||||
_logger.Trace("Updating Plex host: {0}, Section: {1}", settings.Host, key);
|
||||
var url = String.Format("http://{0}:{1}/library/sections/{2}/refresh", settings.Host, settings.Port, key);
|
||||
_httpProvider.DownloadString(url);
|
||||
}
|
||||
|
||||
public string SendCommand(string host, int port, string command, string username, string password)
|
||||
{
|
||||
var url = String.Format("http://{0}:{1}/xbmcCmds/xbmcHttp?command={2}", host, port, command);
|
||||
|
||||
if (!String.IsNullOrEmpty(username))
|
||||
{
|
||||
return _httpProvider.DownloadString(url, username, password);
|
||||
}
|
||||
|
||||
return _httpProvider.DownloadString(url);
|
||||
}
|
||||
|
||||
public void Execute(TestPlexClientCommand message)
|
||||
{
|
||||
_logger.Trace("Sending Test Notifcation to Plex Client: {0}", message.Host);
|
||||
var command = String.Format("ExecBuiltIn(Notification({0}, {1}))", "Test Notification", "Success! Notifications are setup correctly");
|
||||
var result = SendCommand(message.Host, message.Port, command, message.Username, message.Password);
|
||||
|
||||
if (String.IsNullOrWhiteSpace(result) ||
|
||||
result.IndexOf("error", StringComparison.InvariantCultureIgnoreCase) > -1)
|
||||
{
|
||||
throw new Exception("Unable to connect to Plex Client");
|
||||
}
|
||||
}
|
||||
|
||||
public void Execute(TestPlexServerCommand message)
|
||||
{
|
||||
if (!GetSectionKeys(new PlexServerSettings {Host = message.Host, Port = message.Port}).Any())
|
||||
{
|
||||
throw new Exception("Unable to connect to Plex Server");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
NzbDrone.Core/Notifications/Plex/TestPlexClientCommand.cs
Normal file
12
NzbDrone.Core/Notifications/Plex/TestPlexClientCommand.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using NzbDrone.Common.Messaging;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Plex
|
||||
{
|
||||
public class TestPlexClientCommand : ICommand
|
||||
{
|
||||
public string Host { get; set; }
|
||||
public int Port { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
||||
}
|
10
NzbDrone.Core/Notifications/Plex/TestPlexServerCommand.cs
Normal file
10
NzbDrone.Core/Notifications/Plex/TestPlexServerCommand.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using NzbDrone.Common.Messaging;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Plex
|
||||
{
|
||||
public class TestPlexServerCommand : ICommand
|
||||
{
|
||||
public string Host { get; set; }
|
||||
public int Port { get; set; }
|
||||
}
|
||||
}
|
18
NzbDrone.Core/Notifications/Prowl/InvalidApiKeyException.cs
Normal file
18
NzbDrone.Core/Notifications/Prowl/InvalidApiKeyException.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Prowl
|
||||
{
|
||||
public class InvalidApiKeyException : Exception
|
||||
{
|
||||
public InvalidApiKeyException()
|
||||
{
|
||||
}
|
||||
|
||||
public InvalidApiKeyException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -5,9 +5,9 @@ namespace NzbDrone.Core.Notifications.Prowl
|
||||
{
|
||||
public class Prowl : NotificationBase<ProwlSettings>
|
||||
{
|
||||
private readonly ProwlProvider _prowlProvider;
|
||||
private readonly IProwlService _prowlProvider;
|
||||
|
||||
public Prowl(ProwlProvider prowlProvider)
|
||||
public Prowl(IProwlService prowlProvider)
|
||||
{
|
||||
_prowlProvider = prowlProvider;
|
||||
}
|
||||
@ -17,6 +17,11 @@ public override string Name
|
||||
get { return "Prowl"; }
|
||||
}
|
||||
|
||||
public override string ImplementationName
|
||||
{
|
||||
get { return "Prowl"; }
|
||||
}
|
||||
|
||||
public override void OnGrab(string message)
|
||||
{
|
||||
const string title = "Episode Grabbed";
|
||||
|
@ -1,78 +0,0 @@
|
||||
using System;
|
||||
using NLog;
|
||||
using Prowlin;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Prowl
|
||||
{
|
||||
public class ProwlProvider
|
||||
{
|
||||
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
public virtual bool Verify(string apiKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
var verificationRequest = new Verification();
|
||||
verificationRequest.ApiKey = apiKey;
|
||||
|
||||
var client = new ProwlClient();
|
||||
|
||||
Logger.Trace("Verifying API Key: {0}", apiKey);
|
||||
|
||||
var verificationResult = client.SendVerification(verificationRequest);
|
||||
if (String.IsNullOrWhiteSpace(verificationResult.ErrorMessage) && verificationResult.ResultCode == "200")
|
||||
return true;
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.TraceException(ex.Message, ex);
|
||||
Logger.Warn("Invalid API Key: {0}", apiKey);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual bool SendNotification(string title, string message, string apiKey, NotificationPriority priority = NotificationPriority.Normal, string url = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var notification = new Prowlin.Notification
|
||||
{
|
||||
Application = "NzbDrone",
|
||||
Description = message,
|
||||
Event = title,
|
||||
Priority = priority,
|
||||
Url = url
|
||||
};
|
||||
|
||||
notification.AddApiKey(apiKey.Trim());
|
||||
|
||||
var client = new ProwlClient();
|
||||
|
||||
Logger.Trace("Sending Prowl Notification");
|
||||
|
||||
var notificationResult = client.SendNotification(notification);
|
||||
|
||||
if (String.IsNullOrWhiteSpace(notificationResult.ErrorMessage))
|
||||
return true;
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.TraceException(ex.Message, ex);
|
||||
Logger.Warn("Invalid API Key: {0}", apiKey);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual void TestNotification(string apiKeys)
|
||||
{
|
||||
const string title = "Test Notification";
|
||||
const string message = "This is a test message from NzbDrone";
|
||||
|
||||
SendNotification(title, message, apiKeys);
|
||||
}
|
||||
}
|
||||
}
|
93
NzbDrone.Core/Notifications/Prowl/ProwlService.cs
Normal file
93
NzbDrone.Core/Notifications/Prowl/ProwlService.cs
Normal file
@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Messaging;
|
||||
using Prowlin;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Prowl
|
||||
{
|
||||
public interface IProwlService
|
||||
{
|
||||
void SendNotification(string title, string message, string apiKey, NotificationPriority priority = NotificationPriority.Normal, string url = null);
|
||||
}
|
||||
|
||||
public class ProwlService : IProwlService, IExecute<TestProwlCommand>
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ProwlService(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void SendNotification(string title, string message, string apiKey, NotificationPriority priority = NotificationPriority.Normal, string url = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var notification = new Prowlin.Notification
|
||||
{
|
||||
Application = "NzbDrone",
|
||||
Description = message,
|
||||
Event = title,
|
||||
Priority = priority,
|
||||
Url = url
|
||||
};
|
||||
|
||||
notification.AddApiKey(apiKey.Trim());
|
||||
|
||||
var client = new ProwlClient();
|
||||
|
||||
_logger.Trace("Sending Prowl Notification");
|
||||
|
||||
var notificationResult = client.SendNotification(notification);
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(notificationResult.ErrorMessage))
|
||||
{
|
||||
throw new InvalidApiKeyException("API Key: " + apiKey + " is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.TraceException(ex.Message, ex);
|
||||
_logger.Warn("Invalid API Key: {0}", apiKey);
|
||||
}
|
||||
}
|
||||
|
||||
public void Verify(string apiKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
var verificationRequest = new Verification();
|
||||
verificationRequest.ApiKey = apiKey;
|
||||
|
||||
var client = new ProwlClient();
|
||||
|
||||
_logger.Trace("Verifying API Key: {0}", apiKey);
|
||||
|
||||
var verificationResult = client.SendVerification(verificationRequest);
|
||||
if (!String.IsNullOrWhiteSpace(verificationResult.ErrorMessage) &&
|
||||
verificationResult.ResultCode != "200")
|
||||
{
|
||||
throw new InvalidApiKeyException("API Key: " + apiKey + " is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.TraceException(ex.Message, ex);
|
||||
_logger.Warn("Invalid API Key: {0}", apiKey);
|
||||
throw new InvalidApiKeyException("API Key: " + apiKey + " is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
public void Execute(TestProwlCommand message)
|
||||
{
|
||||
Verify(message.ApiKey);
|
||||
|
||||
const string title = "Test Notification";
|
||||
const string body = "This is a test message from NzbDrone";
|
||||
|
||||
SendNotification(title, body, message.ApiKey);
|
||||
}
|
||||
}
|
||||
}
|
10
NzbDrone.Core/Notifications/Prowl/TestProwlCommand.cs
Normal file
10
NzbDrone.Core/Notifications/Prowl/TestProwlCommand.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using NzbDrone.Common.Messaging;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Prowl
|
||||
{
|
||||
public class TestProwlCommand : ICommand
|
||||
{
|
||||
public string ApiKey { get; set; }
|
||||
public int Priority { get; set; }
|
||||
}
|
||||
}
|
@ -17,6 +17,11 @@ public override string Name
|
||||
get { return "XBMC"; }
|
||||
}
|
||||
|
||||
public override string ImplementationName
|
||||
{
|
||||
get { return "XBMC"; }
|
||||
}
|
||||
|
||||
public override void OnGrab(string message)
|
||||
{
|
||||
const string header = "NzbDrone [TV] - Grabbed";
|
||||
|
@ -266,7 +266,9 @@
|
||||
<Compile Include="MediaFiles\Events\EpisodeDownloadedEvent.cs" />
|
||||
<Compile Include="Download\EpisodeGrabbedEvent.cs" />
|
||||
<Compile Include="Download\SeriesRenamedEvent.cs" />
|
||||
<Compile Include="Notifications\Email\TestEmailCommand.cs" />
|
||||
<Compile Include="Notifications\Growl\GrowlSettings.cs" />
|
||||
<Compile Include="Notifications\Growl\TestGrowlCommand.cs" />
|
||||
<Compile Include="Notifications\NotificationSettingsProvider.cs" />
|
||||
<Compile Include="Notifications\INotification.cs" />
|
||||
<Compile Include="Notifications\Notification.cs" />
|
||||
@ -324,11 +326,15 @@
|
||||
<Compile Include="MetadataSource\Trakt\Season.cs" />
|
||||
<Compile Include="MetadataSource\Trakt\Show.cs" />
|
||||
<Compile Include="Notifications\INotifcationSettings.cs" />
|
||||
<Compile Include="Notifications\Plex\TestPlexServerCommand.cs" />
|
||||
<Compile Include="Notifications\Plex\PlexServer.cs" />
|
||||
<Compile Include="Notifications\Plex\PlexClientSettings.cs" />
|
||||
<Compile Include="Notifications\Plex\PlexServerSettings.cs" />
|
||||
<Compile Include="Notifications\Plex\TestPlexClientCommand.cs" />
|
||||
<Compile Include="Notifications\Prowl\InvalidApiKeyException.cs" />
|
||||
<Compile Include="Notifications\Prowl\ProwlSettings.cs" />
|
||||
<Compile Include="Notifications\Email\EmailSettings.cs" />
|
||||
<Compile Include="Notifications\Prowl\TestProwlCommand.cs" />
|
||||
<Compile Include="Notifications\Xbmc\HttpApiProvider.cs" />
|
||||
<Compile Include="Notifications\Xbmc\IApiProvider.cs" />
|
||||
<Compile Include="Notifications\Xbmc\InvalidXbmcVersionException.cs" />
|
||||
@ -454,7 +460,7 @@
|
||||
<Compile Include="Notifications\Xbmc\Xbmc.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Notifications\Growl\GrowlProvider.cs">
|
||||
<Compile Include="Notifications\Growl\GrowlService.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="History\HistoryService.cs">
|
||||
@ -472,11 +478,11 @@
|
||||
<Compile Include="MediaFiles\MediaFileService.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Notifications\Plex\PlexProvider.cs" />
|
||||
<Compile Include="Notifications\Plex\PlexService.cs" />
|
||||
<Compile Include="MediaFiles\DownloadedEpisodesImportService.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Notifications\Prowl\ProwlProvider.cs">
|
||||
<Compile Include="Notifications\Prowl\ProwlService.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Qualities\QualityProfileService.cs">
|
||||
@ -492,7 +498,7 @@
|
||||
<Compile Include="Tv\SeriesService.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Notifications\Email\EmailProvider.cs">
|
||||
<Compile Include="Notifications\Email\EmailService.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Notifications\Xbmc\XbmcService.cs">
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="add-notification-item span3">
|
||||
<div class="row">
|
||||
<div class="span3">
|
||||
{{name}}
|
||||
{{implementationName}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -20,7 +20,6 @@ define([
|
||||
|
||||
addNotification: function () {
|
||||
this.model.set('id', undefined);
|
||||
this.model.set('name', '');
|
||||
var view = new NzbDrone.Settings.Notifications.EditView({ model: this.model, notificationCollection: this.notificationCollection });
|
||||
NzbDrone.modalRegion.show(view);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>On Grab</th>
|
||||
<th>On Download</th>
|
||||
<th>Controls</th>
|
||||
|
@ -1,9 +1,9 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
{{#if id}}
|
||||
<h3>Edit</h3>
|
||||
<h3>Edit - {{implementationName}}</h3>
|
||||
{{else}}
|
||||
<h3>Add</h3>
|
||||
<h3>Add - {{implementationName}}</h3>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
@ -1,4 +1,5 @@
|
||||
<td name="name"></td>
|
||||
<td name="implementationName"></td>
|
||||
<td name="onGrab"></td>
|
||||
<td name="onDownload"></td>
|
||||
<td name="cutoff.name"></td>
|
||||
|
Loading…
Reference in New Issue
Block a user