From 57fdbe6e08ddfb14c6fa3910c3884b05fa600c12 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 20 Sep 2013 00:31:02 -0700 Subject: [PATCH 01/34] Added API key authentication --- .../Authentication/EnableBasicAuthInNancy.cs | 5 +- .../EnableStatelessAuthInNancy.cs | 50 +++++++++++++++++++ .../Frontend/Mappers/IndexHtmlMapper.cs | 25 +++++++++- .../Mappers/StaticResourceMapperBase.cs | 3 -- NzbDrone.Api/NancyBootstrapper.cs | 1 + NzbDrone.Api/NzbDrone.Api.csproj | 1 + .../Configuration/ConfigFileProvider.cs | 9 ++++ .../{backbone.ajax.js => jquery.ajax.js} | 8 ++- UI/ServerStatus.js | 6 ++- UI/app.js | 15 +++--- UI/index.html | 6 +++ 11 files changed, 114 insertions(+), 15 deletions(-) create mode 100644 NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs rename UI/Mixins/{backbone.ajax.js => jquery.ajax.js} (78%) diff --git a/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs b/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs index 48ae43b2a..ab0bd9f60 100644 --- a/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs +++ b/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs @@ -27,7 +27,10 @@ namespace NzbDrone.Api.Authentication private Response RequiresAuthentication(NancyContext context) { Response response = null; - if (context.CurrentUser == null && _authenticationService.Enabled) + + if (!context.Request.Path.StartsWith("/api/") && + context.CurrentUser == null && + _authenticationService.Enabled) { response = new Response { StatusCode = HttpStatusCode.Unauthorized }; } diff --git a/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs b/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs new file mode 100644 index 000000000..4ad2e2995 --- /dev/null +++ b/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs @@ -0,0 +1,50 @@ +using System.Linq; +using Nancy; +using Nancy.Bootstrapper; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Api.Authentication +{ + public interface IEnableStatelessAuthInNancy + { + void Register(IPipelines pipelines); + } + + public class EnableStatelessAuthInNancy : IEnableStatelessAuthInNancy + { + private readonly IConfigFileProvider _configFileProvider; + + public EnableStatelessAuthInNancy(IConfigFileProvider configFileProvider) + { + _configFileProvider = configFileProvider; + } + + public void Register(IPipelines pipelines) + { + pipelines.BeforeRequest.AddItemToEndOfPipeline(ValidateApiKey); + } + + public Response ValidateApiKey(NancyContext context) + { + Response response = null; + var apiKey = context.Request.Headers["ApiKey"].FirstOrDefault(); + + if (!RuntimeInfo.IsProduction && + (context.Request.UserHostAddress.Equals("localhost") || + context.Request.UserHostAddress.Equals("127.0.0.1") || + context.Request.UserHostAddress.Equals("::1"))) + { + return response; + } + + if (context.Request.Path.StartsWith("/api/") && + (apiKey == null || !apiKey.Equals(_configFileProvider.ApiKey))) + { + response = new Response { StatusCode = HttpStatusCode.Unauthorized }; + } + + return response; + } + } +} \ No newline at end of file diff --git a/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs b/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs index b15167619..51f263a8f 100644 --- a/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs +++ b/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs @@ -1,20 +1,28 @@ +using System; using System.IO; using Nancy; +using Nancy.Responses; using NLog; using NzbDrone.Common; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; namespace NzbDrone.Api.Frontend.Mappers { public class IndexHtmlMapper : StaticResourceMapperBase { private readonly IDiskProvider _diskProvider; + private readonly IConfigFileProvider _configFileProvider; private readonly string _indexPath; - public IndexHtmlMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, Logger logger) + public IndexHtmlMapper(IAppFolderInfo appFolderInfo, + IDiskProvider diskProvider, + IConfigFileProvider configFileProvider, + Logger logger) : base(diskProvider, logger) { _diskProvider = diskProvider; + _configFileProvider = configFileProvider; _indexPath = Path.Combine(appFolderInfo.StartUpFolder, "UI", "index.html"); } @@ -30,7 +38,21 @@ namespace NzbDrone.Api.Frontend.Mappers public override Response GetResponse(string resourceUrl) { + string content; var response = base.GetResponse(resourceUrl); + var stream = new MemoryStream(); + + response.Contents.Invoke(stream); + stream.Position = 0; + + using (var reader = new StreamReader(stream)) + { + content = reader.ReadToEnd(); + } + + content = content.Replace("API_KEY", _configFileProvider.ApiKey); + + response = new StreamResponse(() => StringToStream(content), response.ContentType); response.Headers["X-UA-Compatible"] = "IE=edge"; return response; @@ -51,6 +73,5 @@ namespace NzbDrone.Api.Frontend.Mappers return text; } - } } \ No newline at end of file diff --git a/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs b/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs index eae69b9de..4cc42f49f 100644 --- a/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs +++ b/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs @@ -24,13 +24,10 @@ namespace NzbDrone.Api.Frontend.Mappers { _caseSensitive = true; } - - } protected abstract string Map(string resourceUrl); - public abstract bool CanHandle(string resourceUrl); public virtual Response GetResponse(string resourceUrl) diff --git a/NzbDrone.Api/NancyBootstrapper.cs b/NzbDrone.Api/NancyBootstrapper.cs index 29535d272..e9464ebd5 100644 --- a/NzbDrone.Api/NancyBootstrapper.cs +++ b/NzbDrone.Api/NancyBootstrapper.cs @@ -31,6 +31,7 @@ namespace NzbDrone.Api container.Resolve().Register(); container.Resolve().Register(pipelines); + container.Resolve().Register(pipelines); container.Resolve().PublishEvent(new ApplicationStartedEvent()); ApplicationPipelines.OnError.AddItemToEndOfPipeline(container.Resolve().HandleException); diff --git a/NzbDrone.Api/NzbDrone.Api.csproj b/NzbDrone.Api/NzbDrone.Api.csproj index 331591808..257e03887 100644 --- a/NzbDrone.Api/NzbDrone.Api.csproj +++ b/NzbDrone.Api/NzbDrone.Api.csproj @@ -74,6 +74,7 @@ Properties\SharedAssemblyInfo.cs + diff --git a/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/NzbDrone.Core/Configuration/ConfigFileProvider.cs index a25e85b7c..da0fa5569 100644 --- a/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -26,6 +26,7 @@ namespace NzbDrone.Core.Configuration string Password { get; } string LogLevel { get; } string Branch { get; } + string ApiKey { get; } bool Torrent { get; } } @@ -95,6 +96,14 @@ namespace NzbDrone.Core.Configuration get { return GetValueBoolean("LaunchBrowser", true); } } + public string ApiKey + { + get + { + return GetValue("ApiKey", Guid.NewGuid().ToString().Replace("-", "")); + } + } + public bool Torrent { get { return GetValueBoolean("Torrent", false, persist: false); } diff --git a/UI/Mixins/backbone.ajax.js b/UI/Mixins/jquery.ajax.js similarity index 78% rename from UI/Mixins/backbone.ajax.js rename to UI/Mixins/jquery.ajax.js index 0544b2e28..0f7abf8d5 100644 --- a/UI/Mixins/backbone.ajax.js +++ b/UI/Mixins/jquery.ajax.js @@ -20,9 +20,15 @@ define(function () { delete xhr.data; } + if (xhr) { + if (!xhr.headers) { + xhr.headers = {}; + } + + xhr.headers["ApiKey"] = window.NzbDrone.ApiKey; + } return original.apply(this, arguments); }; }; - }); diff --git a/UI/ServerStatus.js b/UI/ServerStatus.js index 1a5da687b..020713f5b 100644 --- a/UI/ServerStatus.js +++ b/UI/ServerStatus.js @@ -1,10 +1,12 @@ -window.NzbDrone = {}; window.NzbDrone.ApiRoot = '/api'; var statusText = $.ajax({ type : 'GET', url : window.NzbDrone.ApiRoot + '/system/status', - async: false + async: false, + headers: { + ApiKey: window.NzbDrone.ApiKey + } }).responseText; window.NzbDrone.ServerStatus = JSON.parse(statusText); diff --git a/UI/app.js b/UI/app.js index 280a16d39..8f64f4790 100644 --- a/UI/app.js +++ b/UI/app.js @@ -33,12 +33,19 @@ require.config({ $: { exports: '$', - init: function () { + deps : + [ + 'Mixins/jquery.ajax' + ], + + init: function (AjaxMixin) { require( [ 'jQuery/ToTheTop', 'Instrumentation/ErrorHandler' ]); + + AjaxMixin.apply($); } }, @@ -75,14 +82,10 @@ require.config({ backbone: { deps : [ - 'Mixins/backbone.ajax', 'underscore', '$' ], - exports: 'Backbone', - init : function (AjaxMixin) { - AjaxMixin.apply(Backbone); - } + exports: 'Backbone' }, diff --git a/UI/index.html b/UI/index.html index 178c5beb2..a5aa6ba57 100644 --- a/UI/index.html +++ b/UI/index.html @@ -60,6 +60,12 @@ + + + From de607e207b8c9baed0ba9769d6b87ce8a39bfded Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 20 Sep 2013 15:19:48 -0700 Subject: [PATCH 02/34] ApiKey Authentication cleanup --- .../Authentication/EnableBasicAuthInNancy.cs | 11 +++----- .../EnableStatelessAuthInNancy.cs | 26 ++++++++--------- NzbDrone.Api/Extensions/RequestExtensions.cs | 28 +++++++++++++++++++ .../Frontend/Mappers/IndexHtmlMapper.cs | 15 +--------- NzbDrone.Api/NancyBootstrapper.cs | 2 -- UI/Mixins/jquery.ajax.js | 7 ++--- UI/ServerStatus.js | 2 +- UI/index.html | 2 +- 8 files changed, 48 insertions(+), 45 deletions(-) create mode 100644 NzbDrone.Api/Extensions/RequestExtensions.cs diff --git a/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs b/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs index ab0bd9f60..a6994caf3 100644 --- a/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs +++ b/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs @@ -1,15 +1,12 @@ using Nancy; using Nancy.Authentication.Basic; using Nancy.Bootstrapper; +using NzbDrone.Api.Extensions; +using NzbDrone.Api.Extensions.Pipelines; namespace NzbDrone.Api.Authentication { - public interface IEnableBasicAuthInNancy - { - void Register(IPipelines pipelines); - } - - public class EnableBasicAuthInNancy : IEnableBasicAuthInNancy + public class EnableBasicAuthInNancy : IRegisterNancyPipeline { private readonly IAuthenticationService _authenticationService; @@ -28,7 +25,7 @@ namespace NzbDrone.Api.Authentication { Response response = null; - if (!context.Request.Path.StartsWith("/api/") && + if (!context.Request.IsApiRequest() && context.CurrentUser == null && _authenticationService.Enabled) { diff --git a/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs b/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs index 4ad2e2995..68d737387 100644 --- a/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs +++ b/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs @@ -1,17 +1,15 @@ -using System.Linq; +using System; +using System.Linq; using Nancy; using Nancy.Bootstrapper; +using NzbDrone.Api.Extensions; +using NzbDrone.Api.Extensions.Pipelines; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; namespace NzbDrone.Api.Authentication { - public interface IEnableStatelessAuthInNancy - { - void Register(IPipelines pipelines); - } - - public class EnableStatelessAuthInNancy : IEnableStatelessAuthInNancy + public class EnableStatelessAuthInNancy : IRegisterNancyPipeline { private readonly IConfigFileProvider _configFileProvider; @@ -28,18 +26,16 @@ namespace NzbDrone.Api.Authentication public Response ValidateApiKey(NancyContext context) { Response response = null; - var apiKey = context.Request.Headers["ApiKey"].FirstOrDefault(); - - if (!RuntimeInfo.IsProduction && - (context.Request.UserHostAddress.Equals("localhost") || - context.Request.UserHostAddress.Equals("127.0.0.1") || - context.Request.UserHostAddress.Equals("::1"))) + + if (!RuntimeInfo.IsProduction && context.Request.IsLocalRequest()) { return response; } + + var apiKey = context.Request.Headers.Authorization; - if (context.Request.Path.StartsWith("/api/") && - (apiKey == null || !apiKey.Equals(_configFileProvider.ApiKey))) + if (context.Request.IsApiRequest() && + (String.IsNullOrWhiteSpace(apiKey) || !apiKey.Equals(_configFileProvider.ApiKey))) { response = new Response { StatusCode = HttpStatusCode.Unauthorized }; } diff --git a/NzbDrone.Api/Extensions/RequestExtensions.cs b/NzbDrone.Api/Extensions/RequestExtensions.cs new file mode 100644 index 000000000..3d0329522 --- /dev/null +++ b/NzbDrone.Api/Extensions/RequestExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nancy; + +namespace NzbDrone.Api.Extensions +{ + public static class RequestExtensions + { + public static bool IsApiRequest(this Request request) + { + return request.Path.StartsWith("/api/", StringComparison.InvariantCultureIgnoreCase); + } + + public static bool IsSignalRRequest(this Request request) + { + return request.Path.StartsWith("/signalr/", StringComparison.InvariantCultureIgnoreCase); + } + + public static bool IsLocalRequest(this Request request) + { + return (request.UserHostAddress.Equals("localhost") || + request.UserHostAddress.Equals("127.0.0.1") || + request.UserHostAddress.Equals("::1")); + } + } +} diff --git a/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs b/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs index 51f263a8f..ae950aae0 100644 --- a/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs +++ b/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs @@ -38,21 +38,7 @@ namespace NzbDrone.Api.Frontend.Mappers public override Response GetResponse(string resourceUrl) { - string content; var response = base.GetResponse(resourceUrl); - var stream = new MemoryStream(); - - response.Contents.Invoke(stream); - stream.Position = 0; - - using (var reader = new StreamReader(stream)) - { - content = reader.ReadToEnd(); - } - - content = content.Replace("API_KEY", _configFileProvider.ApiKey); - - response = new StreamResponse(() => StringToStream(content), response.ContentType); response.Headers["X-UA-Compatible"] = "IE=edge"; return response; @@ -70,6 +56,7 @@ namespace NzbDrone.Api.Frontend.Mappers text = text.Replace(".css", ".css?v=" + BuildInfo.Version); text = text.Replace(".js", ".js?v=" + BuildInfo.Version); + text = text.Replace("API_KEY", _configFileProvider.ApiKey); return text; } diff --git a/NzbDrone.Api/NancyBootstrapper.cs b/NzbDrone.Api/NancyBootstrapper.cs index e9464ebd5..bee581921 100644 --- a/NzbDrone.Api/NancyBootstrapper.cs +++ b/NzbDrone.Api/NancyBootstrapper.cs @@ -30,8 +30,6 @@ namespace NzbDrone.Api RegisterPipelines(pipelines); container.Resolve().Register(); - container.Resolve().Register(pipelines); - container.Resolve().Register(pipelines); container.Resolve().PublishEvent(new ApplicationStartedEvent()); ApplicationPipelines.OnError.AddItemToEndOfPipeline(container.Resolve().HandleException); diff --git a/UI/Mixins/jquery.ajax.js b/UI/Mixins/jquery.ajax.js index 0f7abf8d5..e05a7d8fb 100644 --- a/UI/Mixins/jquery.ajax.js +++ b/UI/Mixins/jquery.ajax.js @@ -21,11 +21,8 @@ define(function () { delete xhr.data; } if (xhr) { - if (!xhr.headers) { - xhr.headers = {}; - } - - xhr.headers["ApiKey"] = window.NzbDrone.ApiKey; + xhr.headers = xhr.headers || {}; + xhr.headers['Authorization'] = window.NzbDrone.ApiKey; } return original.apply(this, arguments); diff --git a/UI/ServerStatus.js b/UI/ServerStatus.js index 020713f5b..589d45fc6 100644 --- a/UI/ServerStatus.js +++ b/UI/ServerStatus.js @@ -5,7 +5,7 @@ var statusText = $.ajax({ url : window.NzbDrone.ApiRoot + '/system/status', async: false, headers: { - ApiKey: window.NzbDrone.ApiKey + Authorization: window.NzbDrone.ApiKey } }).responseText; diff --git a/UI/index.html b/UI/index.html index a5aa6ba57..c2b0897e7 100644 --- a/UI/index.html +++ b/UI/index.html @@ -62,7 +62,7 @@ From e19e5238248f0182bc64b1c8f52a18c3f3752878 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 20 Sep 2013 15:31:26 -0700 Subject: [PATCH 03/34] csproj fail :( --- NzbDrone.Api/NzbDrone.Api.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/NzbDrone.Api/NzbDrone.Api.csproj b/NzbDrone.Api/NzbDrone.Api.csproj index 257e03887..b88c2277d 100644 --- a/NzbDrone.Api/NzbDrone.Api.csproj +++ b/NzbDrone.Api/NzbDrone.Api.csproj @@ -98,6 +98,7 @@ + From d19a755fb129e895aef5f45e77daac88ea616d00 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 20 Sep 2013 19:07:42 -0700 Subject: [PATCH 04/34] Integration tests use the api key now --- .../Configuration/ConfigFileProvider.cs | 4 ++- .../Client/ClientBase.cs | 13 ++++++--- .../Client/EpisodeClient.cs | 4 +-- .../Client/IndexerClient.cs | 7 ++--- .../Client/ReleaseClient.cs | 7 ++--- .../Client/SeriesClient.cs | 11 +++---- NzbDrone.Integration.Test/IntegrationTest.cs | 19 ++++++------ NzbDrone.Integration.Test/NzbDroneRunner.cs | 29 +++++++++++++++---- 8 files changed, 54 insertions(+), 40 deletions(-) diff --git a/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/NzbDrone.Core/Configuration/ConfigFileProvider.cs index da0fa5569..f2b970763 100644 --- a/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -32,7 +32,7 @@ namespace NzbDrone.Core.Configuration public class ConfigFileProvider : IConfigFileProvider { - private const string CONFIG_ELEMENT_NAME = "Config"; + public const string CONFIG_ELEMENT_NAME = "Config"; private readonly IEventAggregator _eventAggregator; private readonly ICached _cache; @@ -214,6 +214,8 @@ namespace NzbDrone.Core.Configuration var xDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes")); xDoc.Add(new XElement(CONFIG_ELEMENT_NAME)); xDoc.Save(_configFile); + + SaveConfigDictionary(GetConfigDictionary()); } } diff --git a/NzbDrone.Integration.Test/Client/ClientBase.cs b/NzbDrone.Integration.Test/Client/ClientBase.cs index 20e35117f..9cd0b4b7a 100644 --- a/NzbDrone.Integration.Test/Client/ClientBase.cs +++ b/NzbDrone.Integration.Test/Client/ClientBase.cs @@ -14,10 +14,10 @@ namespace NzbDrone.Integration.Test.Client { private readonly IRestClient _restClient; private readonly string _resource; - + private readonly string _apiKey; private readonly Logger _logger; - public ClientBase(IRestClient restClient, string resource = null) + public ClientBase(IRestClient restClient, string apiKey, string resource = null) { if (resource == null) { @@ -26,6 +26,7 @@ namespace NzbDrone.Integration.Test.Client _restClient = restClient; _resource = resource; + _apiKey = apiKey; _logger = LogManager.GetLogger("REST"); } @@ -88,10 +89,14 @@ namespace NzbDrone.Integration.Test.Client public RestRequest BuildRequest(string command = "") { - return new RestRequest(_resource + "/" + command.Trim('/')) + var request = new RestRequest(_resource + "/" + command.Trim('/')) { - RequestFormat = DataFormat.Json + RequestFormat = DataFormat.Json, }; + + request.AddHeader("Authorization", _apiKey); + + return request; } public T Get(IRestRequest request, HttpStatusCode statusCode = HttpStatusCode.OK) where T : class, new() diff --git a/NzbDrone.Integration.Test/Client/EpisodeClient.cs b/NzbDrone.Integration.Test/Client/EpisodeClient.cs index 8cc89af4e..437226c2f 100644 --- a/NzbDrone.Integration.Test/Client/EpisodeClient.cs +++ b/NzbDrone.Integration.Test/Client/EpisodeClient.cs @@ -6,8 +6,8 @@ namespace NzbDrone.Integration.Test.Client { public class EpisodeClient : ClientBase { - public EpisodeClient(IRestClient restClient) - : base(restClient, "episodes") + public EpisodeClient(IRestClient restClient, string apiKey) + : base(restClient, apiKey, "episodes") { } diff --git a/NzbDrone.Integration.Test/Client/IndexerClient.cs b/NzbDrone.Integration.Test/Client/IndexerClient.cs index 44f5d4c77..9d6f9b974 100644 --- a/NzbDrone.Integration.Test/Client/IndexerClient.cs +++ b/NzbDrone.Integration.Test/Client/IndexerClient.cs @@ -5,12 +5,9 @@ namespace NzbDrone.Integration.Test.Client { public class IndexerClient : ClientBase { - public IndexerClient(IRestClient restClient) - : base(restClient) + public IndexerClient(IRestClient restClient, string apiKey) + : base(restClient, apiKey) { } - - - } } \ No newline at end of file diff --git a/NzbDrone.Integration.Test/Client/ReleaseClient.cs b/NzbDrone.Integration.Test/Client/ReleaseClient.cs index fba274856..46a6db839 100644 --- a/NzbDrone.Integration.Test/Client/ReleaseClient.cs +++ b/NzbDrone.Integration.Test/Client/ReleaseClient.cs @@ -5,12 +5,9 @@ namespace NzbDrone.Integration.Test.Client { public class ReleaseClient : ClientBase { - public ReleaseClient(IRestClient restClient) - : base(restClient) + public ReleaseClient(IRestClient restClient, string apiKey) + : base(restClient, apiKey) { } - - - } } diff --git a/NzbDrone.Integration.Test/Client/SeriesClient.cs b/NzbDrone.Integration.Test/Client/SeriesClient.cs index 3dc88c969..1f0a572f9 100644 --- a/NzbDrone.Integration.Test/Client/SeriesClient.cs +++ b/NzbDrone.Integration.Test/Client/SeriesClient.cs @@ -7,8 +7,8 @@ namespace NzbDrone.Integration.Test.Client { public class SeriesClient : ClientBase { - public SeriesClient(IRestClient restClient) - : base(restClient) + public SeriesClient(IRestClient restClient, string apiKey) + : base(restClient, apiKey) { } @@ -27,14 +27,11 @@ namespace NzbDrone.Integration.Test.Client } - public class SystemInfoClient : ClientBase { - public SystemInfoClient(IRestClient restClient) - : base(restClient) + public SystemInfoClient(IRestClient restClient, string apiKey) + : base(restClient, apiKey) { } - - } } diff --git a/NzbDrone.Integration.Test/IntegrationTest.cs b/NzbDrone.Integration.Test/IntegrationTest.cs index a0b5b3bcd..f8556a58d 100644 --- a/NzbDrone.Integration.Test/IntegrationTest.cs +++ b/NzbDrone.Integration.Test/IntegrationTest.cs @@ -47,22 +47,21 @@ namespace NzbDrone.Integration.Test _runner = new NzbDroneRunner(); _runner.KillAll(); - InitRestClients(); - _runner.Start(); + InitRestClients(); } private void InitRestClients() { RestClient = new RestClient("http://localhost:8989/api"); - Series = new SeriesClient(RestClient); - Releases = new ReleaseClient(RestClient); - RootFolders = new ClientBase(RestClient); - Commands = new ClientBase(RestClient); - History = new ClientBase(RestClient); - Indexers = new IndexerClient(RestClient); - Episodes = new EpisodeClient(RestClient); - NamingConfig = new ClientBase(RestClient, "config/naming"); + Series = new SeriesClient(RestClient, _runner.ApiKey); + Releases = new ReleaseClient(RestClient, _runner.ApiKey); + RootFolders = new ClientBase(RestClient, _runner.ApiKey); + Commands = new ClientBase(RestClient, _runner.ApiKey); + History = new ClientBase(RestClient, _runner.ApiKey); + Indexers = new IndexerClient(RestClient, _runner.ApiKey); + Episodes = new EpisodeClient(RestClient, _runner.ApiKey); + NamingConfig = new ClientBase(RestClient, _runner.ApiKey, "config/naming"); } //[TestFixtureTearDown] diff --git a/NzbDrone.Integration.Test/NzbDroneRunner.cs b/NzbDrone.Integration.Test/NzbDroneRunner.cs index 5955166ff..111ff02db 100644 --- a/NzbDrone.Integration.Test/NzbDroneRunner.cs +++ b/NzbDrone.Integration.Test/NzbDroneRunner.cs @@ -1,10 +1,13 @@ using System; using System.Diagnostics; using System.IO; +using System.Linq; using System.Threading; +using System.Xml.Linq; using NUnit.Framework; using NzbDrone.Common; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; using RestSharp; namespace NzbDrone.Integration.Test @@ -15,16 +18,18 @@ namespace NzbDrone.Integration.Test private readonly IRestClient _restClient; private Process _nzbDroneProcess; + public string AppData { get; private set; } + public string ApiKey { get; private set; } + public NzbDroneRunner(int port = 8989) { _processProvider = new ProcessProvider(); _restClient = new RestClient("http://localhost:8989/api"); } - public void Start() { - AppDate = Path.Combine(Directory.GetCurrentDirectory(), "_intg_" + DateTime.Now.Ticks); + AppData = Path.Combine(Directory.GetCurrentDirectory(), "_intg_" + DateTime.Now.Ticks); var nzbdroneConsoleExe = "NzbDrone.Console.exe"; @@ -33,7 +38,6 @@ namespace NzbDrone.Integration.Test nzbdroneConsoleExe = "NzbDrone.exe"; } - if (BuildInfo.IsDebug) { @@ -53,8 +57,12 @@ namespace NzbDrone.Integration.Test Assert.Fail("Process has exited"); } + SetApiKey(); + + var request = new RestRequest("system/status"); + request.AddHeader("Authorization", ApiKey); - var statusCall = _restClient.Get(new RestRequest("system/status")); + var statusCall = _restClient.Get(request); if (statusCall.ResponseStatus == ResponseStatus.Completed) { @@ -76,7 +84,7 @@ namespace NzbDrone.Integration.Test private void Start(string outputNzbdroneConsoleExe) { - var args = "-nobrowser -data=\"" + AppDate + "\""; + var args = "-nobrowser -data=\"" + AppData + "\""; _nzbDroneProcess = _processProvider.Start(outputNzbdroneConsoleExe, args, OnOutputDataReceived, OnOutputDataReceived); } @@ -91,7 +99,16 @@ namespace NzbDrone.Integration.Test } } + private void SetApiKey() + { + var configFile = Path.Combine(AppData, "config.xml"); + + if (!String.IsNullOrWhiteSpace(ApiKey)) return; + if (!File.Exists(configFile)) return; - public string AppDate { get; private set; } + var xDoc = XDocument.Load(configFile); + var config = xDoc.Descendants(ConfigFileProvider.CONFIG_ELEMENT_NAME).Single(); + ApiKey = config.Descendants("ApiKey").Single().Value; + } } } \ No newline at end of file From 1a2ae4bd2c200b233f6f4b646dd5aecdb80e9add Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 23 Sep 2013 12:03:33 -0700 Subject: [PATCH 05/34] pluck and findWhere, not map and find --- UI/Navbar/Search.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/UI/Navbar/Search.js b/UI/Navbar/Search.js index 35e0d733b..3391f9320 100644 --- a/UI/Navbar/Search.js +++ b/UI/Navbar/Search.js @@ -7,9 +7,7 @@ define( $.fn.bindSearch = function () { $(this).typeahead({ source : function () { - return SeriesCollection.map(function (model) { - return model.get('title'); - }); + return SeriesCollection.pluck('title'); }, sorter: function (items) { @@ -17,9 +15,7 @@ define( }, updater: function (item) { - var series = SeriesCollection.find(function (model) { - return model.get('title') === item; - }); + var series = SeriesCollection.findWhere({ title: item }); this.$element.blur(); App.Router.navigate('/series/{0}'.format(series.get('titleSlug')), { trigger: true }); From c5ae38638a23f547beaf7f8435245b60d570c3fb Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 23 Sep 2013 13:06:15 -0700 Subject: [PATCH 06/34] Show yellow dot and season not monitored when no episode files and season isn't monitored --- UI/Content/theme.less | 4 ++++ UI/Series/Details/SeasonLayoutTemplate.html | 16 ++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/UI/Content/theme.less b/UI/Content/theme.less index debaf887c..2eb6e6481 100644 --- a/UI/Content/theme.less +++ b/UI/Content/theme.less @@ -162,6 +162,10 @@ footer { color : @successText; } +.status-warning { + color : @warningText; +} + .status-danger { color : @errorText; } diff --git a/UI/Series/Details/SeasonLayoutTemplate.html b/UI/Series/Details/SeasonLayoutTemplate.html index 4b401c2e6..827671ab2 100644 --- a/UI/Series/Details/SeasonLayoutTemplate.html +++ b/UI/Series/Details/SeasonLayoutTemplate.html @@ -9,13 +9,17 @@ {{/if}} {{#if_eq episodeCount compare=0}} - - {{else}} - {{#if_eq percentOfEpisodes compare=100}} - + {{#if monitored}} + + {{else}} + + {{/if}} {{else}} - - {{/if_eq}} + {{#if_eq percentOfEpisodes compare=100}} + + {{else}} + + {{/if_eq}} {{/if_eq}} From 5841140c99d08ca156ac5a523b5268b01b5abc7e Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 23 Sep 2013 15:31:50 -0700 Subject: [PATCH 07/34] Allow Basic Auth on API --- .../Authentication/AuthenticationService.cs | 12 +++++++++- .../Authentication/EnableBasicAuthInNancy.cs | 4 +--- .../EnableStatelessAuthInNancy.cs | 23 +++++++++++++------ 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/NzbDrone.Api/Authentication/AuthenticationService.cs b/NzbDrone.Api/Authentication/AuthenticationService.cs index 17d7e5022..961dc22e4 100644 --- a/NzbDrone.Api/Authentication/AuthenticationService.cs +++ b/NzbDrone.Api/Authentication/AuthenticationService.cs @@ -1,4 +1,6 @@ -using Nancy.Authentication.Basic; +using System; +using Nancy; +using Nancy.Authentication.Basic; using Nancy.Security; using NzbDrone.Core.Configuration; @@ -7,6 +9,7 @@ namespace NzbDrone.Api.Authentication public interface IAuthenticationService : IUserValidator { bool Enabled { get; } + bool IsAuthenticated(NancyContext context); } public class AuthenticationService : IAuthenticationService @@ -44,5 +47,12 @@ namespace NzbDrone.Api.Authentication return _configFileProvider.AuthenticationEnabled; } } + + public bool IsAuthenticated(NancyContext context) + { + if (context.CurrentUser == null && _configFileProvider.AuthenticationEnabled) return false; + + return true; + } } } diff --git a/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs b/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs index a6994caf3..c5622eb75 100644 --- a/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs +++ b/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs @@ -25,9 +25,7 @@ namespace NzbDrone.Api.Authentication { Response response = null; - if (!context.Request.IsApiRequest() && - context.CurrentUser == null && - _authenticationService.Enabled) + if (!context.Request.IsApiRequest() && !_authenticationService.IsAuthenticated(context)) { response = new Response { StatusCode = HttpStatusCode.Unauthorized }; } diff --git a/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs b/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs index 68d737387..8896482b2 100644 --- a/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs +++ b/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs @@ -11,10 +11,12 @@ namespace NzbDrone.Api.Authentication { public class EnableStatelessAuthInNancy : IRegisterNancyPipeline { + private readonly IAuthenticationService _authenticationService; private readonly IConfigFileProvider _configFileProvider; - public EnableStatelessAuthInNancy(IConfigFileProvider configFileProvider) + public EnableStatelessAuthInNancy(IAuthenticationService authenticationService, IConfigFileProvider configFileProvider) { + _authenticationService = authenticationService; _configFileProvider = configFileProvider; } @@ -27,20 +29,27 @@ namespace NzbDrone.Api.Authentication { Response response = null; - if (!RuntimeInfo.IsProduction && context.Request.IsLocalRequest()) - { - return response; - } +// if (!RuntimeInfo.IsProduction && context.Request.IsLocalRequest()) +// { +// return response; +// } var apiKey = context.Request.Headers.Authorization; - if (context.Request.IsApiRequest() && - (String.IsNullOrWhiteSpace(apiKey) || !apiKey.Equals(_configFileProvider.ApiKey))) + if (context.Request.IsApiRequest() && !ValidApiKey(apiKey) && !_authenticationService.IsAuthenticated(context)) { response = new Response { StatusCode = HttpStatusCode.Unauthorized }; } return response; } + + private bool ValidApiKey(string apiKey) + { + if (String.IsNullOrWhiteSpace(apiKey)) return false; + if (!apiKey.Equals(_configFileProvider.ApiKey)) return false; + + return true; + } } } \ No newline at end of file From df967c1ed1b37e645ab39d9369e2685f431bf418 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 23 Sep 2013 15:56:01 -0700 Subject: [PATCH 08/34] Don't check for API key on local requests --- NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs b/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs index 8896482b2..bbdf22e85 100644 --- a/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs +++ b/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs @@ -29,10 +29,10 @@ namespace NzbDrone.Api.Authentication { Response response = null; -// if (!RuntimeInfo.IsProduction && context.Request.IsLocalRequest()) -// { -// return response; -// } + if (!RuntimeInfo.IsProduction && context.Request.IsLocalRequest()) + { + return response; + } var apiKey = context.Request.Headers.Authorization; From 154a6b39be035a7b9b73b202d3719245ab575d5e Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 23 Sep 2013 16:55:24 -0700 Subject: [PATCH 09/34] Use t as shortcut to series search navigator --- UI/app.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/UI/app.js b/UI/app.js index 8f64f4790..d726c0830 100644 --- a/UI/app.js +++ b/UI/app.js @@ -230,5 +230,16 @@ define( 'jQuery/TooltipBinder' ]); + $(document).on('keydown', function (e){ + if ($(e.target).is('input')) { + return; + } + + if (e.keyCode === 84) { + $('.x-series-search').focus(); + e.preventDefault(); + } + }); + return app; }); From f32cbbb224750b16c4b59d9a5f524c77c5270555 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 23 Sep 2013 17:53:56 -0700 Subject: [PATCH 10/34] Toggle monitored status from season pass as well as all seasons (skip specials) --- UI/AddSeries/SearchResultView.js | 3 +- UI/Controller.js | 2 +- UI/Instrumentation/ErrorHandler.js | 2 +- .../{Layout.js => SeasonPassLayout.js} | 2 +- ...ate.html => SeasonPassLayoutTemplate.html} | 0 UI/SeasonPass/SeriesLayout.js | 59 +++++++++++++++---- UI/SeasonPass/SeriesLayoutTemplate.html | 20 +++++-- UI/Series/Details/SeriesDetailsTemplate.html | 2 +- UI/Series/series.less | 13 ++++ 9 files changed, 81 insertions(+), 22 deletions(-) rename UI/SeasonPass/{Layout.js => SeasonPassLayout.js} (93%) rename UI/SeasonPass/{LayoutTemplate.html => SeasonPassLayoutTemplate.html} (100%) diff --git a/UI/AddSeries/SearchResultView.js b/UI/AddSeries/SearchResultView.js index e19bfecaf..3f8c6ec33 100644 --- a/UI/AddSeries/SearchResultView.js +++ b/UI/AddSeries/SearchResultView.js @@ -2,6 +2,7 @@ define( [ 'app', + 'underscore', 'marionette', 'Quality/QualityProfileCollection', 'AddSeries/RootFolders/Collection', @@ -11,7 +12,7 @@ define( 'Shared/Messenger', 'Mixins/AsValidatedView', 'jquery.dotdotdot' - ], function (App, Marionette, QualityProfiles, RootFolders, RootFolderLayout, SeriesCollection, Config, Messenger, AsValidatedView) { + ], function (App, _, Marionette, QualityProfiles, RootFolders, RootFolderLayout, SeriesCollection, Config, Messenger, AsValidatedView) { var view = Marionette.ItemView.extend({ diff --git a/UI/Controller.js b/UI/Controller.js index 190708195..8f0c059c8 100644 --- a/UI/Controller.js +++ b/UI/Controller.js @@ -15,7 +15,7 @@ define( 'Logs/Files/Layout', 'Release/Layout', 'System/Layout', - 'SeasonPass/Layout', + 'SeasonPass/SeasonPassLayout', 'Shared/NotFoundView', 'Shared/Modal/Region' ], function (App, Marionette, HistoryLayout, SettingsLayout, AddSeriesLayout, SeriesIndexLayout, SeriesDetailsLayout, SeriesCollection, MissingLayout, CalendarLayout, diff --git a/UI/Instrumentation/ErrorHandler.js b/UI/Instrumentation/ErrorHandler.js index 475edc68f..92ccd4db1 100644 --- a/UI/Instrumentation/ErrorHandler.js +++ b/UI/Instrumentation/ErrorHandler.js @@ -13,7 +13,7 @@ var filename = a.pathname.split('/').pop(); //Suppress Firefox debug errors when console window is closed - if (filename.toLowerCase() === 'markupview.jsm') { + if (filename.toLowerCase() === 'markupview.jsm' || filename.toLowerCase() === 'markup-view.js') { return false; } diff --git a/UI/SeasonPass/Layout.js b/UI/SeasonPass/SeasonPassLayout.js similarity index 93% rename from UI/SeasonPass/Layout.js rename to UI/SeasonPass/SeasonPassLayout.js index ebf2609cf..0a1e51d92 100644 --- a/UI/SeasonPass/Layout.js +++ b/UI/SeasonPass/SeasonPassLayout.js @@ -12,7 +12,7 @@ define( SeriesCollectionView, LoadingView) { return Marionette.Layout.extend({ - template: 'SeasonPass/LayoutTemplate', + template: 'SeasonPass/SeasonPassLayoutTemplate', regions: { series: '#x-series' diff --git a/UI/SeasonPass/LayoutTemplate.html b/UI/SeasonPass/SeasonPassLayoutTemplate.html similarity index 100% rename from UI/SeasonPass/LayoutTemplate.html rename to UI/SeasonPass/SeasonPassLayoutTemplate.html diff --git a/UI/SeasonPass/SeriesLayout.js b/UI/SeasonPass/SeriesLayout.js index 1eae0ce0e..34b26855a 100644 --- a/UI/SeasonPass/SeriesLayout.js +++ b/UI/SeasonPass/SeriesLayout.js @@ -1,24 +1,28 @@ 'use strict'; define( [ + 'underscore', 'marionette', 'backgrid', 'Series/SeasonCollection' - ], function (Marionette, Backgrid, SeasonCollection) { + ], function (_, Marionette, Backgrid, SeasonCollection) { return Marionette.Layout.extend({ template: 'SeasonPass/SeriesLayoutTemplate', ui: { - seasonSelect: '.x-season-select', - expander : '.x-expander', - seasonGrid : '.x-season-grid' + seasonSelect : '.x-season-select', + expander : '.x-expander', + seasonGrid : '.x-season-grid', + seriesMonitored: '.x-series-monitored' }, events: { - 'change .x-season-select': '_seasonSelected', - 'click .x-expander' : '_expand', - 'click .x-latest' : '_latest', - 'click .x-monitored' : '_toggleSeasonMonitored' + 'change .x-season-select' : '_seasonSelected', + 'click .x-expander' : '_expand', + 'click .x-latest' : '_latest', + 'click .x-all' : '_all', + 'click .x-monitored' : '_toggleSeasonMonitored', + 'click .x-series-monitored': '_toggleSeriesMonitored' }, regions: { @@ -26,6 +30,7 @@ define( }, initialize: function () { + this.listenTo(this.model, 'sync', this._setSeriesMonitoredState); this.seasonCollection = new SeasonCollection(this.model.get('seasons')); this.expanded = false; }, @@ -36,16 +41,17 @@ define( } this._setExpanderIcon(); + this._setSeriesMonitoredState(); }, _seasonSelected: function () { var seasonNumber = parseInt(this.ui.seasonSelect.val()); - if (seasonNumber == -1 || isNaN(seasonNumber)) { + if (seasonNumber === -1 || isNaN(seasonNumber)) { return; } - this._setMonitored(seasonNumber) + this._setSeasonMonitored(seasonNumber); }, _expand: function () { @@ -79,10 +85,16 @@ define( return s.seasonNumber; }); - this._setMonitored(season.seasonNumber); + this._setSeasonMonitored(season.seasonNumber); }, - _setMonitored: function (seasonNumber) { + _all: function () { + var minSeasonNotZero = _.min(_.reject(this.model.get('seasons'), { seasonNumber: 0 }), 'seasonNumber'); + + this._setSeasonMonitored(minSeasonNotZero.seasonNumber); + }, + + _setSeasonMonitored: function (seasonNumber) { var self = this; this.model.setSeasonPass(seasonNumber); @@ -118,6 +130,29 @@ define( _afterToggleSeasonMonitored: function () { this.render(); + }, + + _setSeriesMonitoredState: function () { + var monitored = this.model.get('monitored'); + + this.ui.seriesMonitored.removeAttr('data-idle-icon'); + + if (monitored) { + this.ui.seriesMonitored.addClass('icon-nd-monitored'); + this.ui.seriesMonitored.removeClass('icon-nd-unmonitored'); + } + else { + this.ui.seriesMonitored.addClass('icon-nd-unmonitored'); + this.ui.seriesMonitored.removeClass('icon-nd-monitored'); + } + }, + + _toggleSeriesMonitored: function (e) { + var savePromise = this.model.save('monitored', !this.model.get('monitored'), { + wait: true + }); + + this.ui.seriesMonitored.spinForPromise(savePromise); } }); }); diff --git a/UI/SeasonPass/SeriesLayoutTemplate.html b/UI/SeasonPass/SeriesLayoutTemplate.html index 9c12a4483..844307b51 100644 --- a/UI/SeasonPass/SeriesLayoutTemplate.html +++ b/UI/SeasonPass/SeriesLayoutTemplate.html @@ -1,8 +1,8 @@ 
- diff --git a/UI/Series/Details/SeriesDetailsTemplate.html b/UI/Series/Details/SeriesDetailsTemplate.html index 1aa4ed9fe..c653d54cb 100644 --- a/UI/Series/Details/SeriesDetailsTemplate.html +++ b/UI/Series/Details/SeriesDetailsTemplate.html @@ -2,7 +2,7 @@

- + {{title}}
diff --git a/UI/Series/series.less b/UI/Series/series.less index e80f978b1..70fa26eaa 100644 --- a/UI/Series/series.less +++ b/UI/Series/series.less @@ -274,3 +274,16 @@ font-size : 16px; vertical-align : middle !important; } + + +.seasonpass-series { + .season-pass-button { + display: inline-block; + width: 120px; + } + + .series-monitor-toggle { + font-size: 24px; + margin-top: 3px; + } +} \ No newline at end of file From 283cd36db8810be5718b05e8f69a1c152350f371 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 23 Sep 2013 22:27:51 -0700 Subject: [PATCH 11/34] Moved search hotkey to search.js --- UI/Navbar/Search.js | 11 +++++++++++ UI/app.js | 11 ----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/UI/Navbar/Search.js b/UI/Navbar/Search.js index 3391f9320..4e0e3b7d5 100644 --- a/UI/Navbar/Search.js +++ b/UI/Navbar/Search.js @@ -4,6 +4,17 @@ define( 'app', 'Series/SeriesCollection' ], function (App, SeriesCollection) { + $(document).on('keydown', function (e){ + if ($(e.target).is('input')) { + return; + } + + if (e.keyCode === 84) { + $('.x-series-search').focus(); + e.preventDefault(); + } + }); + $.fn.bindSearch = function () { $(this).typeahead({ source : function () { diff --git a/UI/app.js b/UI/app.js index d726c0830..8f64f4790 100644 --- a/UI/app.js +++ b/UI/app.js @@ -230,16 +230,5 @@ define( 'jQuery/TooltipBinder' ]); - $(document).on('keydown', function (e){ - if ($(e.target).is('input')) { - return; - } - - if (e.keyCode === 84) { - $('.x-series-search').focus(); - e.preventDefault(); - } - }); - return app; }); From dc2af41e16e9bf7133651a34574a2a4f2106cf09 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 24 Sep 2013 21:51:44 -0700 Subject: [PATCH 12/34] Advanced settings! SSL and branch --- UI/Config.js | 5 +- .../Overrides/bootstrap.toggle-switch.less | 6 +- UI/Settings/General/GeneralTemplate.html | 56 +++++-- UI/Settings/General/GeneralView.js | 62 +++++--- UI/Settings/SettingsLayout.js | 140 +++++++++++------- UI/Settings/SettingsLayoutTemplate.html | 13 ++ UI/Settings/settings.less | 36 ++++- 7 files changed, 227 insertions(+), 91 deletions(-) diff --git a/UI/Config.js b/UI/Config.js index f0d8c4722..e5409f25b 100644 --- a/UI/Config.js +++ b/UI/Config.js @@ -12,6 +12,10 @@ define( DefaultRootFolderId: 'DefaultRootFolderId' }, + getValueBoolean: function (key, defaultValue) { + return this.getValue(key, defaultValue) === 'true'; + }, + getValue: function (key, defaultValue) { var storeValue = localStorage.getItem(key); @@ -35,6 +39,5 @@ define( App.vent.trigger(this.Events.ConfigUpdatedEvent, {key: key, value: value}); } - }; }); diff --git a/UI/Content/Overrides/bootstrap.toggle-switch.less b/UI/Content/Overrides/bootstrap.toggle-switch.less index 153ff546d..1c8da8516 100644 --- a/UI/Content/Overrides/bootstrap.toggle-switch.less +++ b/UI/Content/Overrides/bootstrap.toggle-switch.less @@ -5,7 +5,7 @@ .slide-button { .buttonBackground(@btnDangerBackground, @btnDangerBackgroundHighlight); - &.btn-danger { + &.btn-danger, &.btn-warning { .buttonBackground(@btnInverseBackground, @btnInverseBackgroundHighlight); } } @@ -16,5 +16,9 @@ &.btn-danger { .buttonBackground(@btnDangerBackground, @btnDangerBackgroundHighlight); } + + &.btn-warning { + .buttonBackground(@btnWarningBackground, @btnWarningBackgroundHighlight); + } } } \ No newline at end of file diff --git a/UI/Settings/General/GeneralTemplate.html b/UI/Settings/General/GeneralTemplate.html index bb9de7626..01b9c40d1 100644 --- a/UI/Settings/General/GeneralTemplate.html +++ b/UI/Settings/General/GeneralTemplate.html @@ -7,11 +7,49 @@
- - - + + +
+
+ +
+ +
+
+ +
+
+ + +
+ +
+
+ +
+ + +
+ +
+
@@ -29,9 +67,9 @@
- - - + + +
@@ -51,7 +89,7 @@ - +
@@ -91,8 +129,7 @@

- {{#unless_eq branch compare="master"}} -
+
Development
@@ -106,5 +143,4 @@
- {{/unless_eq}}
diff --git a/UI/Settings/General/GeneralView.js b/UI/Settings/General/GeneralView.js index 56900fb50..6971dc5f5 100644 --- a/UI/Settings/General/GeneralView.js +++ b/UI/Settings/General/GeneralView.js @@ -5,38 +5,56 @@ define( 'Mixins/AsModelBoundView' ], function (Marionette, AsModelBoundView) { var view = Marionette.ItemView.extend({ - template: 'Settings/General/GeneralTemplate', + template: 'Settings/General/GeneralTemplate', + + events: { + 'change .x-auth': '_setAuthOptionsVisibility', + 'change .x-ssl': '_setSslOptionsVisibility' + }, + + ui: { + authToggle : '.x-auth', + authOptions: '.x-auth-options', + sslToggle : '.x-ssl', + sslOptions: '.x-ssl-options' + }, + + onRender: function(){ + if(!this.ui.authToggle.prop('checked')){ + this.ui.authOptions.hide(); + } - events: { - 'change .x-auth': '_setAuthOptionsVisibility' - }, + if(!this.ui.sslToggle.prop('checked')){ + this.ui.sslOptions.hide(); + } + }, - ui: { - authToggle : '.x-auth', - authOptions: '.x-auth-options' - }, + _setAuthOptionsVisibility: function () { + var showAuthOptions = this.ui.authToggle.prop('checked'); - onRender: function(){ - if(!this.ui.authToggle.prop('checked')){ - this.ui.authOptions.hide(); - } - }, + if (showAuthOptions) { + this.ui.authOptions.slideDown(); + } - _setAuthOptionsVisibility: function () { + else { + this.ui.authOptions.slideUp(); + } + }, - var showAuthOptions = this.ui.authToggle.prop('checked'); + _setSslOptionsVisibility: function () { - if (showAuthOptions) { - this.ui.authOptions.slideDown(); - } + var showSslOptions = this.ui.sslToggle.prop('checked'); - else { - this.ui.authOptions.slideUp(); - } + if (showSslOptions) { + this.ui.sslOptions.slideDown(); } - }); + else { + this.ui.sslOptions.slideUp(); + } + } + }); return AsModelBoundView.call(view); }); diff --git a/UI/Settings/SettingsLayout.js b/UI/Settings/SettingsLayout.js index f7c10c7a2..dd90b9a38 100644 --- a/UI/Settings/SettingsLayout.js +++ b/UI/Settings/SettingsLayout.js @@ -14,7 +14,8 @@ define( 'Settings/Notifications/CollectionView', 'Settings/Notifications/Collection', 'Settings/General/GeneralView', - 'Shared/LoadingView' + 'Shared/LoadingView', + 'Config' ], function (App, Marionette, SettingsModel, @@ -28,7 +29,8 @@ define( NotificationCollectionView, NotificationCollection, GeneralView, - LoadingView) { + LoadingView, + Config) { return Marionette.Layout.extend({ template: 'Settings/SettingsLayoutTemplate', @@ -48,7 +50,8 @@ define( indexersTab : '.x-indexers-tab', downloadClientTab : '.x-download-client-tab', notificationsTab : '.x-notifications-tab', - generalTab : '.x-general-tab' + generalTab : '.x-general-tab', + advancedSettings : '.x-advanced-settings' }, events: { @@ -58,7 +61,67 @@ define( 'click .x-download-client-tab' : '_showDownloadClient', 'click .x-notifications-tab' : '_showNotifications', 'click .x-general-tab' : '_showGeneral', - 'click .x-save-settings' : '_save' + 'click .x-save-settings' : '_save', + 'change .x-advanced-settings' : '_toggleAdvancedSettings' + }, + + initialize: function (options) { + if (options.action) { + this.action = options.action.toLowerCase(); + } + }, + + onRender: function () { + this.loading.show(new LoadingView()); + var self = this; + + this.settings = new SettingsModel(); + this.generalSettings = new GeneralSettingsModel(); + this.namingSettings = new NamingModel(); + this.indexerSettings = new IndexerCollection(); + this.notificationSettings = new NotificationCollection(); + + $.when(this.settings.fetch(), + this.generalSettings.fetch(), + this.namingSettings.fetch(), + this.indexerSettings.fetch(), + this.notificationSettings.fetch() + ).done(function () { + self.loading.$el.hide(); + self.mediaManagement.show(new MediaManagementLayout({ settings: self.settings, namingSettings: self.namingSettings })); + self.quality.show(new QualityLayout({ settings: self.settings })); + self.indexers.show(new IndexerLayout({ settings: self.settings, indexersCollection: self.indexerSettings })); + self.downloadClient.show(new DownloadClientLayout({ model: self.settings })); + self.notifications.show(new NotificationCollectionView({ collection: self.notificationSettings })); + self.general.show(new GeneralView({ model: self.generalSettings })); + }); + + this._setAdvancedSettingsState(); + }, + + onShow: function () { + switch (this.action) { + case 'quality': + this._showQuality(); + break; + case 'indexers': + this._showIndexers(); + break; + case 'downloadclient': + this._showDownloadClient(); + break; + case 'connect': + this._showNotifications(); + break; + case 'notifications': + this._showNotifications(); + break; + case 'general': + this._showGeneral(); + break; + default: + this._showMediaManagement(); + } }, _showMediaManagement: function (e) { @@ -121,65 +184,30 @@ define( }); }, - initialize: function (options) { - if (options.action) { - this.action = options.action.toLowerCase(); - } + _save: function () { + App.vent.trigger(App.Commands.SaveSettings); }, - onRender: function () { - this.loading.show(new LoadingView()); - var self = this; - - this.settings = new SettingsModel(); - this.generalSettings = new GeneralSettingsModel(); - this.namingSettings = new NamingModel(); - this.indexerSettings = new IndexerCollection(); - this.notificationSettings = new NotificationCollection(); + _setAdvancedSettingsState: function () { + var checked = Config.getValueBoolean('advancedSettings'); + this.ui.advancedSettings.prop('checked', checked); - $.when(this.settings.fetch(), - this.generalSettings.fetch(), - this.namingSettings.fetch(), - this.indexerSettings.fetch(), - this.notificationSettings.fetch() - ).done(function () { - self.loading.$el.hide(); - self.mediaManagement.show(new MediaManagementLayout({ settings: self.settings, namingSettings: self.namingSettings })); - self.quality.show(new QualityLayout({settings: self.settings})); - self.indexers.show(new IndexerLayout({ settings: self.settings, indexersCollection: self.indexerSettings })); - self.downloadClient.show(new DownloadClientLayout({model: self.settings})); - self.notifications.show(new NotificationCollectionView({collection: self.notificationSettings})); - self.general.show(new GeneralView({model: self.generalSettings})); - }); + if (checked) { + this.$el.addClass('show-advanced-settings'); + } }, - onShow: function () { - switch (this.action) { - case 'quality': - this._showQuality(); - break; - case 'indexers': - this._showIndexers(); - break; - case 'downloadclient': - this._showDownloadClient(); - break; - case 'connect': - this._showNotifications(); - break; - case 'notifications': - this._showNotifications(); - break; - case 'general': - this._showGeneral(); - break; - default: - this._showMediaManagement(); + _toggleAdvancedSettings: function () { + var checked = this.ui.advancedSettings.prop('checked'); + Config.setValue('advancedSettings', checked); + + if (checked) { + this.$el.addClass('show-advanced-settings'); } - }, - _save: function () { - App.vent.trigger(App.Commands.SaveSettings); + else { + this.$el.removeClass('show-advanced-settings'); + } } }); }); diff --git a/UI/Settings/SettingsLayoutTemplate.html b/UI/Settings/SettingsLayoutTemplate.html index 6264503ac..c5f22dd23 100644 --- a/UI/Settings/SettingsLayoutTemplate.html +++ b/UI/Settings/SettingsLayoutTemplate.html @@ -6,6 +6,19 @@
  • Connect
  • General
  • +
  • +
  • diff --git a/UI/Settings/settings.less b/UI/Settings/settings.less index e1d594cc2..5addfd8fb 100644 --- a/UI/Settings/settings.less +++ b/UI/Settings/settings.less @@ -1,5 +1,5 @@ +@import "../Content/Bootstrap/variables"; @import "../Shared/Styles/clickable.less"; - @import "Indexers/indexers"; @import "Quality/quality"; @import "Notifications/notifications"; @@ -43,4 +43,38 @@ li.save-and-add:hover { .naming-example { display: inline-block; margin-top: 5px; +} + +.advanced-settings-toggle { + margin-right: 40px; + + .checkbox { + width : 100px; + margin-left : 0px; + display : inline-block; + padding-top : 0px; + margin-bottom : 0px; + margin-top : -1px; + } + + .help-inline-checkbox { + display : inline-block; + margin-top : -23px; + margin-bottom : 0; + vertical-align : middle; + } +} + +.advanced-setting { + display: none; + + .control-label { + color: @warningText; + } +} + +.show-advanced-settings { + .advanced-setting { + display: block; + } } \ No newline at end of file From 1c77712bf79da30927734149e9be8ef001432710 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 24 Sep 2013 22:04:33 -0700 Subject: [PATCH 13/34] Advancified some more settings --- UI/Settings/Indexers/{Layout.js => IndexerLayout.js} | 4 ++-- .../{LayoutTemplate.html => IndexerLayoutTemplate.html} | 0 .../View.js => Indexers/Options/IndexerOptionsView.js} | 2 +- .../{ViewTemplate.html => IndexerOptionsViewTemplate.html} | 4 ++-- .../FileManagement/FileManagementView.js} | 2 +- .../{ViewTemplate.html => FileManagementViewTemplate.html} | 2 +- .../MediaManagement/{Layout.js => MediaManagementLayout.js} | 4 ++-- ...LayoutTemplate.html => MediaManagementLayoutTemplate.html} | 0 UI/Settings/SettingsLayout.js | 4 ++-- 9 files changed, 11 insertions(+), 11 deletions(-) rename UI/Settings/Indexers/{Layout.js => IndexerLayout.js} (86%) rename UI/Settings/Indexers/{LayoutTemplate.html => IndexerLayoutTemplate.html} (100%) rename UI/Settings/{MediaManagement/FileManagement/View.js => Indexers/Options/IndexerOptionsView.js} (80%) rename UI/Settings/Indexers/Options/{ViewTemplate.html => IndexerOptionsViewTemplate.html} (92%) rename UI/Settings/{Indexers/Options/View.js => MediaManagement/FileManagement/FileManagementView.js} (73%) rename UI/Settings/MediaManagement/FileManagement/{ViewTemplate.html => FileManagementViewTemplate.html} (97%) rename UI/Settings/MediaManagement/{Layout.js => MediaManagementLayout.js} (86%) rename UI/Settings/MediaManagement/{LayoutTemplate.html => MediaManagementLayoutTemplate.html} (100%) diff --git a/UI/Settings/Indexers/Layout.js b/UI/Settings/Indexers/IndexerLayout.js similarity index 86% rename from UI/Settings/Indexers/Layout.js rename to UI/Settings/Indexers/IndexerLayout.js index 6cbcd5352..a5b200ed2 100644 --- a/UI/Settings/Indexers/Layout.js +++ b/UI/Settings/Indexers/IndexerLayout.js @@ -4,10 +4,10 @@ define( [ 'marionette', 'Settings/Indexers/CollectionView', - 'Settings/Indexers/Options/View' + 'Settings/Indexers/Options/IndexerOptionsView' ], function (Marionette, CollectionView, OptionsView) { return Marionette.Layout.extend({ - template: 'Settings/Indexers/LayoutTemplate', + template: 'Settings/Indexers/IndexerLayoutTemplate', regions: { indexersRegion : '#indexers-collection', diff --git a/UI/Settings/Indexers/LayoutTemplate.html b/UI/Settings/Indexers/IndexerLayoutTemplate.html similarity index 100% rename from UI/Settings/Indexers/LayoutTemplate.html rename to UI/Settings/Indexers/IndexerLayoutTemplate.html diff --git a/UI/Settings/MediaManagement/FileManagement/View.js b/UI/Settings/Indexers/Options/IndexerOptionsView.js similarity index 80% rename from UI/Settings/MediaManagement/FileManagement/View.js rename to UI/Settings/Indexers/Options/IndexerOptionsView.js index 7c4a89638..2fa8f3a59 100644 --- a/UI/Settings/MediaManagement/FileManagement/View.js +++ b/UI/Settings/Indexers/Options/IndexerOptionsView.js @@ -6,7 +6,7 @@ define( ], function (Marionette, AsModelBoundView) { var view = Marionette.ItemView.extend({ - template: 'Settings/MediaManagement/FileManagement/ViewTemplate' + template: 'Settings/Indexers/Options/IndexerOptionsViewTemplate' }); return AsModelBoundView.call(view); diff --git a/UI/Settings/Indexers/Options/ViewTemplate.html b/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.html similarity index 92% rename from UI/Settings/Indexers/Options/ViewTemplate.html rename to UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.html index 1830fd4b2..616a66f76 100644 --- a/UI/Settings/Indexers/Options/ViewTemplate.html +++ b/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.html @@ -9,7 +9,7 @@
    -
    +
    @@ -21,7 +21,7 @@
    -
    +
    diff --git a/UI/Settings/Indexers/Options/View.js b/UI/Settings/MediaManagement/FileManagement/FileManagementView.js similarity index 73% rename from UI/Settings/Indexers/Options/View.js rename to UI/Settings/MediaManagement/FileManagement/FileManagementView.js index ff1b67f51..314a7b79e 100644 --- a/UI/Settings/Indexers/Options/View.js +++ b/UI/Settings/MediaManagement/FileManagement/FileManagementView.js @@ -6,7 +6,7 @@ define( ], function (Marionette, AsModelBoundView) { var view = Marionette.ItemView.extend({ - template: 'Settings/Indexers/Options/ViewTemplate' + template: 'Settings/MediaManagement/FileManagement/FileManagementViewTemplate' }); return AsModelBoundView.call(view); diff --git a/UI/Settings/MediaManagement/FileManagement/ViewTemplate.html b/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html similarity index 97% rename from UI/Settings/MediaManagement/FileManagement/ViewTemplate.html rename to UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html index 5f5b5c052..95b53a1a2 100644 --- a/UI/Settings/MediaManagement/FileManagement/ViewTemplate.html +++ b/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html @@ -1,4 +1,4 @@ -
    +
    File Management
    diff --git a/UI/Settings/MediaManagement/Layout.js b/UI/Settings/MediaManagement/MediaManagementLayout.js similarity index 86% rename from UI/Settings/MediaManagement/Layout.js rename to UI/Settings/MediaManagement/MediaManagementLayout.js index 7b7a4e42c..47d670a40 100644 --- a/UI/Settings/MediaManagement/Layout.js +++ b/UI/Settings/MediaManagement/MediaManagementLayout.js @@ -5,10 +5,10 @@ define( 'marionette', 'Settings/MediaManagement/Naming/View', 'Settings/MediaManagement/Sorting/View', - 'Settings/MediaManagement/FileManagement/View' + 'Settings/MediaManagement/FileManagement/FileManagementView' ], function (Marionette, NamingView, SortingView, FileManagementView) { return Marionette.Layout.extend({ - template: 'Settings/MediaManagement/LayoutTemplate', + template: 'Settings/MediaManagement/MediaManagementLayoutTemplate', regions: { episodeNaming : '#episode-naming', diff --git a/UI/Settings/MediaManagement/LayoutTemplate.html b/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.html similarity index 100% rename from UI/Settings/MediaManagement/LayoutTemplate.html rename to UI/Settings/MediaManagement/MediaManagementLayoutTemplate.html diff --git a/UI/Settings/SettingsLayout.js b/UI/Settings/SettingsLayout.js index dd90b9a38..cad4cfaa9 100644 --- a/UI/Settings/SettingsLayout.js +++ b/UI/Settings/SettingsLayout.js @@ -6,9 +6,9 @@ define( 'Settings/SettingsModel', 'Settings/General/GeneralSettingsModel', 'Settings/MediaManagement/Naming/Model', - 'Settings/MediaManagement/Layout', + 'Settings/MediaManagement/MediaManagementLayout', 'Settings/Quality/QualityLayout', - 'Settings/Indexers/Layout', + 'Settings/Indexers/IndexerLayout', 'Settings/Indexers/Collection', 'Settings/DownloadClient/Layout', 'Settings/Notifications/CollectionView', From 34b5a833f59be852a87bd282f8b80431ae8fa104 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 25 Sep 2013 17:14:27 -0700 Subject: [PATCH 14/34] SeasonCount excludes specials --- NzbDrone.Api/Series/SeriesResource.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/NzbDrone.Api/Series/SeriesResource.cs b/NzbDrone.Api/Series/SeriesResource.cs index 170fa8301..dc7cc3479 100644 --- a/NzbDrone.Api/Series/SeriesResource.cs +++ b/NzbDrone.Api/Series/SeriesResource.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using NzbDrone.Api.REST; using NzbDrone.Core.MediaCover; using NzbDrone.Core.Tv; @@ -19,9 +20,9 @@ namespace NzbDrone.Api.Series { get { - if (Seasons != null) return Seasons.Count; + if (Seasons == null) return 0; - return 0; + return Seasons.Where(s => s.SeasonNumber > 0).Count(); } } From f0e721ee8013e7eff1cf17d04d5079972f1f29c6 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 25 Sep 2013 19:51:07 -0700 Subject: [PATCH 15/34] Validate newznab indexers when adding --- .../Exceptions/BadRequestException.cs | 4 +- .../Exceptions/StatusCodeToExceptions.cs | 2 +- NzbDrone.Core/Indexers/IndexerService.cs | 11 +++- NzbDrone.Core/Indexers/Newznab/Newznab.cs | 2 - NzbDrone.Core/Indexers/NewznabTestService.cs | 54 +++++++++++++++++++ NzbDrone.Core/NzbDrone.Core.csproj | 1 + UI/Settings/Indexers/EditTemplate.html | 3 ++ UI/Settings/Indexers/EditView.js | 16 ++++++ 8 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 NzbDrone.Core/Indexers/NewznabTestService.cs diff --git a/NzbDrone.Core/Exceptions/BadRequestException.cs b/NzbDrone.Core/Exceptions/BadRequestException.cs index 212b69e87..adf8bf57f 100644 --- a/NzbDrone.Core/Exceptions/BadRequestException.cs +++ b/NzbDrone.Core/Exceptions/BadRequestException.cs @@ -4,11 +4,11 @@ namespace NzbDrone.Core.Exceptions { public class BadRequestException : DownstreamException { - public BadRequestException(HttpStatusCode statusCode, string message) : base(statusCode, message) + public BadRequestException(string message) : base(HttpStatusCode.BadRequest, message) { } - public BadRequestException(HttpStatusCode statusCode, string message, params object[] args) : base(statusCode, message, args) + public BadRequestException(string message, params object[] args) : base(HttpStatusCode.BadRequest, message, args) { } } diff --git a/NzbDrone.Core/Exceptions/StatusCodeToExceptions.cs b/NzbDrone.Core/Exceptions/StatusCodeToExceptions.cs index 10513d9c6..8deb955bb 100644 --- a/NzbDrone.Core/Exceptions/StatusCodeToExceptions.cs +++ b/NzbDrone.Core/Exceptions/StatusCodeToExceptions.cs @@ -15,7 +15,7 @@ namespace NzbDrone.Core.Exceptions switch (statusCode) { case HttpStatusCode.BadRequest: - throw new BadRequestException(statusCode, message); + throw new BadRequestException(message); case HttpStatusCode.Unauthorized: throw new UnauthorizedAccessException(message); diff --git a/NzbDrone.Core/Indexers/IndexerService.cs b/NzbDrone.Core/Indexers/IndexerService.cs index 2c87569e3..e1b49c2dd 100644 --- a/NzbDrone.Core/Indexers/IndexerService.cs +++ b/NzbDrone.Core/Indexers/IndexerService.cs @@ -37,14 +37,20 @@ namespace NzbDrone.Core.Indexers { private readonly IIndexerRepository _indexerRepository; private readonly IConfigFileProvider _configFileProvider; + private readonly INewznabTestService _newznabTestService; private readonly Logger _logger; private readonly List _indexers; - public IndexerService(IIndexerRepository indexerRepository, IEnumerable indexers, IConfigFileProvider configFileProvider, Logger logger) + public IndexerService(IIndexerRepository indexerRepository, + IEnumerable indexers, + IConfigFileProvider configFileProvider, + INewznabTestService newznabTestService, + Logger logger) { _indexerRepository = indexerRepository; _configFileProvider = configFileProvider; + _newznabTestService = newznabTestService; _logger = logger; @@ -104,6 +110,9 @@ namespace NzbDrone.Core.Indexers Settings = indexer.Settings.ToJson() }; + var instance = ToIndexer(definition).Instance; + _newznabTestService.Test(instance); + definition = _indexerRepository.Insert(definition); indexer.Id = definition.Id; diff --git a/NzbDrone.Core/Indexers/Newznab/Newznab.cs b/NzbDrone.Core/Indexers/Newznab/Newznab.cs index 4341c22f7..8a13d2282 100644 --- a/NzbDrone.Core/Indexers/Newznab/Newznab.cs +++ b/NzbDrone.Core/Indexers/Newznab/Newznab.cs @@ -114,7 +114,6 @@ namespace NzbDrone.Core.Indexers.Newznab return RecentFeed.Select(url => String.Format("{0}&limit=100&q={1}&season={2}&offset={3}", url, NewsnabifyTitle(seriesTitle), seasonNumber, offset)); } - public override string Name { get @@ -131,7 +130,6 @@ namespace NzbDrone.Core.Indexers.Newznab } } - private static string NewsnabifyTitle(string title) { return title.Replace("+", "%20"); diff --git a/NzbDrone.Core/Indexers/NewznabTestService.cs b/NzbDrone.Core/Indexers/NewznabTestService.cs new file mode 100644 index 000000000..dd5fea38c --- /dev/null +++ b/NzbDrone.Core/Indexers/NewznabTestService.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common; + +namespace NzbDrone.Core.Indexers +{ + public interface INewznabTestService + { + void Test(IIndexer indexer); + } + + public class NewznabTestService : INewznabTestService + { + private readonly IFetchFeedFromIndexers _feedFetcher; + private readonly IHttpProvider _httpProvider; + private readonly Logger _logger; + + public NewznabTestService(IFetchFeedFromIndexers feedFetcher, IHttpProvider httpProvider, Logger logger) + { + _feedFetcher = feedFetcher; + _httpProvider = httpProvider; + _logger = logger; + } + + public void Test(IIndexer indexer) + { + var releases = _feedFetcher.FetchRss(indexer); + + if (releases.Any()) return; + + try + { + _httpProvider.DownloadString(indexer.RecentFeed.First()); + } + + catch (Exception) + { + _logger.Warn("No result returned from RSS Feed, please confirm you're using a newznab indexer"); + + var failure = new ValidationFailure("Url", "Invalid Newznab URL entered"); + throw new ValidationException(new List { failure }.ToArray()); + } + + _logger.Warn("Indexer returned result for Newznab RSS URL, API Key appears to be invalid"); + + var apiKeyFailure = new ValidationFailure("ApiKey", "Invalid API Key"); + throw new ValidationException(new List { apiKeyFailure }.ToArray()); + } + } +} diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 08803f3fa..205b6c817 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -238,6 +238,7 @@ + diff --git a/UI/Settings/Indexers/EditTemplate.html b/UI/Settings/Indexers/EditTemplate.html index e433b1a29..f9c3779ca 100644 --- a/UI/Settings/Indexers/EditTemplate.html +++ b/UI/Settings/Indexers/EditTemplate.html @@ -39,6 +39,9 @@ {{#if id}} {{/if}} + + +
    diff --git a/UI/Settings/Indexers/EditView.js b/UI/Settings/Indexers/EditView.js index 1d49366cb..0e4f227f2 100644 --- a/UI/Settings/Indexers/EditView.js +++ b/UI/Settings/Indexers/EditView.js @@ -11,6 +11,10 @@ define( var view = Marionette.ItemView.extend({ template: 'Settings/Indexers/EditTemplate', + ui : { + activity: '.x-activity' + }, + events: { 'click .x-save' : '_save', 'click .x-save-and-add': '_saveAndAdd' @@ -21,6 +25,8 @@ define( }, _save: function () { + this.ui.activity.html(''); + var self = this; var promise = this.model.saveSettings(); @@ -29,10 +35,16 @@ define( self.indexerCollection.add(self.model, { merge: true }); App.vent.trigger(App.Commands.CloseModalCommand); }); + + promise.always(function () { + self.ui.activity.empty(); + }); } }, _saveAndAdd: function () { + this.ui.activity.html(''); + var self = this; var promise = this.model.saveSettings(); @@ -50,6 +62,10 @@ define( self.model.set('fields.' + key + '.value', ''); }); }); + + promise.always(function () { + self.ui.activity.empty(); + }); } } }); From 0f4bfd7afc4476d6855141b77bc9ed6f6352f34e Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 25 Sep 2013 20:49:22 -0700 Subject: [PATCH 16/34] Quality sorting for ManualSearch --- UI/Cells/Header/QualityHeaderCell.js | 69 ++++++++++++++++++++++++++++ UI/Episode/Search/ManualLayout.js | 14 +++--- UI/Release/Collection.js | 10 ++-- UI/Shared/Grid/DateHeaderCell.js | 8 ++-- 4 files changed, 85 insertions(+), 16 deletions(-) create mode 100644 UI/Cells/Header/QualityHeaderCell.js diff --git a/UI/Cells/Header/QualityHeaderCell.js b/UI/Cells/Header/QualityHeaderCell.js new file mode 100644 index 000000000..533386cb2 --- /dev/null +++ b/UI/Cells/Header/QualityHeaderCell.js @@ -0,0 +1,69 @@ +'use strict'; + +define( + [ + 'backgrid', + 'Shared/Grid/HeaderCell' + ], function (Backgrid, NzbDroneHeaderCell) { + + Backgrid.QualityHeaderCell = NzbDroneHeaderCell.extend({ + events: { + 'click': 'onClick' + }, + + onClick: function (e) { + e.preventDefault(); + + var self = this; + var columnName = this.column.get('name'); + + if (this.column.get('sortable')) { + if (this.direction() === 'ascending') { + this.sort(columnName, 'descending', function (left, right) { + var leftVal = left.get(columnName); + var rightVal = right.get(columnName); + + return self._comparator(leftVal, rightVal); + }); + } + else { + this.sort(columnName, 'ascending', function (left, right) { + var leftVal = left.get(columnName); + var rightVal = right.get(columnName); + + return self._comparator(rightVal, leftVal); + }); + } + } + }, + + _comparator: function (leftVal, rightVal) { + var leftWeight = leftVal.quality.weight; + var rightWeight = rightVal.quality.weight; + + if (!leftWeight && !rightWeight) { + return 0; + } + + if (!leftWeight) { + return -1; + } + + if (!rightWeight) { + return 1; + } + + if (leftWeight === rightWeight) { + return 0; + } + + if (leftWeight > rightWeight) { + return -1; + } + + return 1; + } + }); + + return Backgrid.QualityHeaderCell; + }); diff --git a/UI/Episode/Search/ManualLayout.js b/UI/Episode/Search/ManualLayout.js index 8647b4c2a..c782179df 100644 --- a/UI/Episode/Search/ManualLayout.js +++ b/UI/Episode/Search/ManualLayout.js @@ -6,9 +6,10 @@ define( 'Cells/FileSizeCell', 'Cells/QualityCell', 'Cells/ApprovalStatusCell', - 'Release/DownloadReportCell' + 'Release/DownloadReportCell', + 'Cells/Header/QualityHeaderCell' - ], function (Marionette, Backgrid, FileSizeCell, QualityCell, ApprovalStatusCell, DownloadReportCell) { + ], function (Marionette, Backgrid, FileSizeCell, QualityCell, ApprovalStatusCell, DownloadReportCell, QualityHeaderCell) { return Marionette.Layout.extend({ template: 'Episode/Search/ManualLayoutTemplate', @@ -44,10 +45,11 @@ define( cell : FileSizeCell }, { - name : 'quality', - label : 'Quality', - sortable: true, - cell : QualityCell + name : 'quality', + label : 'Quality', + sortable : true, + cell : QualityCell, + headerCell: QualityHeaderCell }, { diff --git a/UI/Release/Collection.js b/UI/Release/Collection.js index 93ec60c15..fb2a2ec63 100644 --- a/UI/Release/Collection.js +++ b/UI/Release/Collection.js @@ -1,15 +1,13 @@ 'use strict'; define( [ - 'Release/Model', - 'backbone.pageable' - ], function (ReleaseModel, PagableCollection) { - return PagableCollection.extend({ + 'backbone', + 'Release/Model' + ], function (Backbone, ReleaseModel) { + return Backbone.Collection.extend({ url : window.NzbDrone.ApiRoot + '/release', model: ReleaseModel, - mode: 'client', - state: { pageSize: 2000 }, diff --git a/UI/Shared/Grid/DateHeaderCell.js b/UI/Shared/Grid/DateHeaderCell.js index 5473f1093..fcc3e2ba4 100644 --- a/UI/Shared/Grid/DateHeaderCell.js +++ b/UI/Shared/Grid/DateHeaderCell.js @@ -23,7 +23,7 @@ define( var leftVal = left.get(columnName); var rightVal = right.get(columnName); - return self._comparator(leftVal, rightVal) + return self._comparator(leftVal, rightVal); }); } else { @@ -31,7 +31,7 @@ define( var leftVal = left.get(columnName); var rightVal = right.get(columnName); - return self._comparator(rightVal, leftVal) + return self._comparator(rightVal, leftVal); }); } } @@ -39,7 +39,7 @@ define( _comparator: function (leftVal, rightVal) { if (!leftVal && !rightVal) { - return 0 + return 0; } if (!leftVal) { @@ -47,7 +47,7 @@ define( } if (!rightVal) { - return 1 + return 1; } if (leftVal === rightVal) { From 8d5e92d60265efed738e6fed41a3decf708d63b6 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 25 Sep 2013 21:05:08 -0700 Subject: [PATCH 17/34] Disable quality sorting on history - since its json --- UI/History/HistoryLayout.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/UI/History/HistoryLayout.js b/UI/History/HistoryLayout.js index 160ef9227..bf3d9928b 100644 --- a/UI/History/HistoryLayout.js +++ b/UI/History/HistoryLayout.js @@ -60,9 +60,10 @@ define( cell : EpisodeTitleCell }, { - name : 'quality', - label: 'Quality', - cell : QualityCell + name : 'quality', + label : 'Quality', + cell : QualityCell, + sortable: false }, { name : 'date', From 4cb9db3230124f6cb2a697cbfaf9542c449a066c Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 26 Sep 2013 11:04:26 -0700 Subject: [PATCH 18/34] Updated donation link --- UI/Navbar/NavbarTemplate.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/Navbar/NavbarTemplate.html b/UI/Navbar/NavbarTemplate.html index d9b1c83e6..57b62d37c 100644 --- a/UI/Navbar/NavbarTemplate.html +++ b/UI/Navbar/NavbarTemplate.html @@ -50,7 +50,7 @@
  • - +
    Donate From 377a32e5e63d7a0acc47b2e9ca3a1dc63553e3ba Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 26 Sep 2013 12:15:16 -0700 Subject: [PATCH 19/34] Fixed SSL cert registration string --- NzbDrone.Host/AccessControl/SslAdapter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NzbDrone.Host/AccessControl/SslAdapter.cs b/NzbDrone.Host/AccessControl/SslAdapter.cs index c94e307a3..dced23f96 100644 --- a/NzbDrone.Host/AccessControl/SslAdapter.cs +++ b/NzbDrone.Host/AccessControl/SslAdapter.cs @@ -36,7 +36,7 @@ namespace NzbDrone.Host.AccessControl return; } - var arguments = String.Format("netsh http add sslcert ipport=0.0.0.0:{0} certhash={1} appid={{{2}}", _configFileProvider.SslPort, _configFileProvider.SslCertHash, APP_ID); + var arguments = String.Format("netsh http add sslcert ipport=0.0.0.0:{0} certhash={1} appid={{{2}}}", _configFileProvider.SslPort, _configFileProvider.SslCertHash, APP_ID); _netshProvider.Run(arguments); } From dbc0c2021e802592546e766dea5d39a3103c5710 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 26 Sep 2013 14:07:39 -0700 Subject: [PATCH 20/34] Fixed sslcert registration arguments --- NzbDrone.Host/AccessControl/SslAdapter.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/NzbDrone.Host/AccessControl/SslAdapter.cs b/NzbDrone.Host/AccessControl/SslAdapter.cs index dced23f96..ddb2e7201 100644 --- a/NzbDrone.Host/AccessControl/SslAdapter.cs +++ b/NzbDrone.Host/AccessControl/SslAdapter.cs @@ -36,7 +36,12 @@ namespace NzbDrone.Host.AccessControl return; } - var arguments = String.Format("netsh http add sslcert ipport=0.0.0.0:{0} certhash={1} appid={{{2}}}", _configFileProvider.SslPort, _configFileProvider.SslCertHash, APP_ID); + var arguments = String.Format("http add sslcert ipport=0.0.0.0:{0} certhash={1} appid={{{2}}}", + _configFileProvider.SslPort, + _configFileProvider.SslCertHash, + APP_ID); + + //TODO: Validate that the cert was added properly, invisible spaces FTL _netshProvider.Run(arguments); } From 74ac67eab1eeae3698df19e124334cf6b8c1bb43 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 26 Sep 2013 14:57:36 -0700 Subject: [PATCH 21/34] Open SSL port in firewall when SSL is enabled --- .../AccessControl/FirewallAdapter.cs | 19 ++++++++++--------- UI/Settings/General/GeneralTemplate.html | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/NzbDrone.Host/AccessControl/FirewallAdapter.cs b/NzbDrone.Host/AccessControl/FirewallAdapter.cs index 72d194446..3aa624d10 100644 --- a/NzbDrone.Host/AccessControl/FirewallAdapter.cs +++ b/NzbDrone.Host/AccessControl/FirewallAdapter.cs @@ -26,18 +26,21 @@ namespace NzbDrone.Host.AccessControl { if (IsFirewallEnabled()) { - if (IsNzbDronePortOpen()) + if (!IsNzbDronePortOpen(_configFileProvider.Port)) { - _logger.Trace("NzbDrone port is already open, skipping."); - return; + _logger.Trace("Opening Port for NzbDrone: {0}", _configFileProvider.Port); + OpenFirewallPort(_configFileProvider.Port); } - OpenFirewallPort(_configFileProvider.Port); + if (_configFileProvider.EnableSsl && !IsNzbDronePortOpen(_configFileProvider.SslPort)) + { + _logger.Trace("Opening SSL Port for NzbDrone: {0}", _configFileProvider.SslPort); + OpenFirewallPort(_configFileProvider.SslPort); + } } } - - private bool IsNzbDronePortOpen() + private bool IsNzbDronePortOpen(int port) { try { @@ -52,7 +55,7 @@ namespace NzbDrone.Host.AccessControl foreach (INetFwOpenPort p in ports) { - if (p.Port == _configFileProvider.Port) + if (p.Port == port) return true; } } @@ -63,8 +66,6 @@ namespace NzbDrone.Host.AccessControl return false; } - - private void OpenFirewallPort(int portNumber) { try diff --git a/UI/Settings/General/GeneralTemplate.html b/UI/Settings/General/GeneralTemplate.html index 01b9c40d1..44d9e3d11 100644 --- a/UI/Settings/General/GeneralTemplate.html +++ b/UI/Settings/General/GeneralTemplate.html @@ -47,7 +47,7 @@
    - +
  • From ca429cf5dea2a9a608d897b1d0e9e433406305e5 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 26 Sep 2013 20:40:36 -0700 Subject: [PATCH 22/34] Set year to current year when show doesn't have a valid year --- NzbDrone.Core/MetadataSource/TraktProxy.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/NzbDrone.Core/MetadataSource/TraktProxy.cs b/NzbDrone.Core/MetadataSource/TraktProxy.cs index 69a526140..b907a0a16 100644 --- a/NzbDrone.Core/MetadataSource/TraktProxy.cs +++ b/NzbDrone.Core/MetadataSource/TraktProxy.cs @@ -71,7 +71,7 @@ namespace NzbDrone.Core.MetadataSource series.ImdbId = show.imdb_id; series.Title = show.title; series.CleanTitle = Parser.Parser.CleanSeriesTitle(show.title); - series.Year = show.year; + series.Year = GetYear(show.year, show.first_aired); series.FirstAired = FromIso(show.first_aired_iso); series.Overview = show.overview; series.Runtime = show.runtime; @@ -159,5 +159,14 @@ namespace NzbDrone.Core.MetadataSource return phrase; } + + private static int GetYear(int year, int firstAired) + { + if (year > 1969) return year; + + if (firstAired == 0) return DateTime.Today.Year; + + return year; + } } } \ No newline at end of file From 9fa4cedb714bdde7e4e03494832740ece2c19df2 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 26 Sep 2013 21:41:08 -0700 Subject: [PATCH 23/34] Now checking for errors before parsing newznab feeds --- .../Files/Indexers/Newznab/error.xml | 2 ++ NzbDrone.Core.Test/NzbDrone.Core.Test.csproj | 2 +- .../Indexers/Exceptions/ApiKeyException.cs | 19 +++++++++++++++ NzbDrone.Core/Indexers/IndexerFetchService.cs | 13 ++++++---- .../Indexers/Newznab/NewznabException.cs | 19 +++++++++++++++ .../Indexers/Newznab/NewznabParser.cs | 7 ++++++ .../Indexers/Newznab/NewznabPreProcessor.cs | 24 +++++++++++++++++++ NzbDrone.Core/Indexers/NewznabTestService.cs | 22 ++++++++++------- NzbDrone.Core/Indexers/RssParserBase.cs | 6 +++++ NzbDrone.Core/NzbDrone.Core.csproj | 3 +++ UI/Settings/Indexers/EditView.js | 4 ++-- 11 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 NzbDrone.Core.Test/Files/Indexers/Newznab/error.xml create mode 100644 NzbDrone.Core/Indexers/Exceptions/ApiKeyException.cs create mode 100644 NzbDrone.Core/Indexers/Newznab/NewznabException.cs create mode 100644 NzbDrone.Core/Indexers/Newznab/NewznabPreProcessor.cs diff --git a/NzbDrone.Core.Test/Files/Indexers/Newznab/error.xml b/NzbDrone.Core.Test/Files/Indexers/Newznab/error.xml new file mode 100644 index 000000000..33e46d9b6 --- /dev/null +++ b/NzbDrone.Core.Test/Files/Indexers/Newznab/error.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 22ee553f8..99987c84b 100644 --- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -252,6 +252,7 @@ Always + Always @@ -351,7 +352,6 @@ - diff --git a/NzbDrone.Core/Indexers/Exceptions/ApiKeyException.cs b/NzbDrone.Core/Indexers/Exceptions/ApiKeyException.cs new file mode 100644 index 000000000..ff97425f8 --- /dev/null +++ b/NzbDrone.Core/Indexers/Exceptions/ApiKeyException.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Indexers.Exceptions +{ + public class ApiKeyException : NzbDroneException + { + public ApiKeyException(string message, params object[] args) : base(message, args) + { + } + + public ApiKeyException(string message) : base(message) + { + } + } +} diff --git a/NzbDrone.Core/Indexers/IndexerFetchService.cs b/NzbDrone.Core/Indexers/IndexerFetchService.cs index 9814adc0d..ba284c8b7 100644 --- a/NzbDrone.Core/Indexers/IndexerFetchService.cs +++ b/NzbDrone.Core/Indexers/IndexerFetchService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Net; using NLog; using NzbDrone.Common; +using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; using System.Linq; @@ -30,7 +31,6 @@ namespace NzbDrone.Core.Indexers _logger = logger; } - public virtual IList FetchRss(IIndexer indexer) { _logger.Debug("Fetching feeds from " + indexer.Name); @@ -53,7 +53,6 @@ namespace NzbDrone.Core.Indexers return result; } - private IList Fetch(IIndexer indexer, SeasonSearchCriteria searchCriteria, int offset) { _logger.Debug("Searching for {0} offset: {1}", searchCriteria, offset); @@ -117,15 +116,21 @@ namespace NzbDrone.Core.Indexers } catch (WebException webException) { - if (webException.Message.Contains("502") || webException.Message.Contains("503") || webException.Message.Contains("timed out")) + if (webException.Message.Contains("502") || webException.Message.Contains("503") || + webException.Message.Contains("timed out")) { - _logger.Warn("{0} server is currently unavailable. {1} {2}", indexer.Name, url, webException.Message); + _logger.Warn("{0} server is currently unavailable. {1} {2}", indexer.Name, url, + webException.Message); } else { _logger.Warn("{0} {1} {2}", indexer.Name, url, webException.Message); } } + catch (ApiKeyException) + { + _logger.Warn("Invalid API Key for {0} {1}", indexer.Name, url); + } catch (Exception feedEx) { feedEx.Data.Add("FeedUrl", url); diff --git a/NzbDrone.Core/Indexers/Newznab/NewznabException.cs b/NzbDrone.Core/Indexers/Newznab/NewznabException.cs new file mode 100644 index 000000000..df858ac24 --- /dev/null +++ b/NzbDrone.Core/Indexers/Newznab/NewznabException.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Indexers.Newznab +{ + public class NewznabException : NzbDroneException + { + public NewznabException(string message, params object[] args) : base(message, args) + { + } + + public NewznabException(string message) : base(message) + { + } + } +} diff --git a/NzbDrone.Core/Indexers/Newznab/NewznabParser.cs b/NzbDrone.Core/Indexers/Newznab/NewznabParser.cs index fc9e54d93..bd8096877 100644 --- a/NzbDrone.Core/Indexers/Newznab/NewznabParser.cs +++ b/NzbDrone.Core/Indexers/Newznab/NewznabParser.cs @@ -1,6 +1,8 @@ using System; using System.Linq; +using System.Xml; using System.Xml.Linq; +using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Indexers.Newznab @@ -46,5 +48,10 @@ namespace NzbDrone.Core.Indexers.Newznab return currentResult; } + + protected override void PreProcess(string source, string url) + { + NewznabPreProcessor.Process(source, url); + } } } \ No newline at end of file diff --git a/NzbDrone.Core/Indexers/Newznab/NewznabPreProcessor.cs b/NzbDrone.Core/Indexers/Newznab/NewznabPreProcessor.cs new file mode 100644 index 000000000..3977a2c75 --- /dev/null +++ b/NzbDrone.Core/Indexers/Newznab/NewznabPreProcessor.cs @@ -0,0 +1,24 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using NzbDrone.Core.Indexers.Exceptions; + +namespace NzbDrone.Core.Indexers.Newznab +{ + public static class NewznabPreProcessor + { + public static void Process(string source, string url) + { + var xdoc = XDocument.Parse(source); + var error = xdoc.Descendants("error").FirstOrDefault(); + + if (error == null) return; + + var code = Convert.ToInt32(error.Attribute("code").Value); + + if (code >= 100 && code <= 199) throw new ApiKeyException("Invalid API key: {0}"); + + throw new NewznabException("Newznab error detected: {0}", error.Attribute("description").Value); + } + } +} diff --git a/NzbDrone.Core/Indexers/NewznabTestService.cs b/NzbDrone.Core/Indexers/NewznabTestService.cs index dd5fea38c..c56b9b464 100644 --- a/NzbDrone.Core/Indexers/NewznabTestService.cs +++ b/NzbDrone.Core/Indexers/NewznabTestService.cs @@ -5,6 +5,8 @@ using FluentValidation; using FluentValidation.Results; using NLog; using NzbDrone.Common; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.Indexers.Newznab; namespace NzbDrone.Core.Indexers { @@ -34,21 +36,25 @@ namespace NzbDrone.Core.Indexers try { - _httpProvider.DownloadString(indexer.RecentFeed.First()); + var url = indexer.RecentFeed.First(); + var xml = _httpProvider.DownloadString(url); + + NewznabPreProcessor.Process(xml, url); } + catch (ApiKeyException apiKeyException) + { + _logger.Warn("Indexer returned result for Newznab RSS URL, API Key appears to be invalid"); - catch (Exception) + var apiKeyFailure = new ValidationFailure("ApiKey", "Invalid API Key"); + throw new ValidationException(new List { apiKeyFailure }.ToArray()); + } + catch (Exception ex) { - _logger.Warn("No result returned from RSS Feed, please confirm you're using a newznab indexer"); + _logger.Warn("Indexer doesn't appear to be Newznab based"); var failure = new ValidationFailure("Url", "Invalid Newznab URL entered"); throw new ValidationException(new List { failure }.ToArray()); } - - _logger.Warn("Indexer returned result for Newznab RSS URL, API Key appears to be invalid"); - - var apiKeyFailure = new ValidationFailure("ApiKey", "Invalid API Key"); - throw new ValidationException(new List { apiKeyFailure }.ToArray()); } } } diff --git a/NzbDrone.Core/Indexers/RssParserBase.cs b/NzbDrone.Core/Indexers/RssParserBase.cs index ca4bf91f8..7eae9a117 100644 --- a/NzbDrone.Core/Indexers/RssParserBase.cs +++ b/NzbDrone.Core/Indexers/RssParserBase.cs @@ -29,6 +29,8 @@ namespace NzbDrone.Core.Indexers public IEnumerable Process(string xml, string url) { + PreProcess(xml, url); + using (var xmlTextReader = XmlReader.Create(new StringReader(xml), new XmlReaderSettings { ProhibitDtd = false, IgnoreComments = true })) { @@ -103,6 +105,10 @@ namespace NzbDrone.Core.Indexers protected abstract long GetSize(XElement item); + protected virtual void PreProcess(string source, string url) + { + } + protected virtual ReleaseInfo PostProcessor(XElement item, ReleaseInfo currentResult) { return currentResult; diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 205b6c817..34bc316bb 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -234,6 +234,7 @@ + @@ -241,6 +242,8 @@ + + diff --git a/UI/Settings/Indexers/EditView.js b/UI/Settings/Indexers/EditView.js index 0e4f227f2..4d0defee0 100644 --- a/UI/Settings/Indexers/EditView.js +++ b/UI/Settings/Indexers/EditView.js @@ -36,7 +36,7 @@ define( App.vent.trigger(App.Commands.CloseModalCommand); }); - promise.always(function () { + promise.fail(function () { self.ui.activity.empty(); }); } @@ -63,7 +63,7 @@ define( }); }); - promise.always(function () { + promise.fail(function () { self.ui.activity.empty(); }); } From 90001b1a3ba8140752edc42e0f24239474e998e2 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 27 Sep 2013 11:09:53 -0700 Subject: [PATCH 24/34] unauthorized.xml --- .../Files/Indexers/Newznab/{error.xml => unauthorized.xml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename NzbDrone.Core.Test/Files/Indexers/Newznab/{error.xml => unauthorized.xml} (100%) diff --git a/NzbDrone.Core.Test/Files/Indexers/Newznab/error.xml b/NzbDrone.Core.Test/Files/Indexers/Newznab/unauthorized.xml similarity index 100% rename from NzbDrone.Core.Test/Files/Indexers/Newznab/error.xml rename to NzbDrone.Core.Test/Files/Indexers/Newznab/unauthorized.xml From de556764bdee43fabf5082ada1ffc14ba8aeee80 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 28 Sep 2013 11:48:30 -0700 Subject: [PATCH 25/34] Changelog is now available in the UI New: Added changelog to UI --- Gruntfile.js | 1 + NzbDrone.Api/Update/UpdateModule.cs | 20 +++++-- NzbDrone.Core.Test/NzbDrone.Core.Test.csproj | 2 +- NzbDrone.Core/NzbDrone.Core.csproj | 2 + NzbDrone.Core/Update/RecentUpdateProvider.cs | 32 ++++++++++ NzbDrone.Core/Update/UpdateChanges.cs | 17 ++++++ NzbDrone.Core/Update/UpdatePackage.cs | 2 + NzbDrone.Core/Update/UpdatePackageProvider.cs | 16 +++++ UI/.idea/runConfigurations/Debug___Chrome.xml | 38 ++++++------ .../runConfigurations/Debug___Firefox.xml | 38 ++++++------ UI/Controller.js | 8 ++- UI/Router.js | 1 + UI/System/Layout.js | 6 +- UI/Update/UpdateCollection.js | 11 ++++ UI/Update/UpdateCollectionView.js | 10 ++++ UI/Update/UpdateItemView.js | 11 ++++ UI/Update/UpdateItemViewTemplate.html | 23 ++++++++ UI/Update/UpdateLayout.js | 58 +++++++++++++++++++ UI/Update/UpdateLayoutTemplate.html | 6 ++ UI/Update/UpdateModel.js | 9 +++ UI/Update/update.less | 25 ++++++++ UI/index.html | 1 + 22 files changed, 285 insertions(+), 52 deletions(-) create mode 100644 NzbDrone.Core/Update/RecentUpdateProvider.cs create mode 100644 NzbDrone.Core/Update/UpdateChanges.cs create mode 100644 UI/Update/UpdateCollection.js create mode 100644 UI/Update/UpdateCollectionView.js create mode 100644 UI/Update/UpdateItemView.js create mode 100644 UI/Update/UpdateItemViewTemplate.html create mode 100644 UI/Update/UpdateLayout.js create mode 100644 UI/Update/UpdateLayoutTemplate.html create mode 100644 UI/Update/UpdateModel.js create mode 100644 UI/Update/update.less diff --git a/Gruntfile.js b/Gruntfile.js index d78fac4da..7025f8c6b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -73,6 +73,7 @@ module.exports = function (grunt) { 'UI/Cells/cells.less', 'UI/Logs/logs.less', 'UI/Settings/settings.less', + 'UI/Update/update.less' ], dest : outputRoot, ext: '.css' diff --git a/NzbDrone.Api/Update/UpdateModule.cs b/NzbDrone.Api/Update/UpdateModule.cs index fbe13bd8e..e4a8cbf3a 100644 --- a/NzbDrone.Api/Update/UpdateModule.cs +++ b/NzbDrone.Api/Update/UpdateModule.cs @@ -10,25 +10,33 @@ namespace NzbDrone.Api.Update public class UpdateModule : NzbDroneRestModule { private readonly ICheckUpdateService _checkUpdateService; + private readonly IRecentUpdateProvider _recentUpdateProvider; - public UpdateModule(ICheckUpdateService checkUpdateService) + public UpdateModule(ICheckUpdateService checkUpdateService, + IRecentUpdateProvider recentUpdateProvider) { _checkUpdateService = checkUpdateService; - GetResourceAll = GetAvailableUpdate; + _recentUpdateProvider = recentUpdateProvider; + GetResourceAll = GetRecentUpdates; } - private List GetAvailableUpdate() + private UpdateResource GetAvailableUpdate() { var update = _checkUpdateService.AvailableUpdate(); - var response = new List(); + var response = new UpdateResource(); if (update != null) { - response.Add(update.InjectTo()); + return update.InjectTo(); } return response; } + + private List GetRecentUpdates() + { + return ToListResource(_recentUpdateProvider.GetRecentUpdatePackages); + } } public class UpdateResource : RestResource @@ -40,5 +48,7 @@ namespace NzbDrone.Api.Update public DateTime ReleaseDate { get; set; } public String FileName { get; set; } public String Url { get; set; } + + public UpdateChanges Changes { get; set; } } } \ No newline at end of file diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 99987c84b..58b09e81e 100644 --- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -252,7 +252,7 @@ Always - + Always diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 34bc316bb..23d3657e3 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -550,6 +550,8 @@ + + diff --git a/NzbDrone.Core/Update/RecentUpdateProvider.cs b/NzbDrone.Core/Update/RecentUpdateProvider.cs new file mode 100644 index 000000000..feee0d34f --- /dev/null +++ b/NzbDrone.Core/Update/RecentUpdateProvider.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Core.Update +{ + public interface IRecentUpdateProvider + { + List GetRecentUpdatePackages(); + } + + public class RecentUpdateProvider : IRecentUpdateProvider + { + private readonly IConfigFileProvider _configFileProvider; + private readonly IUpdatePackageProvider _updatePackageProvider; + + public RecentUpdateProvider(IConfigFileProvider configFileProvider, + IUpdatePackageProvider updatePackageProvider) + { + _configFileProvider = configFileProvider; + _updatePackageProvider = updatePackageProvider; + } + + public List GetRecentUpdatePackages() + { + var branch = _configFileProvider.Branch; + return _updatePackageProvider.GetRecentUpdates(branch); + } + } +} diff --git a/NzbDrone.Core/Update/UpdateChanges.cs b/NzbDrone.Core/Update/UpdateChanges.cs new file mode 100644 index 000000000..a26bba93b --- /dev/null +++ b/NzbDrone.Core/Update/UpdateChanges.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.Update +{ + public class UpdateChanges + { + public List New { get; set; } + public List Fixed { get; set; } + + public UpdateChanges() + { + New = new List(); + Fixed = new List(); + } + } +} diff --git a/NzbDrone.Core/Update/UpdatePackage.cs b/NzbDrone.Core/Update/UpdatePackage.cs index f94a77e4b..f8159686a 100644 --- a/NzbDrone.Core/Update/UpdatePackage.cs +++ b/NzbDrone.Core/Update/UpdatePackage.cs @@ -13,5 +13,7 @@ namespace NzbDrone.Core.Update public DateTime ReleaseDate { get; set; } public String FileName { get; set; } public String Url { get; set; } + + public UpdateChanges Changes { get; set; } } } diff --git a/NzbDrone.Core/Update/UpdatePackageProvider.cs b/NzbDrone.Core/Update/UpdatePackageProvider.cs index c6b4b64c0..29a9073df 100644 --- a/NzbDrone.Core/Update/UpdatePackageProvider.cs +++ b/NzbDrone.Core/Update/UpdatePackageProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using NzbDrone.Common; using RestSharp; using NzbDrone.Core.Rest; @@ -8,6 +9,7 @@ namespace NzbDrone.Core.Update public interface IUpdatePackageProvider { UpdatePackage GetLatestUpdate(string branch, Version currentVersion); + List GetRecentUpdates(string branch); } public class UpdatePackageProvider : IUpdatePackageProvider @@ -27,5 +29,19 @@ namespace NzbDrone.Core.Update return update.UpdatePackage; } + + public List GetRecentUpdates(string branch) + { + var restClient = new RestClient(Services.RootUrl); + + var request = new RestRequest("/v1/update/{branch}/all"); + + request.AddUrlSegment("branch", branch); + request.AddParameter("limit", 5); + + var updates = restClient.ExecuteAndValidate>(request); + + return updates; + } } } \ No newline at end of file diff --git a/UI/.idea/runConfigurations/Debug___Chrome.xml b/UI/.idea/runConfigurations/Debug___Chrome.xml index 2323f096d..82eb4863d 100644 --- a/UI/.idea/runConfigurations/Debug___Chrome.xml +++ b/UI/.idea/runConfigurations/Debug___Chrome.xml @@ -1,25 +1,21 @@ - - - + + + + + + + + + + + + + + + + + diff --git a/UI/.idea/runConfigurations/Debug___Firefox.xml b/UI/.idea/runConfigurations/Debug___Firefox.xml index 1f0cbd78b..2e020afbc 100644 --- a/UI/.idea/runConfigurations/Debug___Firefox.xml +++ b/UI/.idea/runConfigurations/Debug___Firefox.xml @@ -1,25 +1,21 @@ - - - + + + + + + + + + + + + + + + + + diff --git a/UI/Controller.js b/UI/Controller.js index 8f0c059c8..a4bc4d1f9 100644 --- a/UI/Controller.js +++ b/UI/Controller.js @@ -16,10 +16,11 @@ define( 'Release/Layout', 'System/Layout', 'SeasonPass/SeasonPassLayout', + 'Update/UpdateLayout', 'Shared/NotFoundView', 'Shared/Modal/Region' ], function (App, Marionette, HistoryLayout, SettingsLayout, AddSeriesLayout, SeriesIndexLayout, SeriesDetailsLayout, SeriesCollection, MissingLayout, CalendarLayout, - LogsLayout, LogFileLayout, ReleaseLayout, SystemLayout, SeasonPassLayout, NotFoundView) { + LogsLayout, LogFileLayout, ReleaseLayout, SystemLayout, SeasonPassLayout, UpdateLayout, NotFoundView) { return Marionette.Controller.extend({ series: function () { @@ -94,6 +95,11 @@ define( App.mainRegion.show(new SeasonPassLayout()); }, + update: function () { + this._setTitle('Updates'); + App.mainRegion.show(new UpdateLayout()); + }, + notFound: function () { this._setTitle('Not Found'); App.mainRegion.show(new NotFoundView(this)); diff --git a/UI/Router.js b/UI/Router.js index de2111644..22c13b04f 100644 --- a/UI/Router.js +++ b/UI/Router.js @@ -31,6 +31,7 @@ require( 'rss' : 'rss', 'system' : 'system', 'seasonpass' : 'seasonPass', + 'update' : 'update', ':whatever' : 'notFound' } }); diff --git a/UI/System/Layout.js b/UI/System/Layout.js index 390ce48d8..5f4778c59 100644 --- a/UI/System/Layout.js +++ b/UI/System/Layout.js @@ -33,9 +33,9 @@ define( route: 'logs' }, { - title : 'Check for Update', - icon : 'icon-nd-update', - command: 'applicationUpdate' + title : 'Updates', + icon : 'icon-upload-alt', + route : 'update' } ] }, diff --git a/UI/Update/UpdateCollection.js b/UI/Update/UpdateCollection.js new file mode 100644 index 000000000..b1e7bcf60 --- /dev/null +++ b/UI/Update/UpdateCollection.js @@ -0,0 +1,11 @@ +'use strict'; +define( + [ + 'backbone', + 'Update/UpdateModel' + ], function (Backbone, UpdateModel) { + return Backbone.Collection.extend({ + url : window.NzbDrone.ApiRoot + '/update', + model: UpdateModel + }); + }); diff --git a/UI/Update/UpdateCollectionView.js b/UI/Update/UpdateCollectionView.js new file mode 100644 index 000000000..267af7f85 --- /dev/null +++ b/UI/Update/UpdateCollectionView.js @@ -0,0 +1,10 @@ +'use strict'; +define( + [ + 'marionette', + 'Update/UpdateItemView' + ], function (Marionette, UpdateItemView) { + return Marionette.CollectionView.extend({ + itemView: UpdateItemView + }); + }); diff --git a/UI/Update/UpdateItemView.js b/UI/Update/UpdateItemView.js new file mode 100644 index 000000000..8d478f038 --- /dev/null +++ b/UI/Update/UpdateItemView.js @@ -0,0 +1,11 @@ +'use strict'; + +define( + [ + 'app', + 'marionette' + ], function (App, Marionette) { + return Marionette.ItemView.extend({ + template: 'Update/UpdateItemViewTemplate' + }); + }); diff --git a/UI/Update/UpdateItemViewTemplate.html b/UI/Update/UpdateItemViewTemplate.html new file mode 100644 index 000000000..f932fa618 --- /dev/null +++ b/UI/Update/UpdateItemViewTemplate.html @@ -0,0 +1,23 @@ +
    +
    + {{version}} - {{ShortDate releaseDate}} + + {{#with changes}} + {{#each new}} +
    + New {{this}} +
    + {{/each}} + + {{#each fixed}} +
    + Fixed {{this}} +
    + {{/each}} + {{/with}} + + {{#unless changes}} + No notable changes + {{/unless}} +
    +
    diff --git a/UI/Update/UpdateLayout.js b/UI/Update/UpdateLayout.js new file mode 100644 index 000000000..4794f761c --- /dev/null +++ b/UI/Update/UpdateLayout.js @@ -0,0 +1,58 @@ +'use strict'; +define( + [ + 'marionette', + 'backgrid', + 'Update/UpdateCollection', + 'Update/UpdateCollectionView', + 'Shared/Toolbar/ToolbarLayout', + 'Shared/LoadingView' + ], function (Marionette, Backgrid, UpdateCollection, UpdateCollectionView, ToolbarLayout, LoadingView) { + return Marionette.Layout.extend({ + template: 'Update/UpdateLayoutTemplate', + + regions: { + updates: '#x-updates', + toolbar: '#x-toolbar' + }, + + leftSideButtons: { + type : 'default', + storeState: false, + items : + [ + { + title : 'Check for Update', + icon : 'icon-nd-update', + command: 'applicationUpdate' + } + ] + }, + + initialize: function () { + this.updateCollection = new UpdateCollection(); + }, + + onRender: function () { + this.updates.show(new LoadingView()); + this._showToolbar(); + + var self = this; + var promise = this.updateCollection.fetch(); + + promise.done(function (){ + self.updates.show(new UpdateCollectionView({ collection: self.updateCollection })); + }); + }, + + _showToolbar: function () { + this.toolbar.show(new ToolbarLayout({ + left : + [ + this.leftSideButtons + ], + context: this + })); + } + }); + }); diff --git a/UI/Update/UpdateLayoutTemplate.html b/UI/Update/UpdateLayoutTemplate.html new file mode 100644 index 000000000..c405cb562 --- /dev/null +++ b/UI/Update/UpdateLayoutTemplate.html @@ -0,0 +1,6 @@ +
    +
    +
    +
    +
    +
    diff --git a/UI/Update/UpdateModel.js b/UI/Update/UpdateModel.js new file mode 100644 index 000000000..530a080c6 --- /dev/null +++ b/UI/Update/UpdateModel.js @@ -0,0 +1,9 @@ +'use strict'; +define( + [ + 'backbone' + ], function (Backbone) { + return Backbone.Model.extend({ + + }); + }); diff --git a/UI/Update/update.less b/UI/Update/update.less new file mode 100644 index 000000000..e25b23b70 --- /dev/null +++ b/UI/Update/update.less @@ -0,0 +1,25 @@ +.update { + margin-bottom: 30px; + + legend { + margin-bottom: 5px; + line-height: 30px; + + .date { + font-size: 16px; + } + } + + .changes-header { + font-size: 18px; + } + + .label { + width: 40px; + text-align: center; + } + .change { + margin-bottom: 2px; + font-size: 13px; + } +} \ No newline at end of file diff --git a/UI/index.html b/UI/index.html index c2b0897e7..093473a8a 100644 --- a/UI/index.html +++ b/UI/index.html @@ -15,6 +15,7 @@ + From f9384d48dd8eefdad9a25cb367339b2624a6ddfb Mon Sep 17 00:00:00 2001 From: Eric Yen Date: Sat, 28 Sep 2013 11:05:40 -0400 Subject: [PATCH 26/34] Implement https://trello.com/c/a1il1sTd modified: NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs -modified: DiskProvider.cs added SetFolderAccessTime to the interface added SetFolderAccessTime function -modified MoveEpisodeFile.cs call SetFolderAccessTime from both MoveEpisodeFile --- NzbDrone.Common/DiskProvider.cs | 5 +++++ NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/NzbDrone.Common/DiskProvider.cs b/NzbDrone.Common/DiskProvider.cs index 56180cdda..e83604017 100644 --- a/NzbDrone.Common/DiskProvider.cs +++ b/NzbDrone.Common/DiskProvider.cs @@ -39,6 +39,7 @@ namespace NzbDrone.Common string GetPathRoot(string path); void SetPermissions(string filename, WellKnownSidType accountSid, FileSystemRights rights, AccessControlType controlType); bool IsParent(string parentPath, string childPath); + void SetFolderAccessTime(string path, DateTime time){ FileAttributes GetFileAttributes(string path); void EmptyFolder(string path); } @@ -451,6 +452,10 @@ namespace NzbDrone.Common } } + private void SetFolderAccessTime(string path, DateTime time){ + Directory.SetLastWriteTimeUtc(path,time); + } + public FileAttributes GetFileAttributes(string path) { return File.GetAttributes(path); diff --git a/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index f14a0ae44..afa34a1c1 100644 --- a/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -43,6 +43,8 @@ namespace NzbDrone.Core.MediaFiles var newFileName = _buildFileNames.BuildFilename(episodes, series, episodeFile); var filePath = _buildFileNames.BuildFilePath(series, episodes.First().SeasonNumber, newFileName, Path.GetExtension(episodeFile.Path)); MoveFile(episodeFile, filePath); + _diskProvider.SetFolderAccessTime( Path.GetDirectoryName(filePath), episodeFile.DateAdded); + _diskProvider.SetFolderAccessTime( series.Path, episodeFile.DateAdded); return filePath; } @@ -52,6 +54,8 @@ namespace NzbDrone.Core.MediaFiles var newFileName = _buildFileNames.BuildFilename(localEpisode.Episodes, localEpisode.Series, episodeFile); var filePath = _buildFileNames.BuildFilePath(localEpisode.Series, localEpisode.SeasonNumber, newFileName, Path.GetExtension(episodeFile.Path)); MoveFile(episodeFile, filePath); + _diskProvider.SetFolderAccessTime( Path.GetDirectoryName(filePath), episodeFile.DateAdded); + _diskProvider.SetFolderAccessTime( localEpisode.Series.Path, episodeFile.DateAdded); return filePath; } From 88afc197165ea45ab1a283d2bb66d25d9cbbb839 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 28 Sep 2013 15:25:22 -0700 Subject: [PATCH 27/34] Fixed LastWriteTime --- NzbDrone.Common/DiskProvider.cs | 10 ++++---- .../MediaFiles/EpisodeFileMovingService.cs | 23 ++++++++++++------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/NzbDrone.Common/DiskProvider.cs b/NzbDrone.Common/DiskProvider.cs index e83604017..5860d95cd 100644 --- a/NzbDrone.Common/DiskProvider.cs +++ b/NzbDrone.Common/DiskProvider.cs @@ -39,7 +39,7 @@ namespace NzbDrone.Common string GetPathRoot(string path); void SetPermissions(string filename, WellKnownSidType accountSid, FileSystemRights rights, AccessControlType controlType); bool IsParent(string parentPath, string childPath); - void SetFolderAccessTime(string path, DateTime time){ + void SetFolderWriteTime(string path, DateTime time); FileAttributes GetFileAttributes(string path); void EmptyFolder(string path); } @@ -442,6 +442,10 @@ namespace NzbDrone.Common return false; } + public void SetFolderWriteTime(string path, DateTime time) + { + Directory.SetLastWriteTimeUtc(path, time); + } private static void RemoveReadOnly(string path) { @@ -452,10 +456,6 @@ namespace NzbDrone.Common } } - private void SetFolderAccessTime(string path, DateTime time){ - Directory.SetLastWriteTimeUtc(path,time); - } - public FileAttributes GetFileAttributes(string path) { return File.GetAttributes(path); diff --git a/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index afa34a1c1..d347f3654 100644 --- a/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -42,9 +42,7 @@ namespace NzbDrone.Core.MediaFiles var episodes = _episodeService.GetEpisodesByFileId(episodeFile.Id); var newFileName = _buildFileNames.BuildFilename(episodes, series, episodeFile); var filePath = _buildFileNames.BuildFilePath(series, episodes.First().SeasonNumber, newFileName, Path.GetExtension(episodeFile.Path)); - MoveFile(episodeFile, filePath); - _diskProvider.SetFolderAccessTime( Path.GetDirectoryName(filePath), episodeFile.DateAdded); - _diskProvider.SetFolderAccessTime( series.Path, episodeFile.DateAdded); + MoveFile(episodeFile, series, filePath); return filePath; } @@ -53,14 +51,12 @@ namespace NzbDrone.Core.MediaFiles { var newFileName = _buildFileNames.BuildFilename(localEpisode.Episodes, localEpisode.Series, episodeFile); var filePath = _buildFileNames.BuildFilePath(localEpisode.Series, localEpisode.SeasonNumber, newFileName, Path.GetExtension(episodeFile.Path)); - MoveFile(episodeFile, filePath); - _diskProvider.SetFolderAccessTime( Path.GetDirectoryName(filePath), episodeFile.DateAdded); - _diskProvider.SetFolderAccessTime( localEpisode.Series.Path, episodeFile.DateAdded); - + MoveFile(episodeFile, localEpisode.Series, filePath); + return filePath; } - private void MoveFile(EpisodeFile episodeFile, string destinationFilename) + private void MoveFile(EpisodeFile episodeFile, Series series, string destinationFilename) { if (!_diskProvider.FileExists(episodeFile.Path)) { @@ -77,6 +73,17 @@ namespace NzbDrone.Core.MediaFiles _logger.Debug("Moving [{0}] > [{1}]", episodeFile.Path, destinationFilename); _diskProvider.MoveFile(episodeFile.Path, destinationFilename); + _logger.Trace("Setting last write time on series folder: {0}", series.Path); + _diskProvider.SetFolderWriteTime(series.Path, episodeFile.DateAdded); + + if (series.SeasonFolder) + { + var seasonFolder = Path.GetDirectoryName(episodeFile.Path); + + _logger.Trace("Setting last write time on season folder: {0}", seasonFolder); + _diskProvider.SetFolderWriteTime(seasonFolder, episodeFile.DateAdded); + } + //Wrapped in Try/Catch to prevent this from causing issues with remote NAS boxes, the move worked, which is more important. try { From 1e0c053bd30d219b3c64c08e34ee9555da53dc17 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 28 Sep 2013 20:29:05 -0700 Subject: [PATCH 28/34] Set write time on destination season folder - oops --- NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index d347f3654..e4e58c6ed 100644 --- a/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -78,7 +78,7 @@ namespace NzbDrone.Core.MediaFiles if (series.SeasonFolder) { - var seasonFolder = Path.GetDirectoryName(episodeFile.Path); + var seasonFolder = Path.GetDirectoryName(destinationFilename); _logger.Trace("Setting last write time on season folder: {0}", seasonFolder); _diskProvider.SetFolderWriteTime(seasonFolder, episodeFile.DateAdded); From 0ee82feab7e33b6aece660deeae769285684df5b Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 28 Sep 2013 21:17:20 -0700 Subject: [PATCH 29/34] Recycling bin setting is available on Media Management (advanced) New: Optional recycling bin on Media Management (advanced) --- .../FileManagement/FileManagementView.js | 13 +++++++++++-- .../FileManagement/FileManagementViewTemplate.html | 11 +++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/UI/Settings/MediaManagement/FileManagement/FileManagementView.js b/UI/Settings/MediaManagement/FileManagement/FileManagementView.js index 314a7b79e..af28961b3 100644 --- a/UI/Settings/MediaManagement/FileManagement/FileManagementView.js +++ b/UI/Settings/MediaManagement/FileManagement/FileManagementView.js @@ -2,11 +2,20 @@ define( [ 'marionette', - 'Mixins/AsModelBoundView' + 'Mixins/AsModelBoundView', + 'Mixins/AutoComplete' ], function (Marionette, AsModelBoundView) { var view = Marionette.ItemView.extend({ - template: 'Settings/MediaManagement/FileManagement/FileManagementViewTemplate' + template: 'Settings/MediaManagement/FileManagement/FileManagementViewTemplate', + + ui: { + recyclingBin: '.x-path' + }, + + onShow: function () { + this.ui.recyclingBin.autoComplete('/directories'); + } }); return AsModelBoundView.call(view); diff --git a/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html b/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html index 95b53a1a2..02938c4e0 100644 --- a/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html +++ b/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html @@ -40,4 +40,15 @@
    + +
    + + +
    + + + + +
    +
    From 5d8bc50c773f81bbc1dc649fa321f99fc43f0a1d Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 28 Sep 2013 23:29:52 -0700 Subject: [PATCH 30/34] Add new series will clear results and re-focus search box --- .../{Collection.js => AddSeriesCollection.js} | 0 UI/AddSeries/AddSeriesLayout.js | 5 +- ...late.html => AddSeriesLayoutTemplate.html} | 0 UI/AddSeries/AddSeriesView.js | 46 +++++++++++-------- ...mplate.html => AddSeriesViewTemplate.html} | 0 UI/AddSeries/Existing/CollectionView.js | 4 +- UI/AddSeries/SearchResultCollectionView.js | 3 +- UI/AddSeries/SearchResultView.js | 31 +++++++------ 8 files changed, 49 insertions(+), 40 deletions(-) rename UI/AddSeries/{Collection.js => AddSeriesCollection.js} (100%) rename UI/AddSeries/{addSeriesLayoutTemplate.html => AddSeriesLayoutTemplate.html} (100%) rename UI/AddSeries/{AddSeriesTemplate.html => AddSeriesViewTemplate.html} (100%) diff --git a/UI/AddSeries/Collection.js b/UI/AddSeries/AddSeriesCollection.js similarity index 100% rename from UI/AddSeries/Collection.js rename to UI/AddSeries/AddSeriesCollection.js diff --git a/UI/AddSeries/AddSeriesLayout.js b/UI/AddSeries/AddSeriesLayout.js index ca577de9b..cc3c88486 100644 --- a/UI/AddSeries/AddSeriesLayout.js +++ b/UI/AddSeries/AddSeriesLayout.js @@ -15,8 +15,7 @@ define( ExistingSeriesCollectionView, AddSeriesView, QualityProfileCollection, - RootFolderCollection, - SeriesCollection) { + RootFolderCollection) { return Marionette.Layout.extend({ template: 'AddSeries/AddSeriesLayoutTemplate', @@ -35,8 +34,6 @@ define( }, initialize: function () { - - SeriesCollection.fetch(); QualityProfileCollection.fetch(); RootFolderCollection.promise = RootFolderCollection.fetch(); }, diff --git a/UI/AddSeries/addSeriesLayoutTemplate.html b/UI/AddSeries/AddSeriesLayoutTemplate.html similarity index 100% rename from UI/AddSeries/addSeriesLayoutTemplate.html rename to UI/AddSeries/AddSeriesLayoutTemplate.html diff --git a/UI/AddSeries/AddSeriesView.js b/UI/AddSeries/AddSeriesView.js index e66b1e891..957b2cabc 100644 --- a/UI/AddSeries/AddSeriesView.js +++ b/UI/AddSeries/AddSeriesView.js @@ -3,14 +3,14 @@ define( [ 'app', 'marionette', - 'AddSeries/Collection', + 'AddSeries/AddSeriesCollection', 'AddSeries/SearchResultCollectionView', 'AddSeries/NotFoundView', 'Shared/LoadingView', 'underscore' ], function (App, Marionette, AddSeriesCollection, SearchResultCollectionView, NotFoundView, LoadingView, _) { return Marionette.Layout.extend({ - template: 'AddSeries/AddSeriesTemplate', + template: 'AddSeries/AddSeriesViewTemplate', regions: { searchResult: '#search-result' @@ -36,12 +36,12 @@ define( if (this.isExisting) { this.className = 'existing-series'; - this.listenTo(App.vent, App.Events.SeriesAdded, this._onSeriesAdded); } else { this.className = 'new-series'; } + this.listenTo(App.vent, App.Events.SeriesAdded, this._onSeriesAdded); this.listenTo(this.collection, 'sync', this._showResults); this.resultCollectionView = new SearchResultCollectionView({ @@ -52,21 +52,6 @@ define( this.throttledSearch = _.debounce(this.search, 1000, {trailing: true}).bind(this); }, - _onSeriesAdded: function (options) { - if (this.isExisting && options.series.get('path') === this.model.get('folder').path) { - this.close(); - } - }, - - _onLoadMore: function () { - var showingAll = this.resultCollectionView.showMore(); - this.ui.searchBar.show(); - - if (showingAll) { - this.ui.loadMore.hide(); - } - }, - onRender: function () { var self = this; @@ -77,7 +62,7 @@ define( self._abortExistingSearch(); self.throttledSearch({ term: self.ui.seriesSearch.val() - }) + }); }); if (this.isExisting) { @@ -87,6 +72,7 @@ define( onShow: function () { this.searchResult.show(this.resultCollectionView); + this.ui.seriesSearch.focus(); }, search: function (options) { @@ -106,6 +92,28 @@ define( return this.currentSearchPromise; }, + _onSeriesAdded: function (options) { + if (this.isExisting && options.series.get('path') === this.model.get('folder').path) { + this.close(); + } + + else if (!this.isExisting) { + this.collection.reset(); + this.searchResult.show(this.resultCollectionView); + this.ui.seriesSearch.val(''); + this.ui.seriesSearch.focus(); + } + }, + + _onLoadMore: function () { + var showingAll = this.resultCollectionView.showMore(); + this.ui.searchBar.show(); + + if (showingAll) { + this.ui.loadMore.hide(); + } + }, + _showResults: function () { if (!this.isClosed) { diff --git a/UI/AddSeries/AddSeriesTemplate.html b/UI/AddSeries/AddSeriesViewTemplate.html similarity index 100% rename from UI/AddSeries/AddSeriesTemplate.html rename to UI/AddSeries/AddSeriesViewTemplate.html diff --git a/UI/AddSeries/Existing/CollectionView.js b/UI/AddSeries/Existing/CollectionView.js index 74108d33d..849d19fd5 100644 --- a/UI/AddSeries/Existing/CollectionView.js +++ b/UI/AddSeries/Existing/CollectionView.js @@ -29,9 +29,9 @@ define( this.addItemView(model, this.getItemView(), index); this.children.findByModel(model) .search({term: folderName}) - .always((function () { + .always(function () { self._showAndSearch(currentIndex + 1); - })); + }); } }, diff --git a/UI/AddSeries/SearchResultCollectionView.js b/UI/AddSeries/SearchResultCollectionView.js index e1d912f01..53d0a8936 100644 --- a/UI/AddSeries/SearchResultCollectionView.js +++ b/UI/AddSeries/SearchResultCollectionView.js @@ -2,8 +2,7 @@ define( [ 'marionette', - 'AddSeries/SearchResultView', - + 'AddSeries/SearchResultView' ], function (Marionette, SearchResultView) { return Marionette.CollectionView.extend({ diff --git a/UI/AddSeries/SearchResultView.js b/UI/AddSeries/SearchResultView.js index 3f8c6ec33..218bf0ff0 100644 --- a/UI/AddSeries/SearchResultView.js +++ b/UI/AddSeries/SearchResultView.js @@ -38,6 +38,9 @@ define( throw 'model is required'; } + this.templateHelpers = {}; + this._configureTemplateHelpers(); + this.listenTo(App.vent, Config.Events.ConfigUpdatedEvent, this._onConfigUpdated); this.listenTo(this.model, 'change', this.render); this.listenTo(RootFolders, 'all', this.render); @@ -72,22 +75,18 @@ define( }); }, - serializeData: function () { - var data = this.model.toJSON(); - + _configureTemplateHelpers: function () { var existingSeries = SeriesCollection.where({tvdbId: this.model.get('tvdbId')}); if (existingSeries.length > 0) { - data.existing = existingSeries[0].toJSON(); + this.templateHelpers.existing = existingSeries[0].toJSON(); } - data.qualityProfiles = QualityProfiles.toJSON(); + this.templateHelpers.qualityProfiles = QualityProfiles.toJSON(); - if (!data.isExisting) { - data.rootFolders = RootFolders.toJSON(); + if (!this.model.get('isExisting')) { + this.templateHelpers.rootFolders = RootFolders.toJSON(); } - - return data; }, _onConfigUpdated: function (options) { @@ -135,17 +134,23 @@ define( SeriesCollection.add(this.model); - this.model.save().done(function () { + + var promise = this.model.save(); + + promise.done(function () { self.close(); icon.removeClass('icon-spin icon-spinner disabled').addClass('icon-search'); + Messenger.show({ message: 'Added: ' + self.model.get('title') }); App.vent.trigger(App.Events.SeriesAdded, { series: self.model }); - }).fail(function () { - icon.removeClass('icon-spin icon-spinner disabled').addClass('icon-search'); - }); + }); + + promise.fail(function () { + icon.removeClass('icon-spin icon-spinner disabled').addClass('icon-search'); + }); } }); From c2129054a0b56d18783246b6d732b5b3979c0d95 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 29 Sep 2013 00:02:12 -0700 Subject: [PATCH 31/34] Stop searching for existing series when view is changed --- UI/AddSeries/AddSeriesLayout.js | 2 +- .../{CollectionView.js => AddExistingSeriesCollectionView.js} | 4 +++- UI/Commands/CommandController.js | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) rename UI/AddSeries/Existing/{CollectionView.js => AddExistingSeriesCollectionView.js} (89%) diff --git a/UI/AddSeries/AddSeriesLayout.js b/UI/AddSeries/AddSeriesLayout.js index cc3c88486..14f11c356 100644 --- a/UI/AddSeries/AddSeriesLayout.js +++ b/UI/AddSeries/AddSeriesLayout.js @@ -4,7 +4,7 @@ define( 'app', 'marionette', 'AddSeries/RootFolders/Layout', - 'AddSeries/Existing/CollectionView', + 'AddSeries/Existing/AddExistingSeriesCollectionView', 'AddSeries/AddSeriesView', 'Quality/QualityProfileCollection', 'AddSeries/RootFolders/Collection', diff --git a/UI/AddSeries/Existing/CollectionView.js b/UI/AddSeries/Existing/AddExistingSeriesCollectionView.js similarity index 89% rename from UI/AddSeries/Existing/CollectionView.js rename to UI/AddSeries/Existing/AddExistingSeriesCollectionView.js index 849d19fd5..45659ffd8 100644 --- a/UI/AddSeries/Existing/CollectionView.js +++ b/UI/AddSeries/Existing/AddExistingSeriesCollectionView.js @@ -30,7 +30,9 @@ define( this.children.findByModel(model) .search({term: folderName}) .always(function () { - self._showAndSearch(currentIndex + 1); + if (!self.isClosed) { + self._showAndSearch(currentIndex + 1); + } }); } }, diff --git a/UI/Commands/CommandController.js b/UI/Commands/CommandController.js index 650d8f67b..f859e2c51 100644 --- a/UI/Commands/CommandController.js +++ b/UI/Commands/CommandController.js @@ -52,5 +52,5 @@ define( options.element.startSpin(); } - } + }; }); From bd1a9db0ef3e5b772e71df427736d9ff4af1c45c Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 29 Sep 2013 01:48:47 -0700 Subject: [PATCH 32/34] refresh series details after rename/refresh Fixed: Refresh series details after series refresh and rename --- UI/Commands/CommandController.js | 68 ++++++++++++++---------- UI/Commands/CommandModel.js | 14 +++-- UI/Series/Details/SeriesDetailsLayout.js | 12 +++++ UI/app.js | 8 +-- 4 files changed, 64 insertions(+), 38 deletions(-) diff --git a/UI/Commands/CommandController.js b/UI/Commands/CommandController.js index f859e2c51..8e14512cf 100644 --- a/UI/Commands/CommandController.js +++ b/UI/Commands/CommandController.js @@ -1,56 +1,66 @@ 'use strict'; define( [ + 'app', 'Commands/CommandModel', 'Commands/CommandCollection', 'underscore', 'jQuery/jquery.spin' - ], function (CommandModel, CommandCollection, _) { + ], function (App, CommandModel, CommandCollection, _) { - return{ + var singleton = function () { - Execute: function (name, properties) { + return { - var attr = _.extend({name: name.toLocaleLowerCase()}, properties); + Execute: function (name, properties) { - var commandModel = new CommandModel(attr); + var attr = _.extend({name: name.toLocaleLowerCase()}, properties); - return commandModel.save().success(function () { - CommandCollection.add(commandModel); - }); - }, + var commandModel = new CommandModel(attr); - bindToCommand: function (options) { + return commandModel.save().success(function () { + CommandCollection.add(commandModel); + }); + }, - var self = this; + bindToCommand: function (options) { - var existingCommand = CommandCollection.findCommand(options.command); + var self = this; - if (existingCommand) { - this._bindToCommandModel.call(this, existingCommand, options); - } + var existingCommand = CommandCollection.findCommand(options.command); - CommandCollection.bind('add sync', function (model) { - if (model.isSameCommand(options.command)) { - self._bindToCommandModel.call(self, model, options); + if (existingCommand) { + this._bindToCommandModel.call(this, existingCommand, options); } - }); - }, - _bindToCommandModel: function bindToCommand(model, options) { + CommandCollection.bind('add sync', function (model) { + if (model.isSameCommand(options.command)) { + self._bindToCommandModel.call(self, model, options); + } + }); + }, - if (!model.isActive()) { - options.element.stopSpin(); - return; - } + _bindToCommandModel: function bindToCommand(model, options) { - model.bind('change:state', function (model) { if (!model.isActive()) { options.element.stopSpin(); + return; } - }); - options.element.startSpin(); - } + model.bind('change:state', function (model) { + if (!model.isActive()) { + options.element.stopSpin(); + + if (model.isComplete()) { + App.vent.trigger(App.Events.CommandComplete, { command: model, model: options.model }); + } + } + }); + + options.element.startSpin(); + } + }; }; + + return singleton(); }); diff --git a/UI/Commands/CommandModel.js b/UI/Commands/CommandModel.js index fcb08dd6d..cdb492ca6 100644 --- a/UI/Commands/CommandModel.js +++ b/UI/Commands/CommandModel.js @@ -11,13 +11,9 @@ define( return response; }, - isActive: function () { - return this.get('state') !== 'completed' && this.get('state') !== 'failed'; - }, - isSameCommand: function (command) { - if (command.name.toLocaleLowerCase() != this.get('name').toLocaleLowerCase()) { + if (command.name.toLocaleLowerCase() !== this.get('name').toLocaleLowerCase()) { return false; } @@ -28,6 +24,14 @@ define( } return true; + }, + + isActive: function () { + return this.get('state') !== 'completed' && this.get('state') !== 'failed'; + }, + + isComplete: function () { + return this.get('state') === 'completed'; } }); }); diff --git a/UI/Series/Details/SeriesDetailsLayout.js b/UI/Series/Details/SeriesDetailsLayout.js index 238ffb00c..d08abac58 100644 --- a/UI/Series/Details/SeriesDetailsLayout.js +++ b/UI/Series/Details/SeriesDetailsLayout.js @@ -44,6 +44,8 @@ define( this.listenTo(this.model, 'change:monitored', this._setMonitoredState); this.listenTo(App.vent, App.Events.SeriesDeleted, this._onSeriesDeleted); this.listenTo(App.vent, App.Events.SeasonRenamed, this._onSeasonRenamed); + + App.vent.on(App.Events.CommandComplete, this._commandComplete, this); }, onShow: function () { @@ -195,6 +197,16 @@ define( if (this.model.get('id') === event.series.get('id')) { this.episodeFileCollection.fetch(); } + }, + + _commandComplete: function (options) { + if (options.command.get('name') === 'refreshseries' || options.command.get('name') === 'renameseries') { + if (options.command.get('seriesId') === this.model.get('id')) { + this._showSeasons(); + this._setMonitoredState(); + this._showInfo(); + } + } } }); }); diff --git a/UI/app.js b/UI/app.js index 8f64f4790..1834d9163 100644 --- a/UI/app.js +++ b/UI/app.js @@ -25,7 +25,6 @@ require.config({ 'jquery.knob' : 'JsLibraries/jquery.knob', 'jquery.dotdotdot' : 'JsLibraries/jquery.dotdotdot', 'libs' : 'JsLibraries/' - }, shim: { @@ -191,9 +190,10 @@ define( var app = new Marionette.Application(); app.Events = { - SeriesAdded : 'series:added', - SeriesDeleted: 'series:deleted', - SeasonRenamed: 'season:renamed' + SeriesAdded : 'series:added', + SeriesDeleted : 'series:deleted', + SeasonRenamed : 'season:renamed', + CommandComplete: 'command:complete' }; app.Commands = { From 5f93cbc83b61b276f424d4a42f97ad35b9cb64c9 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 30 Sep 2013 08:38:26 -0700 Subject: [PATCH 33/34] Remove duplicate episodes from trakt before processing (by season and episode numbers) Fixed: Better handling of duplicate episodes from trakt --- .../TvTests/RefreshEpisodeServiceFixture.cs | Bin 6867 -> 7755 bytes NzbDrone.Core/Tv/RefreshEpisodeService.cs | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs b/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs index a3cd0b10a189ac5b43d080b752031cf465f5207b..b02608d8d5604848368d71d94e6f0a4dc2251bad 100644 GIT binary patch delta 300 zcmca?dfH}#ocQDbUR|T2)ZF~C)cBOrf}G6c#FEr_kN}WMEyyg+Pf0C~PfAV8FG`Is zD9TSxEiTT?OP{=vMSAjm_Q{i@MfsJTN;7j(Qj2U{A$l{Yv3GfO8rzRHX=lPZ9CZ!fB*eV#JD-eWQ zf`P0H}&)00000 delta 12 TcmX?YbJ=u*ocQKxQd^k;B&`Jy diff --git a/NzbDrone.Core/Tv/RefreshEpisodeService.cs b/NzbDrone.Core/Tv/RefreshEpisodeService.cs index d714ddf3e..31a90761d 100644 --- a/NzbDrone.Core/Tv/RefreshEpisodeService.cs +++ b/NzbDrone.Core/Tv/RefreshEpisodeService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using NLog; +using NzbDrone.Common; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv.Events; @@ -36,8 +37,9 @@ namespace NzbDrone.Core.Tv var updateList = new List(); var newList = new List(); + var dupeFreeRemoteEpisodes = remoteEpisodes.DistinctBy(m => new { m.SeasonNumber, m.EpisodeNumber }).ToList(); - foreach (var episode in remoteEpisodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber)) + foreach (var episode in dupeFreeRemoteEpisodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber)) { try { From ea5549d736174decd51afde7c784e61e0e4c8d74 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 30 Sep 2013 14:31:03 -0700 Subject: [PATCH 34/34] Changelog will only show updates with notable changes --- NzbDrone.Core/Update/UpdatePackageProvider.cs | 3 +-- UI/Handlebars/Helpers/Series.js | 4 ++-- UI/Handlebars/Helpers/Version.js | 18 ++++++++++++++++++ UI/Handlebars/backbone.marionette.templates.js | 1 + UI/Update/UpdateItemViewTemplate.html | 2 +- 5 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 UI/Handlebars/Helpers/Version.js diff --git a/NzbDrone.Core/Update/UpdatePackageProvider.cs b/NzbDrone.Core/Update/UpdatePackageProvider.cs index 29a9073df..0dbae8e34 100644 --- a/NzbDrone.Core/Update/UpdatePackageProvider.cs +++ b/NzbDrone.Core/Update/UpdatePackageProvider.cs @@ -34,10 +34,9 @@ namespace NzbDrone.Core.Update { var restClient = new RestClient(Services.RootUrl); - var request = new RestRequest("/v1/update/{branch}/all"); + var request = new RestRequest("/v1/update/{branch}/changes"); request.AddUrlSegment("branch", branch); - request.AddParameter("limit", 5); var updates = restClient.ExecuteAndValidate>(request); diff --git a/UI/Handlebars/Helpers/Series.js b/UI/Handlebars/Helpers/Series.js index f9d849325..69022243c 100644 --- a/UI/Handlebars/Helpers/Series.js +++ b/UI/Handlebars/Helpers/Series.js @@ -57,10 +57,10 @@ define( } if (seasonCount === 1) { - return new Handlebars.SafeString('{0} Season'.format(seasonCount)) + return new Handlebars.SafeString('{0} Season'.format(seasonCount)); } - return new Handlebars.SafeString('{0} Seasons'.format(seasonCount)) + return new Handlebars.SafeString('{0} Seasons'.format(seasonCount)); }); Handlebars.registerHelper('titleWithYear', function () { diff --git a/UI/Handlebars/Helpers/Version.js b/UI/Handlebars/Helpers/Version.js new file mode 100644 index 000000000..ca6dad750 --- /dev/null +++ b/UI/Handlebars/Helpers/Version.js @@ -0,0 +1,18 @@ +'use strict'; + +define( + [ + 'handlebars' + ], function (Handlebars) { + + Handlebars.registerHelper('currentVersion', function (version) { + var currentVersion = window.NzbDrone.ServerStatus.version; + + if (currentVersion === version) + { + return new Handlebars.SafeString(''); + } + + return ''; + }); + }); diff --git a/UI/Handlebars/backbone.marionette.templates.js b/UI/Handlebars/backbone.marionette.templates.js index 3d477969b..928f692a5 100644 --- a/UI/Handlebars/backbone.marionette.templates.js +++ b/UI/Handlebars/backbone.marionette.templates.js @@ -9,6 +9,7 @@ define( 'Handlebars/Helpers/Episode', 'Handlebars/Helpers/Series', 'Handlebars/Helpers/Quality', + 'Handlebars/Helpers/Version', 'Handlebars/Handlebars.Debug' ], function (Templates) { return function () { diff --git a/UI/Update/UpdateItemViewTemplate.html b/UI/Update/UpdateItemViewTemplate.html index f932fa618..6c9df0a15 100644 --- a/UI/Update/UpdateItemViewTemplate.html +++ b/UI/Update/UpdateItemViewTemplate.html @@ -1,6 +1,6 @@ 
    - {{version}} - {{ShortDate releaseDate}} + {{version}} - {{ShortDate releaseDate}} {{currentVersion version}} {{#with changes}} {{#each new}}