Added Prowl notifications.

pull/6/head
Mark McDowall 13 years ago
parent 1b8e359a63
commit 277b873b39

@ -125,8 +125,8 @@
<virtualDirectory path="/" physicalPath="%NZBDRONE_PATH%\NZBDrone.Web" /> <virtualDirectory path="/" physicalPath="%NZBDRONE_PATH%\NZBDrone.Web" />
</application> </application>
<bindings> <bindings>
<binding protocol="http" bindingInformation="*:8989:localhost" /> <binding protocol="http" bindingInformation="*:12345:localhost" />
<binding protocol="http" bindingInformation="*:8989:" /> <binding protocol="http" bindingInformation="*:12345:" />
</bindings> </bindings>
</site> </site>
<applicationDefaults applicationPool="IISExpressAppPool" /> <applicationDefaults applicationPool="IISExpressAppPool" />

@ -62,6 +62,10 @@
<Reference Include="pnunit.framework"> <Reference Include="pnunit.framework">
<HintPath>..\packages\NUnit.2.5.10.11092\lib\pnunit.framework.dll</HintPath> <HintPath>..\packages\NUnit.2.5.10.11092\lib\pnunit.framework.dll</HintPath>
</Reference> </Reference>
<Reference Include="Prowlin, Version=0.9.4163.39219, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Prowlin 0.9.4163.39219\Prowlin.dll</HintPath>
</Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Configuration" /> <Reference Include="System.Configuration" />
<Reference Include="System.Data" /> <Reference Include="System.Data" />
@ -81,6 +85,7 @@
<Compile Include="JobTests\BacklogSearchJobTest.cs" /> <Compile Include="JobTests\BacklogSearchJobTest.cs" />
<Compile Include="JobTests\BannerDownloadJobTest.cs" /> <Compile Include="JobTests\BannerDownloadJobTest.cs" />
<Compile Include="ProviderTests\ConfigFileProviderTest.cs" /> <Compile Include="ProviderTests\ConfigFileProviderTest.cs" />
<Compile Include="ProviderTests\ProwlProviderTest.cs" />
<Compile Include="ProviderTests\GrowlProviderTest.cs" /> <Compile Include="ProviderTests\GrowlProviderTest.cs" />
<Compile Include="ProviderTests\DiskProviderTests\ExtractArchiveFixture.cs" /> <Compile Include="ProviderTests\DiskProviderTests\ExtractArchiveFixture.cs" />
<Compile Include="ProviderTests\PostDownloadProviderTests\PostDownloadProviderFixture.cs" /> <Compile Include="ProviderTests\PostDownloadProviderTests\PostDownloadProviderFixture.cs" />

