mirror of
https://github.com/Sonarr/Sonarr.git
synced 2024-12-16 11:37:58 +02:00
Merge branch 'develop'
This commit is contained in:
commit
3e545a9d62
@ -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'
|
||||
|
@ -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 @@ public bool Enabled
|
||||
return _configFileProvider.AuthenticationEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAuthenticated(NancyContext context)
|
||||
{
|
||||
if (context.CurrentUser == null && _configFileProvider.AuthenticationEnabled) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
@ -27,7 +24,8 @@ public void Register(IPipelines pipelines)
|
||||
private Response RequiresAuthentication(NancyContext context)
|
||||
{
|
||||
Response response = null;
|
||||
if (context.CurrentUser == null && _authenticationService.Enabled)
|
||||
|
||||
if (!context.Request.IsApiRequest() && !_authenticationService.IsAuthenticated(context))
|
||||
{
|
||||
response = new Response { StatusCode = HttpStatusCode.Unauthorized };
|
||||
}
|
||||
|
55
NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs
Normal file
55
NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs
Normal file
@ -0,0 +1,55 @@
|
||||
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 class EnableStatelessAuthInNancy : IRegisterNancyPipeline
|
||||
{
|
||||
private readonly IAuthenticationService _authenticationService;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
|
||||
public EnableStatelessAuthInNancy(IAuthenticationService authenticationService, IConfigFileProvider configFileProvider)
|
||||
{
|
||||
_authenticationService = authenticationService;
|
||||
_configFileProvider = configFileProvider;
|
||||
}
|
||||
|
||||
public void Register(IPipelines pipelines)
|
||||
{
|
||||
pipelines.BeforeRequest.AddItemToEndOfPipeline(ValidateApiKey);
|
||||
}
|
||||
|
||||
public Response ValidateApiKey(NancyContext context)
|
||||
{
|
||||
Response response = null;
|
||||
|
||||
if (!RuntimeInfo.IsProduction && context.Request.IsLocalRequest())
|
||||
{
|
||||
return response;
|
||||
}
|
||||
|
||||
var apiKey = context.Request.Headers.Authorization;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
28
NzbDrone.Api/Extensions/RequestExtensions.cs
Normal file
28
NzbDrone.Api/Extensions/RequestExtensions.cs
Normal file
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
||||
@ -48,9 +56,9 @@ private string GetIndexText()
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -24,13 +24,10 @@ protected StaticResourceMapperBase(IDiskProvider diskProvider, Logger logger)
|
||||
{
|
||||
_caseSensitive = true;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected abstract string Map(string resourceUrl);
|
||||
|
||||
|
||||
public abstract bool CanHandle(string resourceUrl);
|
||||
|
||||
public virtual Response GetResponse(string resourceUrl)
|
||||
|
@ -30,7 +30,6 @@ protected override void ApplicationStartup(TinyIoCContainer container, IPipeline
|
||||
RegisterPipelines(pipelines);
|
||||
|
||||
container.Resolve<DatabaseTarget>().Register();
|
||||
container.Resolve<IEnableBasicAuthInNancy>().Register(pipelines);
|
||||
container.Resolve<IEventAggregator>().PublishEvent(new ApplicationStartedEvent());
|
||||
|
||||
ApplicationPipelines.OnError.AddItemToEndOfPipeline(container.Resolve<NzbDroneErrorPipeline>().HandleException);
|
||||
|
@ -74,6 +74,7 @@
|
||||
<Link>Properties\SharedAssemblyInfo.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="Authentication\AuthenticationService.cs" />
|
||||
<Compile Include="Authentication\EnableStatelessAuthInNancy.cs" />
|
||||
<Compile Include="Authentication\EnableBasicAuthInNancy.cs" />
|
||||
<Compile Include="Authentication\NzbDroneUser.cs" />
|
||||
<Compile Include="Calendar\CalendarModule.cs" />
|
||||
@ -97,6 +98,7 @@
|
||||
<Compile Include="Extensions\Pipelines\IfModifiedPipeline.cs" />
|
||||
<Compile Include="Extensions\Pipelines\IRegisterNancyPipeline.cs" />
|
||||
<Compile Include="Extensions\NancyJsonSerializer.cs" />
|
||||
<Compile Include="Extensions\RequestExtensions.cs" />
|
||||
<Compile Include="Frontend\IsCacheableSpecification.cs" />
|
||||
<Compile Include="Frontend\Mappers\IndexHtmlMapper.cs" />
|
||||
<Compile Include="Frontend\Mappers\LogFileMapper.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 @@ public Int32 SeasonCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Seasons != null) return Seasons.Count;
|
||||
if (Seasons == null) return 0;
|
||||
|
||||
return 0;
|
||||
return Seasons.Where(s => s.SeasonNumber > 0).Count();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,25 +10,33 @@ namespace NzbDrone.Api.Update
|
||||
public class UpdateModule : NzbDroneRestModule<UpdateResource>
|
||||
{
|
||||
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<UpdateResource> GetAvailableUpdate()
|
||||
private UpdateResource GetAvailableUpdate()
|
||||
{
|
||||
var update = _checkUpdateService.AvailableUpdate();
|
||||
var response = new List<UpdateResource>();
|
||||
var response = new UpdateResource();
|
||||
|
||||
if (update != null)
|
||||
{
|
||||
response.Add(update.InjectTo<UpdateResource>());
|
||||
return update.InjectTo<UpdateResource>();
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private List<UpdateResource> GetRecentUpdates()
|
||||
{
|
||||
return ToListResource(_recentUpdateProvider.GetRecentUpdatePackages);
|
||||
}
|
||||
}
|
||||
|
||||
public class UpdateResource : RestResource
|
||||
@ -40,5 +48,7 @@ public class UpdateResource : RestResource
|
||||
public DateTime ReleaseDate { get; set; }
|
||||
public String FileName { get; set; }
|
||||
public String Url { get; set; }
|
||||
|
||||
public UpdateChanges Changes { get; set; }
|
||||
}
|
||||
}
|
@ -39,6 +39,7 @@ public interface IDiskProvider
|
||||
string GetPathRoot(string path);
|
||||
void SetPermissions(string filename, WellKnownSidType accountSid, FileSystemRights rights, AccessControlType controlType);
|
||||
bool IsParent(string parentPath, string childPath);
|
||||
void SetFolderWriteTime(string path, DateTime time);
|
||||
FileAttributes GetFileAttributes(string path);
|
||||
void EmptyFolder(string path);
|
||||
}
|
||||
@ -441,6 +442,10 @@ public bool IsParent(string parentPath, string childPath)
|
||||
return false;
|
||||
}
|
||||
|
||||
public void SetFolderWriteTime(string path, DateTime time)
|
||||
{
|
||||
Directory.SetLastWriteTimeUtc(path, time);
|
||||
}
|
||||
|
||||
private static void RemoveReadOnly(string path)
|
||||
{
|
||||
|
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<error code="100" description="Incorrect user credentials"/>
|
@ -252,6 +252,7 @@
|
||||
<Content Include="App_Data\Config.xml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Files\Indexers\Newznab\unauthorized.xml" />
|
||||
<Content Include="Files\Media\H264_sample.mp4">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
@ -351,7 +352,6 @@
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Files\Indexers\" />
|
||||
<Folder Include="ProviderTests\UpdateProviderTests\" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
|
Binary file not shown.
@ -28,13 +28,14 @@ public interface IConfigFileProvider : IHandleAsync<ApplicationStartedEvent>
|
||||
string Password { get; }
|
||||
string LogLevel { get; }
|
||||
string Branch { get; }
|
||||
string ApiKey { get; }
|
||||
bool Torrent { get; }
|
||||
string SslCertHash { get; }
|
||||
}
|
||||
|
||||
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<string> _cache;
|
||||
@ -108,6 +109,14 @@ public bool LaunchBrowser
|
||||
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); }
|
||||
@ -223,6 +232,8 @@ private void EnsureDefaultConfigFile()
|
||||
var xDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
|
||||
xDoc.Add(new XElement(CONFIG_ELEMENT_NAME));
|
||||
xDoc.Save(_configFile);
|
||||
|
||||
SaveConfigDictionary(GetConfigDictionary());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ public static void VerifyStatusCode(this HttpStatusCode statusCode, string messa
|
||||
switch (statusCode)
|
||||
{
|
||||
case HttpStatusCode.BadRequest:
|
||||
throw new BadRequestException(statusCode, message);
|
||||
throw new BadRequestException(message);
|
||||
|
||||
case HttpStatusCode.Unauthorized:
|
||||
throw new UnauthorizedAccessException(message);
|
||||
|
19
NzbDrone.Core/Indexers/Exceptions/ApiKeyException.cs
Normal file
19
NzbDrone.Core/Indexers/Exceptions/ApiKeyException.cs
Normal file
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
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 @@ public FetchFeedService(IHttpProvider httpProvider, Logger logger)
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
public virtual IList<ReleaseInfo> FetchRss(IIndexer indexer)
|
||||
{
|
||||
_logger.Debug("Fetching feeds from " + indexer.Name);
|
||||
@ -53,7 +53,6 @@ public IList<ReleaseInfo> Fetch(IIndexer indexer, SeasonSearchCriteria searchCri
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private IList<ReleaseInfo> Fetch(IIndexer indexer, SeasonSearchCriteria searchCriteria, int offset)
|
||||
{
|
||||
_logger.Debug("Searching for {0} offset: {1}", searchCriteria, offset);
|
||||
@ -117,15 +116,21 @@ private List<ReleaseInfo> Fetch(IIndexer indexer, IEnumerable<string> urls)
|
||||
}
|
||||
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);
|
||||
|
@ -37,14 +37,20 @@ public class IndexerService : IIndexerService, IHandle<ApplicationStartedEvent>
|
||||
{
|
||||
private readonly IIndexerRepository _indexerRepository;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly INewznabTestService _newznabTestService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private readonly List<IIndexer> _indexers;
|
||||
|
||||
public IndexerService(IIndexerRepository indexerRepository, IEnumerable<IIndexer> indexers, IConfigFileProvider configFileProvider, Logger logger)
|
||||
public IndexerService(IIndexerRepository indexerRepository,
|
||||
IEnumerable<IIndexer> indexers,
|
||||
IConfigFileProvider configFileProvider,
|
||||
INewznabTestService newznabTestService,
|
||||
Logger logger)
|
||||
{
|
||||
_indexerRepository = indexerRepository;
|
||||
_configFileProvider = configFileProvider;
|
||||
_newznabTestService = newznabTestService;
|
||||
_logger = logger;
|
||||
|
||||
|
||||
@ -104,6 +110,9 @@ public Indexer Create(Indexer indexer)
|
||||
Settings = indexer.Settings.ToJson()
|
||||
};
|
||||
|
||||
var instance = ToIndexer(definition).Instance;
|
||||
_newznabTestService.Test(instance);
|
||||
|
||||
definition = _indexerRepository.Insert(definition);
|
||||
indexer.Id = definition.Id;
|
||||
|
||||
|
@ -114,7 +114,6 @@ public override IEnumerable<string> GetSeasonSearchUrls(string seriesTitle, int
|
||||
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 @@ public override IndexerKind Kind
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static string NewsnabifyTitle(string title)
|
||||
{
|
||||
return title.Replace("+", "%20");
|
||||
|
19
NzbDrone.Core/Indexers/Newznab/NewznabException.cs
Normal file
19
NzbDrone.Core/Indexers/Newznab/NewznabException.cs
Normal file
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -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 @@ protected override ReleaseInfo PostProcessor(XElement item, ReleaseInfo currentR
|
||||
|
||||
return currentResult;
|
||||
}
|
||||
|
||||
protected override void PreProcess(string source, string url)
|
||||
{
|
||||
NewznabPreProcessor.Process(source, url);
|
||||
}
|
||||
}
|
||||
}
|
24
NzbDrone.Core/Indexers/Newznab/NewznabPreProcessor.cs
Normal file
24
NzbDrone.Core/Indexers/Newznab/NewznabPreProcessor.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
60
NzbDrone.Core/Indexers/NewznabTestService.cs
Normal file
60
NzbDrone.Core/Indexers/NewznabTestService.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Indexers.Newznab;
|
||||
|
||||
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
|
||||
{
|
||||
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");
|
||||
|
||||
var apiKeyFailure = new ValidationFailure("ApiKey", "Invalid API Key");
|
||||
throw new ValidationException(new List<ValidationFailure> { apiKeyFailure }.ToArray());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn("Indexer doesn't appear to be Newznab based");
|
||||
|
||||
var failure = new ValidationFailure("Url", "Invalid Newznab URL entered");
|
||||
throw new ValidationException(new List<ValidationFailure> { failure }.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -29,6 +29,8 @@ protected RssParserBase()
|
||||
|
||||
public IEnumerable<ReleaseInfo> 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 @@ protected virtual string GetNzbInfoUrl(XElement item)
|
||||
|
||||
protected abstract long GetSize(XElement item);
|
||||
|
||||
protected virtual void PreProcess(string source, string url)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual ReleaseInfo PostProcessor(XElement item, ReleaseInfo currentResult)
|
||||
{
|
||||
return currentResult;
|
||||
|
@ -42,7 +42,7 @@ public string MoveEpisodeFile(EpisodeFile episodeFile, Series series)
|
||||
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);
|
||||
MoveFile(episodeFile, series, filePath);
|
||||
|
||||
return filePath;
|
||||
}
|
||||
@ -51,12 +51,12 @@ public string MoveEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode
|
||||
{
|
||||
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);
|
||||
|
||||
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))
|
||||
{
|
||||
@ -73,6 +73,17 @@ private void MoveFile(EpisodeFile episodeFile, string destinationFilename)
|
||||
_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(destinationFilename);
|
||||
|
||||
_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
|
||||
{
|
||||
|
@ -71,7 +71,7 @@ private static Series MapSeries(Show show)
|
||||
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;
|
||||
@ -180,5 +180,14 @@ private static string GetSearchTerm(string phrase)
|
||||
|
||||
return phrase;
|
||||
}
|
||||
|
||||
private static int GetYear(int year, int firstAired)
|
||||
{
|
||||
if (year > 1969) return year;
|
||||
|
||||
if (firstAired == 0) return DateTime.Today.Year;
|
||||
|
||||
return year;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -234,12 +234,16 @@
|
||||
<Compile Include="IndexerSearch\SeasonSearchService.cs" />
|
||||
<Compile Include="Indexers\BasicTorrentRssParser.cs" />
|
||||
<Compile Include="Indexers\DownloadProtocols.cs" />
|
||||
<Compile Include="Indexers\Exceptions\ApiKeyException.cs" />
|
||||
<Compile Include="Indexers\Eztv\Eztv.cs" />
|
||||
<Compile Include="Indexers\FetchAndParseRssService.cs" />
|
||||
<Compile Include="Indexers\IIndexer.cs" />
|
||||
<Compile Include="Indexers\IndexerSettingUpdatedEvent.cs" />
|
||||
<Compile Include="Indexers\NewznabTestService.cs" />
|
||||
<Compile Include="Indexers\IndexerWithSetting.cs" />
|
||||
<Compile Include="Indexers\IParseFeed.cs" />
|
||||
<Compile Include="Indexers\Newznab\NewznabException.cs" />
|
||||
<Compile Include="Indexers\Newznab\NewznabPreProcessor.cs" />
|
||||
<Compile Include="Indexers\Newznab\SizeParsingException.cs" />
|
||||
<Compile Include="Indexers\NullSetting.cs" />
|
||||
<Compile Include="Indexers\RssSyncCommand.cs" />
|
||||
@ -546,6 +550,8 @@
|
||||
<Compile Include="Tv\RefreshSeriesService.cs" />
|
||||
<Compile Include="Update\Commands\ApplicationUpdateCommand.cs" />
|
||||
<Compile Include="Update\InstallUpdateService.cs" />
|
||||
<Compile Include="Update\RecentUpdateProvider.cs" />
|
||||
<Compile Include="Update\UpdateChanges.cs" />
|
||||
<Compile Include="Update\UpdatePackageAvailable.cs" />
|
||||
<Compile Include="Update\UpdatePackageProvider.cs" />
|
||||
<Compile Include="Update\UpdatePackage.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 @@ public void RefreshEpisodeInfo(Series series, IEnumerable<Episode> remoteEpisode
|
||||
|
||||
var updateList = new List<Episode>();
|
||||
var newList = new List<Episode>();
|
||||
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
|
||||
{
|
||||
|
32
NzbDrone.Core/Update/RecentUpdateProvider.cs
Normal file
32
NzbDrone.Core/Update/RecentUpdateProvider.cs
Normal file
@ -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<UpdatePackage> 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<UpdatePackage> GetRecentUpdatePackages()
|
||||
{
|
||||
var branch = _configFileProvider.Branch;
|
||||
return _updatePackageProvider.GetRecentUpdates(branch);
|
||||
}
|
||||
}
|
||||
}
|
17
NzbDrone.Core/Update/UpdateChanges.cs
Normal file
17
NzbDrone.Core/Update/UpdateChanges.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.Update
|
||||
{
|
||||
public class UpdateChanges
|
||||
{
|
||||
public List<String> New { get; set; }
|
||||
public List<String> Fixed { get; set; }
|
||||
|
||||
public UpdateChanges()
|
||||
{
|
||||
New = new List<String>();
|
||||
Fixed = new List<String>();
|
||||
}
|
||||
}
|
||||
}
|
@ -13,5 +13,7 @@ public class UpdatePackage
|
||||
public DateTime ReleaseDate { get; set; }
|
||||
public String FileName { get; set; }
|
||||
public String Url { get; set; }
|
||||
|
||||
public UpdateChanges Changes { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -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<UpdatePackage> GetRecentUpdates(string branch);
|
||||
}
|
||||
|
||||
public class UpdatePackageProvider : IUpdatePackageProvider
|
||||
@ -27,5 +29,18 @@ public UpdatePackage GetLatestUpdate(string branch, Version currentVersion)
|
||||
|
||||
return update.UpdatePackage;
|
||||
}
|
||||
|
||||
public List<UpdatePackage> GetRecentUpdates(string branch)
|
||||
{
|
||||
var restClient = new RestClient(Services.RootUrl);
|
||||
|
||||
var request = new RestRequest("/v1/update/{branch}/changes");
|
||||
|
||||
request.AddUrlSegment("branch", branch);
|
||||
|
||||
var updates = restClient.ExecuteAndValidate<List<UpdatePackage>>(request);
|
||||
|
||||
return updates;
|
||||
}
|
||||
}
|
||||
}
|
@ -26,18 +26,21 @@ public void MakeAccessible()
|
||||
{
|
||||
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 @@ private bool IsNzbDronePortOpen()
|
||||
|
||||
foreach (INetFwOpenPort p in ports)
|
||||
{
|
||||
if (p.Port == _configFileProvider.Port)
|
||||
if (p.Port == port)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -63,8 +66,6 @@ private bool IsNzbDronePortOpen()
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void OpenFirewallPort(int portNumber)
|
||||
{
|
||||
try
|
||||
|
@ -36,7 +36,12 @@ public void Register()
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -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 @@ public ClientBase(IRestClient restClient, string resource = null)
|
||||
|
||||
_restClient = restClient;
|
||||
_resource = resource;
|
||||
_apiKey = apiKey;
|
||||
|
||||
_logger = LogManager.GetLogger("REST");
|
||||
}
|
||||
@ -88,10 +89,14 @@ public List<dynamic> InvalidPost(TResource body)
|
||||
|
||||
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<T>(IRestRequest request, HttpStatusCode statusCode = HttpStatusCode.OK) where T : class, new()
|
||||
|
@ -6,8 +6,8 @@ namespace NzbDrone.Integration.Test.Client
|
||||
{
|
||||
public class EpisodeClient : ClientBase<EpisodeResource>
|
||||
{
|
||||
public EpisodeClient(IRestClient restClient)
|
||||
: base(restClient, "episodes")
|
||||
public EpisodeClient(IRestClient restClient, string apiKey)
|
||||
: base(restClient, apiKey, "episodes")
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -5,12 +5,9 @@ namespace NzbDrone.Integration.Test.Client
|
||||
{
|
||||
public class IndexerClient : ClientBase<IndexerResource>
|
||||
{
|
||||
public IndexerClient(IRestClient restClient)
|
||||
: base(restClient)
|
||||
public IndexerClient(IRestClient restClient, string apiKey)
|
||||
: base(restClient, apiKey)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -5,12 +5,9 @@ namespace NzbDrone.Integration.Test.Client
|
||||
{
|
||||
public class ReleaseClient : ClientBase<ReleaseResource>
|
||||
{
|
||||
public ReleaseClient(IRestClient restClient)
|
||||
: base(restClient)
|
||||
public ReleaseClient(IRestClient restClient, string apiKey)
|
||||
: base(restClient, apiKey)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -7,8 +7,8 @@ namespace NzbDrone.Integration.Test.Client
|
||||
{
|
||||
public class SeriesClient : ClientBase<SeriesResource>
|
||||
{
|
||||
public SeriesClient(IRestClient restClient)
|
||||
: base(restClient)
|
||||
public SeriesClient(IRestClient restClient, string apiKey)
|
||||
: base(restClient, apiKey)
|
||||
{
|
||||
}
|
||||
|
||||
@ -27,14 +27,11 @@ public SeriesResource Get(string slug, HttpStatusCode statusCode = HttpStatusCod
|
||||
|
||||
}
|
||||
|
||||
|
||||
public class SystemInfoClient : ClientBase<SeriesResource>
|
||||
{
|
||||
public SystemInfoClient(IRestClient restClient)
|
||||
: base(restClient)
|
||||
public SystemInfoClient(IRestClient restClient, string apiKey)
|
||||
: base(restClient, apiKey)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -47,22 +47,21 @@ public void SmokeTestSetup()
|
||||
_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<RootFolderResource>(RestClient);
|
||||
Commands = new ClientBase<CommandResource>(RestClient);
|
||||
History = new ClientBase<HistoryResource>(RestClient);
|
||||
Indexers = new IndexerClient(RestClient);
|
||||
Episodes = new EpisodeClient(RestClient);
|
||||
NamingConfig = new ClientBase<NamingConfigResource>(RestClient, "config/naming");
|
||||
Series = new SeriesClient(RestClient, _runner.ApiKey);
|
||||
Releases = new ReleaseClient(RestClient, _runner.ApiKey);
|
||||
RootFolders = new ClientBase<RootFolderResource>(RestClient, _runner.ApiKey);
|
||||
Commands = new ClientBase<CommandResource>(RestClient, _runner.ApiKey);
|
||||
History = new ClientBase<HistoryResource>(RestClient, _runner.ApiKey);
|
||||
Indexers = new IndexerClient(RestClient, _runner.ApiKey);
|
||||
Episodes = new EpisodeClient(RestClient, _runner.ApiKey);
|
||||
NamingConfig = new ClientBase<NamingConfigResource>(RestClient, _runner.ApiKey, "config/naming");
|
||||
}
|
||||
|
||||
//[TestFixtureTearDown]
|
||||
|
@ -1,11 +1,14 @@
|
||||
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.Common.Processes;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using RestSharp;
|
||||
|
||||
namespace NzbDrone.Integration.Test
|
||||
@ -16,16 +19,18 @@ public class NzbDroneRunner
|
||||
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";
|
||||
|
||||
@ -34,7 +39,6 @@ public void Start()
|
||||
nzbdroneConsoleExe = "NzbDrone.exe";
|
||||
}
|
||||
|
||||
|
||||
if (BuildInfo.IsDebug)
|
||||
{
|
||||
|
||||
@ -54,8 +58,12 @@ public void Start()
|
||||
Assert.Fail("Process has exited");
|
||||
}
|
||||
|
||||
SetApiKey();
|
||||
|
||||
var statusCall = _restClient.Get(new RestRequest("system/status"));
|
||||
var request = new RestRequest("system/status");
|
||||
request.AddHeader("Authorization", ApiKey);
|
||||
|
||||
var statusCall = _restClient.Get(request);
|
||||
|
||||
if (statusCall.ResponseStatus == ResponseStatus.Completed)
|
||||
{
|
||||
@ -77,7 +85,7 @@ public void KillAll()
|
||||
|
||||
private void Start(string outputNzbdroneConsoleExe)
|
||||
{
|
||||
var args = "-nobrowser -data=\"" + AppDate + "\"";
|
||||
var args = "-nobrowser -data=\"" + AppData + "\"";
|
||||
_nzbDroneProcess = _processProvider.Start(outputNzbdroneConsoleExe, args, OnOutputDataReceived, OnOutputDataReceived);
|
||||
|
||||
}
|
||||
@ -92,7 +100,16 @@ private void OnOutputDataReceived(string data)
|
||||
}
|
||||
}
|
||||
|
||||
private void SetApiKey()
|
||||
{
|
||||
var configFile = Path.Combine(AppData, "config.xml");
|
||||
|
||||
public string AppDate { get; private set; }
|
||||
if (!String.IsNullOrWhiteSpace(ApiKey)) return;
|
||||
if (!File.Exists(configFile)) return;
|
||||
|
||||
var xDoc = XDocument.Load(configFile);
|
||||
var config = xDoc.Descendants(ConfigFileProvider.CONFIG_ELEMENT_NAME).Single();
|
||||
ApiKey = config.Descendants("ApiKey").Single().Value;
|
||||
}
|
||||
}
|
||||
}
|
38
UI/.idea/runConfigurations/Debug___Chrome.xml
generated
38
UI/.idea/runConfigurations/Debug___Chrome.xml
generated
@ -1,25 +1,21 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Debug - Chrome" type="JavascriptDebugSession" factoryName="Remote" singleton="true">
|
||||
<JSRemoteDebuggerConfigurationSettings>
|
||||
<option name="engineId" value="chrome" />
|
||||
<option name="fileUrl" value="http://localhost:8989" />
|
||||
<mapping url="http://localhost:8989/Calendar" local-file="$PROJECT_DIR$/Calendar" />
|
||||
<mapping url="http://localhost:8989/MainMenuView.js" local-file="$PROJECT_DIR$/MainMenuView.js" />
|
||||
<mapping url="http://localhost:8989/Settings" local-file="$PROJECT_DIR$/Settings" />
|
||||
<mapping url="http://localhost:8989/Upcoming" local-file="$PROJECT_DIR$/Upcoming" />
|
||||
<mapping url="http://localhost:8989/app.js" local-file="$PROJECT_DIR$/app.js" />
|
||||
<mapping url="http://localhost:8989/Mixins" local-file="$PROJECT_DIR$/Mixins" />
|
||||
<mapping url="http://localhost:8989/Missing" local-file="$PROJECT_DIR$/Missing" />
|
||||
<mapping url="http://localhost:8989/Quality" local-file="$PROJECT_DIR$/Quality" />
|
||||
<mapping url="http://localhost:8989/Config.js" local-file="$PROJECT_DIR$/Config.js" />
|
||||
<mapping url="http://localhost:8989/Shared" local-file="$PROJECT_DIR$/Shared" />
|
||||
<mapping url="http://localhost:8989/AddSeries" local-file="$PROJECT_DIR$/AddSeries" />
|
||||
<mapping url="http://localhost:8989/HeaderView.js" local-file="$PROJECT_DIR$/HeaderView.js" />
|
||||
<mapping url="http://localhost:8989" local-file="$PROJECT_DIR$" />
|
||||
<mapping url="http://localhost:8989/Routing.js" local-file="$PROJECT_DIR$/Routing.js" />
|
||||
<mapping url="http://localhost:8989/Controller.js" local-file="$PROJECT_DIR$/Controller.js" />
|
||||
<mapping url="http://localhost:8989/Series" local-file="$PROJECT_DIR$/Series" />
|
||||
</JSRemoteDebuggerConfigurationSettings>
|
||||
<configuration default="false" name="Debug - Chrome" type="JavascriptDebugType" factoryName="JavaScript Debug" singleton="true" uri="http://localhost:8989">
|
||||
<mapping url="http://localhost:8989/Calendar" local-file="$PROJECT_DIR$/Calendar" />
|
||||
<mapping url="http://localhost:8989/MainMenuView.js" local-file="$PROJECT_DIR$/MainMenuView.js" />
|
||||
<mapping url="http://localhost:8989/Settings" local-file="$PROJECT_DIR$/Settings" />
|
||||
<mapping url="http://localhost:8989/Upcoming" local-file="$PROJECT_DIR$/Upcoming" />
|
||||
<mapping url="http://localhost:8989/app.js" local-file="$PROJECT_DIR$/app.js" />
|
||||
<mapping url="http://localhost:8989/Mixins" local-file="$PROJECT_DIR$/Mixins" />
|
||||
<mapping url="http://localhost:8989/Missing" local-file="$PROJECT_DIR$/Missing" />
|
||||
<mapping url="http://localhost:8989/Quality" local-file="$PROJECT_DIR$/Quality" />
|
||||
<mapping url="http://localhost:8989/Config.js" local-file="$PROJECT_DIR$/Config.js" />
|
||||
<mapping url="http://localhost:8989/Shared" local-file="$PROJECT_DIR$/Shared" />
|
||||
<mapping url="http://localhost:8989/AddSeries" local-file="$PROJECT_DIR$/AddSeries" />
|
||||
<mapping url="http://localhost:8989/HeaderView.js" local-file="$PROJECT_DIR$/HeaderView.js" />
|
||||
<mapping url="http://localhost:8989" local-file="$PROJECT_DIR$" />
|
||||
<mapping url="http://localhost:8989/Routing.js" local-file="$PROJECT_DIR$/Routing.js" />
|
||||
<mapping url="http://localhost:8989/Controller.js" local-file="$PROJECT_DIR$/Controller.js" />
|
||||
<mapping url="http://localhost:8989/Series" local-file="$PROJECT_DIR$/Series" />
|
||||
<RunnerSettings RunnerId="JavascriptDebugRunner" />
|
||||
<ConfigurationWrapper RunnerId="JavascriptDebugRunner" />
|
||||
<method />
|
||||
|
38
UI/.idea/runConfigurations/Debug___Firefox.xml
generated
38
UI/.idea/runConfigurations/Debug___Firefox.xml
generated
@ -1,25 +1,21 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Debug - Firefox" type="JavascriptDebugSession" factoryName="Remote" singleton="true">
|
||||
<JSRemoteDebuggerConfigurationSettings>
|
||||
<option name="engineId" value="firefox" />
|
||||
<option name="fileUrl" value="http://localhost:8989" />
|
||||
<mapping url="http://localhost:8989/Calendar" local-file="$PROJECT_DIR$/Calendar" />
|
||||
<mapping url="http://localhost:8989/MainMenuView.js" local-file="$PROJECT_DIR$/MainMenuView.js" />
|
||||
<mapping url="http://localhost:8989/Settings" local-file="$PROJECT_DIR$/Settings" />
|
||||
<mapping url="http://localhost:8989/Upcoming" local-file="$PROJECT_DIR$/Upcoming" />
|
||||
<mapping url="http://localhost:8989/app.js" local-file="$PROJECT_DIR$/app.js" />
|
||||
<mapping url="http://localhost:8989/Mixins" local-file="$PROJECT_DIR$/Mixins" />
|
||||
<mapping url="http://localhost:8989/Missing" local-file="$PROJECT_DIR$/Missing" />
|
||||
<mapping url="http://localhost:8989/Config.js" local-file="$PROJECT_DIR$/Config.js" />
|
||||
<mapping url="http://localhost:8989/Quality" local-file="$PROJECT_DIR$/Quality" />
|
||||
<mapping url="http://localhost:8989/AddSeries" local-file="$PROJECT_DIR$/AddSeries" />
|
||||
<mapping url="http://localhost:8989/Shared" local-file="$PROJECT_DIR$/Shared" />
|
||||
<mapping url="http://localhost:8989/HeaderView.js" local-file="$PROJECT_DIR$/HeaderView.js" />
|
||||
<mapping url="http://localhost:8989" local-file="$PROJECT_DIR$" />
|
||||
<mapping url="http://localhost:8989/Routing.js" local-file="$PROJECT_DIR$/Routing.js" />
|
||||
<mapping url="http://localhost:8989/Controller.js" local-file="$PROJECT_DIR$/Controller.js" />
|
||||
<mapping url="http://localhost:8989/Series" local-file="$PROJECT_DIR$/Series" />
|
||||
</JSRemoteDebuggerConfigurationSettings>
|
||||
<configuration default="false" name="Debug - Firefox" type="JavascriptDebugType" factoryName="JavaScript Debug" singleton="true" engineId="firefox" uri="http://localhost:8989">
|
||||
<mapping url="http://localhost:8989/Calendar" local-file="$PROJECT_DIR$/Calendar" />
|
||||
<mapping url="http://localhost:8989/MainMenuView.js" local-file="$PROJECT_DIR$/MainMenuView.js" />
|
||||
<mapping url="http://localhost:8989/Settings" local-file="$PROJECT_DIR$/Settings" />
|
||||
<mapping url="http://localhost:8989/Upcoming" local-file="$PROJECT_DIR$/Upcoming" />
|
||||
<mapping url="http://localhost:8989/app.js" local-file="$PROJECT_DIR$/app.js" />
|
||||
<mapping url="http://localhost:8989/Mixins" local-file="$PROJECT_DIR$/Mixins" />
|
||||
<mapping url="http://localhost:8989/Missing" local-file="$PROJECT_DIR$/Missing" />
|
||||
<mapping url="http://localhost:8989/Config.js" local-file="$PROJECT_DIR$/Config.js" />
|
||||
<mapping url="http://localhost:8989/Quality" local-file="$PROJECT_DIR$/Quality" />
|
||||
<mapping url="http://localhost:8989/AddSeries" local-file="$PROJECT_DIR$/AddSeries" />
|
||||
<mapping url="http://localhost:8989/Shared" local-file="$PROJECT_DIR$/Shared" />
|
||||
<mapping url="http://localhost:8989/HeaderView.js" local-file="$PROJECT_DIR$/HeaderView.js" />
|
||||
<mapping url="http://localhost:8989" local-file="$PROJECT_DIR$" />
|
||||
<mapping url="http://localhost:8989/Routing.js" local-file="$PROJECT_DIR$/Routing.js" />
|
||||
<mapping url="http://localhost:8989/Controller.js" local-file="$PROJECT_DIR$/Controller.js" />
|
||||
<mapping url="http://localhost:8989/Series" local-file="$PROJECT_DIR$/Series" />
|
||||
<RunnerSettings RunnerId="JavascriptDebugRunner" />
|
||||
<ConfigurationWrapper RunnerId="JavascriptDebugRunner" />
|
||||
<method />
|
||||
|
@ -4,7 +4,7 @@ define(
|
||||
'app',
|
||||
'marionette',
|
||||
'AddSeries/RootFolders/Layout',
|
||||
'AddSeries/Existing/CollectionView',
|
||||
'AddSeries/Existing/AddExistingSeriesCollectionView',
|
||||
'AddSeries/AddSeriesView',
|
||||
'Quality/QualityProfileCollection',
|
||||
'AddSeries/RootFolders/Collection',
|
||||
@ -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();
|
||||
},
|
||||
|
@ -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) {
|
||||
|
||||
|
@ -29,9 +29,11 @@ define(
|
||||
this.addItemView(model, this.getItemView(), index);
|
||||
this.children.findByModel(model)
|
||||
.search({term: folderName})
|
||||
.always((function () {
|
||||
self._showAndSearch(currentIndex + 1);
|
||||
}));
|
||||
.always(function () {
|
||||
if (!self.isClosed) {
|
||||
self._showAndSearch(currentIndex + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -2,8 +2,7 @@
|
||||
define(
|
||||
[
|
||||
'marionette',
|
||||
'AddSeries/SearchResultView',
|
||||
|
||||
'AddSeries/SearchResultView'
|
||||
], function (Marionette, SearchResultView) {
|
||||
|
||||
return Marionette.CollectionView.extend({
|
||||
|
@ -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({
|
||||
|
||||
@ -37,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);
|
||||
@ -71,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) {
|
||||
@ -134,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');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
69
UI/Cells/Header/QualityHeaderCell.js
Normal file
69
UI/Cells/Header/QualityHeaderCell.js
Normal file
@ -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;
|
||||
});
|
@ -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();
|
||||
});
|
||||
|
@ -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';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -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});
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -162,6 +162,10 @@ footer {
|
||||
color : @successText;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
color : @warningText;
|
||||
}
|
||||
|
||||
.status-danger {
|
||||
color : @errorText;
|
||||
}
|
||||
|
@ -15,11 +15,12 @@ define(
|
||||
'Logs/Files/Layout',
|
||||
'Release/Layout',
|
||||
'System/Layout',
|
||||
'SeasonPass/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));
|
||||
|
@ -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
|
||||
},
|
||||
|
||||
{
|
||||
|
@ -57,10 +57,10 @@ define(
|
||||
}
|
||||
|
||||
if (seasonCount === 1) {
|
||||
return new Handlebars.SafeString('<span class="label label-info">{0} Season</span>'.format(seasonCount))
|
||||
return new Handlebars.SafeString('<span class="label label-info">{0} Season</span>'.format(seasonCount));
|
||||
}
|
||||
|
||||
return new Handlebars.SafeString('<span class="label label-info">{0} Seasons</span>'.format(seasonCount))
|
||||
return new Handlebars.SafeString('<span class="label label-info">{0} Seasons</span>'.format(seasonCount));
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('titleWithYear', function () {
|
||||
|
18
UI/Handlebars/Helpers/Version.js
Normal file
18
UI/Handlebars/Helpers/Version.js
Normal file
@ -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('<i class="icon-ok" title="Installed"></i>');
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
});
|
@ -9,6 +9,7 @@ define(
|
||||
'Handlebars/Helpers/Episode',
|
||||
'Handlebars/Helpers/Series',
|
||||
'Handlebars/Helpers/Quality',
|
||||
'Handlebars/Helpers/Version',
|
||||
'Handlebars/Handlebars.Debug'
|
||||
], function (Templates) {
|
||||
return function () {
|
||||
|
@ -60,9 +60,10 @@ define(
|
||||
cell : EpisodeTitleCell
|
||||
},
|
||||
{
|
||||
name : 'quality',
|
||||
label: 'Quality',
|
||||
cell : QualityCell
|
||||
name : 'quality',
|
||||
label : 'Quality',
|
||||
cell : QualityCell,
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name : 'date',
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -20,9 +20,12 @@ define(function () {
|
||||
|
||||
delete xhr.data;
|
||||
}
|
||||
if (xhr) {
|
||||
xhr.headers = xhr.headers || {};
|
||||
xhr.headers['Authorization'] = window.NzbDrone.ApiKey;
|
||||
}
|
||||
|
||||
return original.apply(this, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
});
|
@ -50,7 +50,7 @@
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=KRTE52U3XJDSQ" target="_blank">
|
||||
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HGGGM7JT5YVSS" target="_blank">
|
||||
<i class="icon-nd-donate"></i>
|
||||
<br>
|
||||
Donate
|
||||
|
@ -4,12 +4,21 @@ 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 () {
|
||||
return SeriesCollection.map(function (model) {
|
||||
return model.get('title');
|
||||
});
|
||||
return SeriesCollection.pluck('title');
|
||||
},
|
||||
|
||||
sorter: function (items) {
|
||||
@ -17,9 +26,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 });
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -31,6 +31,7 @@ require(
|
||||
'rss' : 'rss',
|
||||
'system' : 'system',
|
||||
'seasonpass' : 'seasonPass',
|
||||
'update' : 'update',
|
||||
':whatever' : 'notFound'
|
||||
}
|
||||
});
|
||||
|
@ -12,7 +12,7 @@ define(
|
||||
SeriesCollectionView,
|
||||
LoadingView) {
|
||||
return Marionette.Layout.extend({
|
||||
template: 'SeasonPass/LayoutTemplate',
|
||||
template: 'SeasonPass/SeasonPassLayoutTemplate',
|
||||
|
||||
regions: {
|
||||
series: '#x-series'
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,8 +1,8 @@
|
||||
<div class="seasonpass-series">
|
||||
<div class="row">
|
||||
<div class="span11">
|
||||
<div class="span12">
|
||||
<i class="icon-chevron-right x-expander expander pull-left"/>
|
||||
|
||||
<i class="x-series-monitored series-monitor-toggle pull-left" title="Toggle monitored state for entire series"/>
|
||||
<span class="title span5">
|
||||
<a href="{{route}}">
|
||||
{{title}}
|
||||
@ -26,10 +26,20 @@
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<button class="btn x-latest">Latest</button>
|
||||
<span class="season-pass-button">
|
||||
<button class="btn x-latest">Latest</button>
|
||||
|
||||
<span class="help-inline">
|
||||
<i class="icon-question-sign" title="Will quickly select the latest season as first monitored"/>
|
||||
<span class="help-inline">
|
||||
<i class="icon-question-sign" title="Will quickly select the latest season as first monitored"/>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="season-pass-button">
|
||||
<button class="btn x-all">All</button>
|
||||
|
||||
<span class="help-inline">
|
||||
<i class="icon-question-sign" title="Will quickly select all seasons except for specials to be monitored"/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,13 +9,17 @@
|
||||
{{/if}}
|
||||
|
||||
{{#if_eq episodeCount compare=0}}
|
||||
<i class="icon-nd-status season-status status-primary" title="No aired episodes"/>
|
||||
{{#if monitored}}
|
||||
<i class="icon-nd-status season-status status-primary" title="No aired episodes"/>
|
||||
{{else}}
|
||||
<i class="icon-nd-status season-status status-warning" title="Season is not monitored"/>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if_eq percentOfEpisodes compare=100}}
|
||||
<i class="icon-nd-status season-status status-success" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded"/>
|
||||
{{else}}
|
||||
<i class="icon-nd-status season-status status-danger" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded"/>
|
||||
{{/if_eq}}
|
||||
{{#if_eq percentOfEpisodes compare=100}}
|
||||
<i class="icon-nd-status season-status status-success" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded"/>
|
||||
{{else}}
|
||||
<i class="icon-nd-status season-status status-danger" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded"/>
|
||||
{{/if_eq}}
|
||||
{{/if_eq}}
|
||||
|
||||
<span class="season-actions pull-right">
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="span11">
|
||||
<div class="row">
|
||||
<h1>
|
||||
<i class="x-monitored clickable series-monitor-toggle" title="Toggle monitored state for entire series"/>
|
||||
<i class="x-monitored" title="Toggle monitored state for entire series"/>
|
||||
{{title}}
|
||||
<div class="series-actions pull-right">
|
||||
<div class="x-refresh">
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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: {
|
||||
Authorization: window.NzbDrone.ApiKey
|
||||
}
|
||||
}).responseText;
|
||||
|
||||
window.NzbDrone.ServerStatus = JSON.parse(statusText);
|
||||
|
@ -7,11 +7,49 @@
|
||||
|
||||
<div class="controls">
|
||||
<input type="number" placeholder="8989" name="port"/>
|
||||
<span>
|
||||
<i class="icon-nd-form-warning" title="Requires restart to take effect"/>
|
||||
</span>
|
||||
<span>
|
||||
<i class="icon-nd-form-warning" title="Requires restart to take effect"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group advanced-setting">
|
||||
<label class="control-label">Enable SSL</label>
|
||||
|
||||
<div class="controls">
|
||||
<label class="checkbox toggle well">
|
||||
<input type="checkbox" name="enableSsl" class="x-ssl"/>
|
||||
|
||||
<p>
|
||||
<span>Yes</span>
|
||||
<span>No</span>
|
||||
</p>
|
||||
|
||||
<div class="btn btn-primary slide-button"/>
|
||||
</label>
|
||||
|
||||
<span class="help-inline-checkbox">
|
||||
<i class="icon-nd-form-warning" title="Requires restart running as administrator to take effect"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="x-ssl-options">
|
||||
<div class="control-group advanced-setting">
|
||||
<label class="control-label">SSL Port Number</label>
|
||||
|
||||
<div class="controls">
|
||||
<input type="number" placeholder="8989" name="sslPort"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group advanced-setting">
|
||||
<label class="control-label">SSL Cert Hash</label>
|
||||
|
||||
<div class="controls">
|
||||
<input type="text" name="sslCertHash"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
@ -29,9 +67,9 @@
|
||||
<div class="btn btn-primary slide-button"/>
|
||||
</label>
|
||||
|
||||
<span class="help-inline-checkbox">
|
||||
<i class="icon-question-sign" title="Open a web browser and navigate to NzbDrone homepage on app start. Has no effect if installed as a windows service"/>
|
||||
</span>
|
||||
<span class="help-inline-checkbox">
|
||||
<i class="icon-nd-form-info" title="Open a web browser and navigate to NzbDrone homepage on app start. Has no effect if installed as a windows service"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
@ -51,7 +89,7 @@
|
||||
</label>
|
||||
|
||||
<span class="help-inline-checkbox">
|
||||
<i class="icon-question-sign" title="Require Username and Password to access Nzbdrone"/>
|
||||
<i class="icon-nd-form-info" title="Require Username and Password to access Nzbdrone"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -91,8 +129,7 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{{#unless_eq branch compare="master"}}
|
||||
<fieldset>
|
||||
<fieldset class="advanced-setting">
|
||||
<legend>Development</legend>
|
||||
<div class="alert">
|
||||
<i class="icon-nd-warning"></i>
|
||||
@ -106,5 +143,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
{{/unless_eq}}
|
||||
</div>
|
||||
|
@ -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'
|
||||
},
|
||||
events: {
|
||||
'change .x-auth': '_setAuthOptionsVisibility',
|
||||
'change .x-ssl': '_setSslOptionsVisibility'
|
||||
},
|
||||
|
||||
ui: {
|
||||
authToggle : '.x-auth',
|
||||
authOptions: '.x-auth-options'
|
||||
},
|
||||
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();
|
||||
}
|
||||
},
|
||||
|
||||
_setAuthOptionsVisibility: function () {
|
||||
|
||||
var showAuthOptions = this.ui.authToggle.prop('checked');
|
||||
|
||||
if (showAuthOptions) {
|
||||
this.ui.authOptions.slideDown();
|
||||
}
|
||||
|
||||
else {
|
||||
this.ui.authOptions.slideUp();
|
||||
}
|
||||
onRender: function(){
|
||||
if(!this.ui.authToggle.prop('checked')){
|
||||
this.ui.authOptions.hide();
|
||||
}
|
||||
|
||||
});
|
||||
if(!this.ui.sslToggle.prop('checked')){
|
||||
this.ui.sslOptions.hide();
|
||||
}
|
||||
},
|
||||
|
||||
_setAuthOptionsVisibility: function () {
|
||||
|
||||
var showAuthOptions = this.ui.authToggle.prop('checked');
|
||||
|
||||
if (showAuthOptions) {
|
||||
this.ui.authOptions.slideDown();
|
||||
}
|
||||
|
||||
else {
|
||||
this.ui.authOptions.slideUp();
|
||||
}
|
||||
},
|
||||
|
||||
_setSslOptionsVisibility: function () {
|
||||
|
||||
var showSslOptions = this.ui.sslToggle.prop('checked');
|
||||
|
||||
if (showSslOptions) {
|
||||
this.ui.sslOptions.slideDown();
|
||||
}
|
||||
|
||||
else {
|
||||
this.ui.sslOptions.slideUp();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return AsModelBoundView.call(view);
|
||||
});
|
||||
|
@ -39,6 +39,9 @@
|
||||
{{#if id}}
|
||||
<button class="btn btn-danger pull-left x-remove">delete</button>
|
||||
{{/if}}
|
||||
|
||||
<span class="x-activity"></span>
|
||||
|
||||
<button class="btn" data-dismiss="modal">cancel</button>
|
||||
|
||||
<div class="btn-group">
|
||||
|
@ -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('<i class="icon-nd-spinner"></i>');
|
||||
|
||||
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.fail(function () {
|
||||
self.ui.activity.empty();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_saveAndAdd: function () {
|
||||
this.ui.activity.html('<i class="icon-nd-spinner"></i>');
|
||||
|
||||
var self = this;
|
||||
var promise = this.model.saveSettings();
|
||||
|
||||
@ -50,6 +62,10 @@ define(
|
||||
self.model.set('fields.' + key + '.value', '');
|
||||
});
|
||||
});
|
||||
|
||||
promise.fail(function () {
|
||||
self.ui.activity.empty();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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',
|
@ -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);
|
@ -9,7 +9,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="control-group advanced-setting">
|
||||
<label class="control-label">RSS Sync Interval</label>
|
||||
|
||||
<div class="controls">
|
||||
@ -21,7 +21,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="control-group advanced-setting">
|
||||
<label class="control-label">Release Restrictions</label>
|
||||
|
||||
<div class="controls">
|
@ -1,13 +0,0 @@
|
||||
'use strict';
|
||||
define(
|
||||
[
|
||||
'marionette',
|
||||
'Mixins/AsModelBoundView'
|
||||
], function (Marionette, AsModelBoundView) {
|
||||
|
||||
var view = Marionette.ItemView.extend({
|
||||
template: 'Settings/Indexers/Options/ViewTemplate'
|
||||
});
|
||||
|
||||
return AsModelBoundView.call(view);
|
||||
});
|
@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
define(
|
||||
[
|
||||
'marionette',
|
||||
'Mixins/AsModelBoundView',
|
||||
'Mixins/AutoComplete'
|
||||
], function (Marionette, AsModelBoundView) {
|
||||
|
||||
var view = Marionette.ItemView.extend({
|
||||
template: 'Settings/MediaManagement/FileManagement/FileManagementViewTemplate',
|
||||
|
||||
ui: {
|
||||
recyclingBin: '.x-path'
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
this.ui.recyclingBin.autoComplete('/directories');
|
||||
}
|
||||
});
|
||||
|
||||
return AsModelBoundView.call(view);
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
<fieldset>
|
||||
<fieldset class="advanced-setting">
|
||||
<legend>File Management</legend>
|
||||
|
||||
<div class="control-group">
|
||||
@ -40,4 +40,15 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label">Recycling Bin</label>
|
||||
|
||||
<div class="controls">
|
||||
<input type="text" name="recycleBin" class="x-path"/>
|
||||
<span class="help-inline">
|
||||
<i class="icon-nd-form-info" title="Episode files will go here when deleted instead of being permanently deleted"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
@ -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',
|
@ -6,15 +6,16 @@ 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',
|
||||
'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();
|
||||
}
|
||||
},
|
||||
|
||||
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}));
|
||||
});
|
||||
},
|
||||
|
||||
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();
|
||||
}
|
||||
},
|
||||
|
||||
_save: function () {
|
||||
App.vent.trigger(App.Commands.SaveSettings);
|
||||
},
|
||||
|
||||
_setAdvancedSettingsState: function () {
|
||||
var checked = Config.getValueBoolean('advancedSettings');
|
||||
this.ui.advancedSettings.prop('checked', checked);
|
||||
|
||||
if (checked) {
|
||||
this.$el.addClass('show-advanced-settings');
|
||||
}
|
||||
},
|
||||
|
||||
_toggleAdvancedSettings: function () {
|
||||
var checked = this.ui.advancedSettings.prop('checked');
|
||||
Config.setValue('advancedSettings', checked);
|
||||
|
||||
if (checked) {
|
||||
this.$el.addClass('show-advanced-settings');
|
||||
}
|
||||
|
||||
else {
|
||||
this.$el.removeClass('show-advanced-settings');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -6,6 +6,19 @@
|
||||
<li><a href="#notifications" class="x-notifications-tab no-router">Connect</a></li>
|
||||
<li><a href="#general" class="x-general-tab no-router">General</a></li>
|
||||
<li class="pull-right"><button class="btn btn-primary x-save-settings">Save</button></li>
|
||||
<li class="pull-right advanced-settings-toggle">
|
||||
<label class="checkbox toggle well">
|
||||
<input type="checkbox" class="x-advanced-settings"/>
|
||||
<p>
|
||||
<span>Show</span>
|
||||
<span>Hide</span>
|
||||
</p>
|
||||
<div class="btn btn-warning slide-button"/>
|
||||
</label>
|
||||
<span class="help-inline-checkbox">
|
||||
<i class="icon-nd-form-info" title="Show advanced options"/>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
11
UI/Update/UpdateCollection.js
Normal file
11
UI/Update/UpdateCollection.js
Normal file
@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
define(
|
||||
[
|
||||
'backbone',
|
||||
'Update/UpdateModel'
|
||||
], function (Backbone, UpdateModel) {
|
||||
return Backbone.Collection.extend({
|
||||
url : window.NzbDrone.ApiRoot + '/update',
|
||||
model: UpdateModel
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user