@ -0,0 +1,147 @@
using System;
using AutoMoq;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Model;
using NzbDrone.Core.Providers;
using NzbDrone.Core.Repository;
using NzbDrone.Core.Repository.Quality;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
using Prowlin;
// ReSharper disable InconsistentNaming
namespace NzbDrone.Core.Test.ProviderTests
{
[Explicit]
[TestFixture]
public class ProwlProviderTest : TestBase
{
private const string _apiKey = "c3bdc0f48168f72d546cc6872925b160f5cbffc1";
private const string _apiKey2 = "46a710a46b111b0b8633819b0d8a1e0272a3affa";
private const string _badApiKey = "1234567890abcdefghijklmnopqrstuvwxyz1234";
[Test]
public void Verify_should_return_true_for_a_valid_apiKey()
{
//Setup
var mocker = new AutoMoqer(MockBehavior.Strict);
//Act
var result = mocker.Resolve<ProwlProvider>().Verify(_apiKey);
//Assert
result.Should().BeTrue();
}
[Test]
public void Verify_should_return_false_for_an_invalid_apiKey()
{
//Setup
var mocker = new AutoMoqer(MockBehavior.Strict);
//Act
var result = mocker.Resolve<ProwlProvider>().Verify(_badApiKey);
//Assert
ExceptionVerification.ExcpectedWarns(1);
result.Should().BeFalse();
}
[Test]
public void SendNotification_should_return_true_for_a_valid_apiKey()
{
//Setup
var mocker = new AutoMoqer(MockBehavior.Strict);
//Act
var result = mocker.Resolve<ProwlProvider>().SendNotification("NzbDrone Test", "This is a test message from NzbDrone", _apiKey);
//Assert
result.Should().BeTrue();
}
[Test]
public void SendNotification_should_return_false_for_an_invalid_apiKey()
{
//Setup
var mocker = new AutoMoqer(MockBehavior.Strict);
//Act
var result = mocker.Resolve<ProwlProvider>().SendNotification("NzbDrone Test", "This is a test message from NzbDrone", _badApiKey);
//Assert
ExceptionVerification.ExcpectedWarns(1);
result.Should().BeFalse();
}
[Test]
public void SendNotification_should_alert_with_high_priority()
{
//Setup
var mocker = new AutoMoqer(MockBehavior.Strict);
//Act
var result = mocker.Resolve<ProwlProvider>().SendNotification("NzbDrone Test", "This is a test message from NzbDrone (High)", _apiKey, NotificationPriority.High);
//Assert
result.Should().BeTrue();
}
[Test]
public void SendNotification_should_alert_with_VeryLow_priority()
{
//Setup
var mocker = new AutoMoqer(MockBehavior.Strict);
//Act
var result = mocker.Resolve<ProwlProvider>().SendNotification("NzbDrone Test", "This is a test message from NzbDrone (VeryLow)", _apiKey, NotificationPriority.VeryLow);
//Assert
result.Should().BeTrue();
}
[Test]
public void SendNotification_should_have_a_call_back_url()
{
//Setup
var mocker = new AutoMoqer(MockBehavior.Strict);
//Act
var result = mocker.Resolve<ProwlProvider>().SendNotification("NzbDrone Test", "This is a test message from NzbDrone", _apiKey, NotificationPriority.Normal, "http://www.nzbdrone.com");
//Assert
result.Should().BeTrue();
}
[Test]
public void SendNotification_should_return_true_for_two_valid_apiKey()
{
//Setup
var mocker = new AutoMoqer(MockBehavior.Strict);
//Act
var result = mocker.Resolve<ProwlProvider>().SendNotification("NzbDrone Test", "This is a test message from NzbDrone", _apiKey + ", " + _apiKey2);
//Assert
result.Should().BeTrue();
}
[Test]
public void SendNotification_should_return_true_for_valid_apiKey_with_bad_apiKey()
{
//Setup
var mocker = new AutoMoqer(MockBehavior.Strict);
//Act
var result = mocker.Resolve<ProwlProvider>().SendNotification("NzbDrone Test", "This is a test message from NzbDrone", _apiKey + ", " + _badApiKey);
//Assert
result.Should().BeTrue();
}
}
}

@ -160,6 +160,9 @@
<Reference Include="NLog, Version=2.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> <Reference Include="NLog, Version=2.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.2.0.0.2000\lib\net40\NLog.dll</HintPath> <HintPath>..\packages\NLog.2.0.0.2000\lib\net40\NLog.dll</HintPath>
</Reference> </Reference>
<Reference Include="Prowlin">
<HintPath>..\packages\Prowlin 0.9.4163.39219\Prowlin.dll</HintPath>
</Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.ComponentModel.Composition" /> <Reference Include="System.ComponentModel.Composition" />
<Reference Include="System.ComponentModel.DataAnnotations" /> <Reference Include="System.ComponentModel.DataAnnotations" />
@ -225,6 +228,8 @@
<Compile Include="Model\Xbmc\IconType.cs" /> <Compile Include="Model\Xbmc\IconType.cs" />
<Compile Include="Providers\Converting\AtomicParsleyProvider.cs" /> <Compile Include="Providers\Converting\AtomicParsleyProvider.cs" />
<Compile Include="Providers\Converting\HandbrakeProvider.cs" /> <Compile Include="Providers\Converting\HandbrakeProvider.cs" />
<Compile Include="Providers\ExternalNotification\Prowl.cs" />
<Compile Include="Providers\ProwlProvider.cs" />
<Compile Include="Providers\Core\ConfigFileProvider.cs" /> <Compile Include="Providers\Core\ConfigFileProvider.cs" />
<Compile Include="Providers\Core\UdpProvider.cs" /> <Compile Include="Providers\Core\UdpProvider.cs" />
<Compile Include="Providers\ExternalNotification\Growl.cs" /> <Compile Include="Providers\ExternalNotification\Growl.cs" />

@ -13,7 +13,6 @@ namespace NzbDrone.Core.Providers.Core
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly IDatabase _database; private readonly IDatabase _database;
[Inject] [Inject]
public ConfigProvider(IDatabase database) public ConfigProvider(IDatabase database)
{ {
@ -364,6 +363,32 @@ namespace NzbDrone.Core.Providers.Core
set { SetValue("GrowlPassword", value); } set { SetValue("GrowlPassword", value); }
} }
public virtual Boolean ProwlNotifyOnGrab
{
get { return GetValueBoolean("ProwlNotifyOnGrab"); }
set { SetValue("ProwlNotifyOnGrab", value); }
}
public virtual Boolean ProwlNotifyOnDownload
{
get { return GetValueBoolean("ProwlNotifyOnDownload"); }
set { SetValue("ProwlNotifyOnDownload", value); }
}
public virtual string ProwlApiKeys
{
get { return GetValue("ProwlApiKeys", String.Empty); }
set { SetValue("ProwlApiKeys", value); }
}
public virtual int ProwlPriority
{
get { return GetValueInt("ProwlPriority", 0); }
set { SetValue("ProwlPriority", value); }
}
private string GetValue(string key) private string GetValue(string key)
{ {
return GetValue(key, String.Empty); return GetValue(key, String.Empty);

@ -0,0 +1,77 @@
using System;
using NLog;
using NzbDrone.Core.Providers.Core;
using NzbDrone.Core.Repository;
using Prowlin;
namespace NzbDrone.Core.Providers.ExternalNotification
{
public class Prowl : ExternalNotificationBase
{
private readonly ProwlProvider _prowlProvider;
private readonly Logger Logger = LogManager.GetCurrentClassLogger();
public Prowl(ConfigProvider configProvider, ProwlProvider prowlProvider)
: base(configProvider)
{
_prowlProvider = prowlProvider;
}
public override string Name
{
get { return "Prowl"; }
}
public override void OnGrab(string message)
{
try
{
if(_configProvider.GrowlNotifyOnGrab)
{
_logger.Trace("Sending Notification to Prowl");
const string title = "Episode Grabbed";
var apiKeys = _configProvider.ProwlApiKeys;
var priority = _configProvider.ProwlPriority;
_prowlProvider.SendNotification(title, message, apiKeys, (NotificationPriority)priority);
}
}
catch (Exception ex)
{
Logger.WarnException(ex.Message, ex);
throw;
}
}
public override void OnDownload(string message, Series series)
{
try
{
if (_configProvider.GrowlNotifyOnDownload)
{
_logger.Trace("Sending Notification to Prowl");
const string title = "Episode Downloaded";
var apiKeys = _configProvider.ProwlApiKeys;
var priority = _configProvider.ProwlPriority;
_prowlProvider.SendNotification(title, message, apiKeys, (NotificationPriority)priority);
}
}
catch (Exception ex)
{
Logger.WarnException(ex.Message, ex);
throw;
}
}
public override void OnRename(string message, Series series)
{
}
}
}

@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NLog;
using Prowlin;
namespace NzbDrone.Core.Providers
{
public class ProwlProvider
{
private readonly Logger Logger = LogManager.GetCurrentClassLogger();
public ProwlProvider()
{
}
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 apiKeys, NotificationPriority priority = NotificationPriority.Normal, string url = null)
{
try
{
var notification = new Notification
{
Application = "NzbDrone",
Description = message,
Event = title,
Priority = priority,
Url = url
};
foreach (var apiKey in apiKeys.Split(','))
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(s): {0}", apiKeys);
}
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);
}
}
}

@ -102,7 +102,7 @@ namespace NzbDrone.Web.Controllers
SabApiKey = _configProvider.SabApiKey, SabApiKey = _configProvider.SabApiKey,
SabUsername = _configProvider.SabUsername, SabUsername = _configProvider.SabUsername,
SabPassword = _configProvider.SabPassword, SabPassword = _configProvider.SabPassword,
SabTvCategory = _configProvider.SabTvCategory, SabTvCategory = tvCategory,
SabTvPriority = _configProvider.SabTvPriority, SabTvPriority = _configProvider.SabTvPriority,
SabDropDirectory = _configProvider.SabDropDirectory, SabDropDirectory = _configProvider.SabDropDirectory,
SabTvCategorySelectList = tvCategorySelectList SabTvCategorySelectList = tvCategorySelectList
@ -178,7 +178,13 @@ namespace NzbDrone.Web.Controllers
GrowlNotifyOnGrab = _configProvider.GrowlNotifyOnGrab, GrowlNotifyOnGrab = _configProvider.GrowlNotifyOnGrab,
GrowlNotifyOnDownload = _configProvider.GrowlNotifyOnDownload, GrowlNotifyOnDownload = _configProvider.GrowlNotifyOnDownload,
GrowlHost = _configProvider.GrowlHost, GrowlHost = _configProvider.GrowlHost,
GrowlPassword = _configProvider.GrowlPassword GrowlPassword = _configProvider.GrowlPassword,
ProwlEnabled = _externalNotificationProvider.GetSettings(typeof(Prowl)).Enable,
ProwlNotifyOnGrab = _configProvider.ProwlNotifyOnGrab,
ProwlNotifyOnDownload = _configProvider.ProwlNotifyOnDownload,
ProwlApiKeys = _configProvider.ProwlApiKeys,
ProwlPriority = _configProvider.ProwlPriority,
ProwlPrioritySelectList = GetProwlPrioritySelectList()
}; };
return View(model); return View(model);
@ -535,5 +541,17 @@ namespace NzbDrone.Web.Controllers
{ {
return Json(new NotificationResult() { Title = "Unable to save setting", Text = "Invalid post data", NotificationType = NotificationType.Error }); return Json(new NotificationResult() { Title = "Unable to save setting", Text = "Invalid post data", NotificationType = NotificationType.Error });
} }
private SelectList GetProwlPrioritySelectList()
{
var list = new List<ProwlPrioritySelectListModel>();
list.Add(new ProwlPrioritySelectListModel{ Name = "Very Low", Value = -2 });
list.Add(new ProwlPrioritySelectListModel { Name = "Moderate", Value = -1 });
list.Add(new ProwlPrioritySelectListModel { Name = "Normal", Value = 0 });
list.Add(new ProwlPrioritySelectListModel { Name = "High", Value = 1 });
list.Add(new ProwlPrioritySelectListModel { Name = "Emergency", Value = 2 });
return new SelectList(list, "Value", "Name");
}
} }
} }

@ -1,5 +1,6 @@
using System.ComponentModel; using System.ComponentModel;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace NzbDrone.Web.Models namespace NzbDrone.Web.Models
{ {
@ -131,5 +132,29 @@ namespace NzbDrone.Web.Models
[DisplayName("Growl host Password")] [DisplayName("Growl host Password")]
[Description("Password is required if Growl is running on another system")] [Description("Password is required if Growl is running on another system")]
public string GrowlPassword { get; set; } public string GrowlPassword { get; set; }
//Prowl
[DisplayName("Enabled")]
[Description("Enable notifications for Prowl?")]
public bool ProwlEnabled { get; set; }
[DisplayName("Notify on Grab")]
[Description("Send notification when episode is sent to SABnzbd?")]
public bool ProwlNotifyOnGrab { get; set; }
[DisplayName("Notify on Download")]
[Description("Send notification when episode is downloaded?")]
public bool ProwlNotifyOnDownload { get; set; }
[DisplayName("API Keys")]
[Description("Comma-Separated list of API Keys")]
public string ProwlApiKeys { get; set; }
[DisplayName("Priority")]
[Description("Priority to send alerts to Prowl with")]
public int ProwlPriority { get; set; }
public SelectList ProwlPrioritySelectList { get; set; }
} }
} }

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace NzbDrone.Web.Models
{
public class ProwlPrioritySelectListModel
{
public string Name { get; set; }
public int Value { get; set; }
}
}

@ -491,6 +491,7 @@
<Compile Include="Models\JobQueueItemModel.cs" /> <Compile Include="Models\JobQueueItemModel.cs" />
<Compile Include="Models\NotificationResult.cs" /> <Compile Include="Models\NotificationResult.cs" />
<Compile Include="Models\PendingProcessingModel.cs" /> <Compile Include="Models\PendingProcessingModel.cs" />
<Compile Include="Models\ProwlPrioritySelectListModel.cs" />
<Compile Include="Models\QualityTypeModel.cs" /> <Compile Include="Models\QualityTypeModel.cs" />
<Compile Include="Models\RootDirModel.cs" /> <Compile Include="Models\RootDirModel.cs" />
<Compile Include="Models\SabnzbdSettingsModel.cs" /> <Compile Include="Models\SabnzbdSettingsModel.cs" />
@ -942,6 +943,9 @@
<ItemGroup> <ItemGroup>
<Content Include="Views\Settings\Growl.cshtml" /> <Content Include="Views\Settings\Growl.cshtml" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Include="Views\Settings\Prowl.cshtml" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" /> <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.

@ -5,7 +5,7 @@
Layout = null; Layout = null;
} }
<div id="growl" class="notifier clearfix"> <div class="notifier clearfix">
<label class="labelClass">@Html.LabelFor(m => m.GrowlEnabled) <label class="labelClass">@Html.LabelFor(m => m.GrowlEnabled)
<span class="small">@Html.DescriptionFor(m => m.GrowlEnabled)</span> <span class="small">@Html.DescriptionFor(m => m.GrowlEnabled)</span>
</label> </label>

@ -58,6 +58,7 @@
<li><a href="#tabs-smtp">SMTP</a></li> <li><a href="#tabs-smtp">SMTP</a></li>
<li><a href="#tabs-twitter">Twitter</a></li> <li><a href="#tabs-twitter">Twitter</a></li>
<li><a href="#tabs-growl">Growl</a></li> <li><a href="#tabs-growl">Growl</a></li>
<li><a href="#tabs-growl">Prowl</a></li>
</ul> </ul>
<div id="tabs-xbmc"> <div id="tabs-xbmc">
@{Html.RenderPartial("Xbmc", Model);} @{Html.RenderPartial("Xbmc", Model);}
@ -71,6 +72,9 @@
<div id="tabs-growl"> <div id="tabs-growl">
@{Html.RenderPartial("Growl", Model);} @{Html.RenderPartial("Growl", Model);}
</div> </div>
<div id="tabs-prowl">
@{Html.RenderPartial("Prowl", Model);}
</div>
</div> </div>
<button type="submit" id="save_button" disabled="disabled">Save</button> <button type="submit" id="save_button" disabled="disabled">Save</button>

@ -0,0 +1,33 @@
@using NzbDrone.Web.Helpers
@model NzbDrone.Web.Models.NotificationSettingsModel
@{
Layout = null;
}
<div class="notifier clearfix">
<label class="labelClass">@Html.LabelFor(m => m.ProwlEnabled)
<span class="small">@Html.DescriptionFor(m => m.ProwlEnabled)</span>
</label>
@Html.CheckBoxFor(m => m.ProwlEnabled, new { @class = "inputClass checkClass" })
<label class="labelClass">@Html.LabelFor(m => m.ProwlNotifyOnGrab)
<span class="small">@Html.DescriptionFor(m => m.ProwlNotifyOnGrab)</span>
</label>
@Html.CheckBoxFor(m => m.ProwlNotifyOnGrab, new { @class = "inputClass checkClass" })
<label class="labelClass">@Html.LabelFor(m => m.ProwlNotifyOnDownload)
<span class="small">@Html.DescriptionFor(m => m.ProwlNotifyOnDownload)</span>
</label>
@Html.CheckBoxFor(m => m.ProwlNotifyOnDownload, new { @class = "inputClass checkClass" })
<label class="labelClass">@Html.LabelFor(m => m.ProwlApiKeys)
<span class="small">@Html.DescriptionFor(m => m.ProwlApiKeys)</span>
</label>
@Html.TextBoxFor(m => m.ProwlApiKeys, new { @class = "inputClass" })
<label class="labelClass">@Html.LabelFor(m => m.ProwlPriority)
<span class="small">@Html.DescriptionFor(m => m.ProwlPriority)</span>
</label>
@Html.DropDownListFor(m => m.ProwlPriority, Model.ProwlPrioritySelectList, new { @class = "inputClass selectClass" })
</div>

@ -5,7 +5,7 @@
Layout = null; Layout = null;
} }
<div id="smtp" class="notifier clearfix"> <div class="notifier clearfix">
<label class="labelClass">@Html.LabelFor(m => m.SmtpEnabled) <label class="labelClass">@Html.LabelFor(m => m.SmtpEnabled)
<span class="small">@Html.DescriptionFor(m => m.SmtpEnabled)</span> <span class="small">@Html.DescriptionFor(m => m.SmtpEnabled)</span>
</label> </label>

@ -5,7 +5,7 @@
Layout = null; Layout = null;
} }
<div id="twitter" class="notifier clearfix"> <div class="notifier clearfix">
<label class="labelClass">@Html.LabelFor(m => m.TwitterEnabled) <label class="labelClass">@Html.LabelFor(m => m.TwitterEnabled)
<span class="small">@Html.DescriptionFor(m => m.TwitterEnabled)</span> <span class="small">@Html.DescriptionFor(m => m.TwitterEnabled)</span>
</label> </label>

@ -5,7 +5,7 @@
Layout = null; Layout = null;
} }
<div id="xbmc" class="notifier clearfix"> <div class="notifier clearfix">
<label class="labelClass">@Html.LabelFor(m => m.XbmcEnabled) <label class="labelClass">@Html.LabelFor(m => m.XbmcEnabled)
<span class="small">@Html.DescriptionFor(m => m.XbmcEnabled)</span> <span class="small">@Html.DescriptionFor(m => m.XbmcEnabled)</span>
</label> </label>

Binary file not shown.
Loading…
Cancel
Save