1
0
mirror of https://github.com/Radarr/Radarr.git synced 2024-11-05 02:22:31 +01:00

Merge branch 'develop'

This commit is contained in:
Mark McDowall 2013-09-30 15:13:14 -07:00
commit 3e545a9d62
109 changed files with 1332 additions and 404 deletions

View File

@ -73,6 +73,7 @@ module.exports = function (grunt) {
'UI/Cells/cells.less', 'UI/Cells/cells.less',
'UI/Logs/logs.less', 'UI/Logs/logs.less',
'UI/Settings/settings.less', 'UI/Settings/settings.less',
'UI/Update/update.less'
], ],
dest : outputRoot, dest : outputRoot,
ext: '.css' ext: '.css'

View File

@ -1,4 +1,6 @@
using Nancy.Authentication.Basic; using System;
using Nancy;
using Nancy.Authentication.Basic;
using Nancy.Security; using Nancy.Security;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
@ -7,6 +9,7 @@ namespace NzbDrone.Api.Authentication
public interface IAuthenticationService : IUserValidator public interface IAuthenticationService : IUserValidator
{ {
bool Enabled { get; } bool Enabled { get; }
bool IsAuthenticated(NancyContext context);
} }
public class AuthenticationService : IAuthenticationService public class AuthenticationService : IAuthenticationService
@ -44,5 +47,12 @@ public bool Enabled
return _configFileProvider.AuthenticationEnabled; return _configFileProvider.AuthenticationEnabled;
} }
} }
public bool IsAuthenticated(NancyContext context)
{
if (context.CurrentUser == null && _configFileProvider.AuthenticationEnabled) return false;
return true;
}
} }
} }

View File

@ -1,15 +1,12 @@
using Nancy; using Nancy;
using Nancy.Authentication.Basic; using Nancy.Authentication.Basic;
using Nancy.Bootstrapper; using Nancy.Bootstrapper;
using NzbDrone.Api.Extensions;
using NzbDrone.Api.Extensions.Pipelines;
namespace NzbDrone.Api.Authentication namespace NzbDrone.Api.Authentication
{ {
public interface IEnableBasicAuthInNancy public class EnableBasicAuthInNancy : IRegisterNancyPipeline
{
void Register(IPipelines pipelines);
}
public class EnableBasicAuthInNancy : IEnableBasicAuthInNancy
{ {
private readonly IAuthenticationService _authenticationService; private readonly IAuthenticationService _authenticationService;
@ -27,7 +24,8 @@ public void Register(IPipelines pipelines)
private Response RequiresAuthentication(NancyContext context) private Response RequiresAuthentication(NancyContext context)
{ {
Response response = null; Response response = null;
if (context.CurrentUser == null && _authenticationService.Enabled)
if (!context.Request.IsApiRequest() && !_authenticationService.IsAuthenticated(context))
{ {
response = new Response { StatusCode = HttpStatusCode.Unauthorized }; response = new Response { StatusCode = HttpStatusCode.Unauthorized };
} }

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

View 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"));
}
}
}

View File

@ -1,20 +1,28 @@
using System;
using System.IO; using System.IO;
using Nancy; using Nancy;
using Nancy.Responses;
using NLog; using NLog;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Api.Frontend.Mappers namespace NzbDrone.Api.Frontend.Mappers
{ {
public class IndexHtmlMapper : StaticResourceMapperBase public class IndexHtmlMapper : StaticResourceMapperBase
{ {
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IConfigFileProvider _configFileProvider;
private readonly string _indexPath; 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) : base(diskProvider, logger)
{ {
_diskProvider = diskProvider; _diskProvider = diskProvider;
_configFileProvider = configFileProvider;
_indexPath = Path.Combine(appFolderInfo.StartUpFolder, "UI", "index.html"); _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(".css", ".css?v=" + BuildInfo.Version);
text = text.Replace(".js", ".js?v=" + BuildInfo.Version); text = text.Replace(".js", ".js?v=" + BuildInfo.Version);
text = text.Replace("API_KEY", _configFileProvider.ApiKey);
return text; return text;
} }
} }
} }

View File

@ -24,13 +24,10 @@ protected StaticResourceMapperBase(IDiskProvider diskProvider, Logger logger)
{ {
_caseSensitive = true; _caseSensitive = true;
} }
} }
protected abstract string Map(string resourceUrl); protected abstract string Map(string resourceUrl);
public abstract bool CanHandle(string resourceUrl); public abstract bool CanHandle(string resourceUrl);
public virtual Response GetResponse(string resourceUrl) public virtual Response GetResponse(string resourceUrl)

View File

@ -30,7 +30,6 @@ protected override void ApplicationStartup(TinyIoCContainer container, IPipeline
RegisterPipelines(pipelines); RegisterPipelines(pipelines);
container.Resolve<DatabaseTarget>().Register(); container.Resolve<DatabaseTarget>().Register();
container.Resolve<IEnableBasicAuthInNancy>().Register(pipelines);
container.Resolve<IEventAggregator>().PublishEvent(new ApplicationStartedEvent()); container.Resolve<IEventAggregator>().PublishEvent(new ApplicationStartedEvent());
ApplicationPipelines.OnError.AddItemToEndOfPipeline(container.Resolve<NzbDroneErrorPipeline>().HandleException); ApplicationPipelines.OnError.AddItemToEndOfPipeline(container.Resolve<NzbDroneErrorPipeline>().HandleException);

View File

@ -74,6 +74,7 @@
<Link>Properties\SharedAssemblyInfo.cs</Link> <Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile> </Compile>
<Compile Include="Authentication\AuthenticationService.cs" /> <Compile Include="Authentication\AuthenticationService.cs" />
<Compile Include="Authentication\EnableStatelessAuthInNancy.cs" />
<Compile Include="Authentication\EnableBasicAuthInNancy.cs" /> <Compile Include="Authentication\EnableBasicAuthInNancy.cs" />
<Compile Include="Authentication\NzbDroneUser.cs" /> <Compile Include="Authentication\NzbDroneUser.cs" />
<Compile Include="Calendar\CalendarModule.cs" /> <Compile Include="Calendar\CalendarModule.cs" />
@ -97,6 +98,7 @@
<Compile Include="Extensions\Pipelines\IfModifiedPipeline.cs" /> <Compile Include="Extensions\Pipelines\IfModifiedPipeline.cs" />
<Compile Include="Extensions\Pipelines\IRegisterNancyPipeline.cs" /> <Compile Include="Extensions\Pipelines\IRegisterNancyPipeline.cs" />
<Compile Include="Extensions\NancyJsonSerializer.cs" /> <Compile Include="Extensions\NancyJsonSerializer.cs" />
<Compile Include="Extensions\RequestExtensions.cs" />
<Compile Include="Frontend\IsCacheableSpecification.cs" /> <Compile Include="Frontend\IsCacheableSpecification.cs" />
<Compile Include="Frontend\Mappers\IndexHtmlMapper.cs" /> <Compile Include="Frontend\Mappers\IndexHtmlMapper.cs" />
<Compile Include="Frontend\Mappers\LogFileMapper.cs" /> <Compile Include="Frontend\Mappers\LogFileMapper.cs" />

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using NzbDrone.Api.REST; using NzbDrone.Api.REST;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
@ -19,9 +20,9 @@ public Int32 SeasonCount
{ {
get get
{ {
if (Seasons != null) return Seasons.Count; if (Seasons == null) return 0;
return 0; return Seasons.Where(s => s.SeasonNumber > 0).Count();
} }
} }

View File

@ -10,25 +10,33 @@ namespace NzbDrone.Api.Update
public class UpdateModule : NzbDroneRestModule<UpdateResource> public class UpdateModule : NzbDroneRestModule<UpdateResource>
{ {
private readonly ICheckUpdateService _checkUpdateService; private readonly ICheckUpdateService _checkUpdateService;
private readonly IRecentUpdateProvider _recentUpdateProvider;
public UpdateModule(ICheckUpdateService checkUpdateService) public UpdateModule(ICheckUpdateService checkUpdateService,
IRecentUpdateProvider recentUpdateProvider)
{ {
_checkUpdateService = checkUpdateService; _checkUpdateService = checkUpdateService;
GetResourceAll = GetAvailableUpdate; _recentUpdateProvider = recentUpdateProvider;
GetResourceAll = GetRecentUpdates;
} }
private List<UpdateResource> GetAvailableUpdate() private UpdateResource GetAvailableUpdate()
{ {
var update = _checkUpdateService.AvailableUpdate(); var update = _checkUpdateService.AvailableUpdate();
var response = new List<UpdateResource>(); var response = new UpdateResource();
if (update != null) if (update != null)
{ {
response.Add(update.InjectTo<UpdateResource>()); return update.InjectTo<UpdateResource>();
} }
return response; return response;
} }
private List<UpdateResource> GetRecentUpdates()
{
return ToListResource(_recentUpdateProvider.GetRecentUpdatePackages);
}
} }
public class UpdateResource : RestResource public class UpdateResource : RestResource
@ -40,5 +48,7 @@ public class UpdateResource : RestResource
public DateTime ReleaseDate { get; set; } public DateTime ReleaseDate { get; set; }
public String FileName { get; set; } public String FileName { get; set; }
public String Url { get; set; } public String Url { get; set; }
public UpdateChanges Changes { get; set; }
} }
} }

View File

@ -39,6 +39,7 @@ public interface IDiskProvider
string GetPathRoot(string path); string GetPathRoot(string path);
void SetPermissions(string filename, WellKnownSidType accountSid, FileSystemRights rights, AccessControlType controlType); void SetPermissions(string filename, WellKnownSidType accountSid, FileSystemRights rights, AccessControlType controlType);
bool IsParent(string parentPath, string childPath); bool IsParent(string parentPath, string childPath);
void SetFolderWriteTime(string path, DateTime time);
FileAttributes GetFileAttributes(string path); FileAttributes GetFileAttributes(string path);
void EmptyFolder(string path); void EmptyFolder(string path);
} }
@ -441,6 +442,10 @@ public bool IsParent(string parentPath, string childPath)
return false; return false;
} }
public void SetFolderWriteTime(string path, DateTime time)
{
Directory.SetLastWriteTimeUtc(path, time);
}
private static void RemoveReadOnly(string path) private static void RemoveReadOnly(string path)
{ {

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8" ?>
<error code="100" description="Incorrect user credentials"/>

View File

@ -252,6 +252,7 @@
<Content Include="App_Data\Config.xml"> <Content Include="App_Data\Config.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Files\Indexers\Newznab\unauthorized.xml" />
<Content Include="Files\Media\H264_sample.mp4"> <Content Include="Files\Media\H264_sample.mp4">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
@ -351,7 +352,6 @@
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Files\Indexers\" />
<Folder Include="ProviderTests\UpdateProviderTests\" /> <Folder Include="ProviderTests\UpdateProviderTests\" />
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

View File

@ -28,13 +28,14 @@ public interface IConfigFileProvider : IHandleAsync<ApplicationStartedEvent>
string Password { get; } string Password { get; }
string LogLevel { get; } string LogLevel { get; }
string Branch { get; } string Branch { get; }
string ApiKey { get; }
bool Torrent { get; } bool Torrent { get; }
string SslCertHash { get; } string SslCertHash { get; }
} }
public class ConfigFileProvider : IConfigFileProvider public class ConfigFileProvider : IConfigFileProvider
{ {
private const string CONFIG_ELEMENT_NAME = "Config"; public const string CONFIG_ELEMENT_NAME = "Config";
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly ICached<string> _cache; private readonly ICached<string> _cache;
@ -108,6 +109,14 @@ public bool LaunchBrowser
get { return GetValueBoolean("LaunchBrowser", true); } get { return GetValueBoolean("LaunchBrowser", true); }
} }
public string ApiKey
{
get
{
return GetValue("ApiKey", Guid.NewGuid().ToString().Replace("-", ""));
}
}
public bool Torrent public bool Torrent
{ {
get { return GetValueBoolean("Torrent", false, persist: false); } 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")); var xDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
xDoc.Add(new XElement(CONFIG_ELEMENT_NAME)); xDoc.Add(new XElement(CONFIG_ELEMENT_NAME));
xDoc.Save(_configFile); xDoc.Save(_configFile);
SaveConfigDictionary(GetConfigDictionary());
} }
} }

View File

@ -4,11 +4,11 @@ namespace NzbDrone.Core.Exceptions
{ {
public class BadRequestException : DownstreamException 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)
{ {
} }
} }

View File

@ -15,7 +15,7 @@ public static void VerifyStatusCode(this HttpStatusCode statusCode, string messa
switch (statusCode) switch (statusCode)
{ {
case HttpStatusCode.BadRequest: case HttpStatusCode.BadRequest:
throw new BadRequestException(statusCode, message); throw new BadRequestException(message);
case HttpStatusCode.Unauthorized: case HttpStatusCode.Unauthorized:
throw new UnauthorizedAccessException(message); throw new UnauthorizedAccessException(message);

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

View File

@ -3,6 +3,7 @@
using System.Net; using System.Net;
using NLog; using NLog;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using System.Linq; using System.Linq;
@ -30,7 +31,6 @@ public FetchFeedService(IHttpProvider httpProvider, Logger logger)
_logger = logger; _logger = logger;
} }
public virtual IList<ReleaseInfo> FetchRss(IIndexer indexer) public virtual IList<ReleaseInfo> FetchRss(IIndexer indexer)
{ {
_logger.Debug("Fetching feeds from " + indexer.Name); _logger.Debug("Fetching feeds from " + indexer.Name);
@ -53,7 +53,6 @@ public IList<ReleaseInfo> Fetch(IIndexer indexer, SeasonSearchCriteria searchCri
return result; return result;
} }
private IList<ReleaseInfo> Fetch(IIndexer indexer, SeasonSearchCriteria searchCriteria, int offset) private IList<ReleaseInfo> Fetch(IIndexer indexer, SeasonSearchCriteria searchCriteria, int offset)
{ {
_logger.Debug("Searching for {0} offset: {1}", searchCriteria, 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) 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 else
{ {
_logger.Warn("{0} {1} {2}", indexer.Name, url, webException.Message); _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) catch (Exception feedEx)
{ {
feedEx.Data.Add("FeedUrl", url); feedEx.Data.Add("FeedUrl", url);

View File

@ -37,14 +37,20 @@ public class IndexerService : IIndexerService, IHandle<ApplicationStartedEvent>
{ {
private readonly IIndexerRepository _indexerRepository; private readonly IIndexerRepository _indexerRepository;
private readonly IConfigFileProvider _configFileProvider; private readonly IConfigFileProvider _configFileProvider;
private readonly INewznabTestService _newznabTestService;
private readonly Logger _logger; private readonly Logger _logger;
private readonly List<IIndexer> _indexers; 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; _indexerRepository = indexerRepository;
_configFileProvider = configFileProvider; _configFileProvider = configFileProvider;
_newznabTestService = newznabTestService;
_logger = logger; _logger = logger;
@ -104,6 +110,9 @@ public Indexer Create(Indexer indexer)
Settings = indexer.Settings.ToJson() Settings = indexer.Settings.ToJson()
}; };
var instance = ToIndexer(definition).Instance;
_newznabTestService.Test(instance);
definition = _indexerRepository.Insert(definition); definition = _indexerRepository.Insert(definition);
indexer.Id = definition.Id; indexer.Id = definition.Id;

View File

@ -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)); return RecentFeed.Select(url => String.Format("{0}&limit=100&q={1}&season={2}&offset={3}", url, NewsnabifyTitle(seriesTitle), seasonNumber, offset));
} }
public override string Name public override string Name
{ {
get get
@ -131,7 +130,6 @@ public override IndexerKind Kind
} }
} }
private static string NewsnabifyTitle(string title) private static string NewsnabifyTitle(string title)
{ {
return title.Replace("+", "%20"); return title.Replace("+", "%20");

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

View File

@ -1,6 +1,8 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Xml;
using System.Xml.Linq; using System.Xml.Linq;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.Newznab namespace NzbDrone.Core.Indexers.Newznab
@ -46,5 +48,10 @@ protected override ReleaseInfo PostProcessor(XElement item, ReleaseInfo currentR
return currentResult; return currentResult;
} }
protected override void PreProcess(string source, string url)
{
NewznabPreProcessor.Process(source, url);
}
} }
} }

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

View 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());
}
}
}
}

View File

@ -29,6 +29,8 @@ protected RssParserBase()
public IEnumerable<ReleaseInfo> Process(string xml, string url) 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 })) 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 abstract long GetSize(XElement item);
protected virtual void PreProcess(string source, string url)
{
}
protected virtual ReleaseInfo PostProcessor(XElement item, ReleaseInfo currentResult) protected virtual ReleaseInfo PostProcessor(XElement item, ReleaseInfo currentResult)
{ {
return currentResult; return currentResult;

View File

@ -42,7 +42,7 @@ public string MoveEpisodeFile(EpisodeFile episodeFile, Series series)
var episodes = _episodeService.GetEpisodesByFileId(episodeFile.Id); var episodes = _episodeService.GetEpisodesByFileId(episodeFile.Id);
var newFileName = _buildFileNames.BuildFilename(episodes, series, episodeFile); var newFileName = _buildFileNames.BuildFilename(episodes, series, episodeFile);
var filePath = _buildFileNames.BuildFilePath(series, episodes.First().SeasonNumber, newFileName, Path.GetExtension(episodeFile.Path)); var filePath = _buildFileNames.BuildFilePath(series, episodes.First().SeasonNumber, newFileName, Path.GetExtension(episodeFile.Path));
MoveFile(episodeFile, filePath); MoveFile(episodeFile, series, filePath);
return filePath; return filePath;
} }
@ -51,12 +51,12 @@ public string MoveEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode
{ {
var newFileName = _buildFileNames.BuildFilename(localEpisode.Episodes, localEpisode.Series, episodeFile); var newFileName = _buildFileNames.BuildFilename(localEpisode.Episodes, localEpisode.Series, episodeFile);
var filePath = _buildFileNames.BuildFilePath(localEpisode.Series, localEpisode.SeasonNumber, newFileName, Path.GetExtension(episodeFile.Path)); var filePath = _buildFileNames.BuildFilePath(localEpisode.Series, localEpisode.SeasonNumber, newFileName, Path.GetExtension(episodeFile.Path));
MoveFile(episodeFile, filePath); MoveFile(episodeFile, localEpisode.Series, filePath);
return filePath; return filePath;
} }
private void MoveFile(EpisodeFile episodeFile, string destinationFilename) private void MoveFile(EpisodeFile episodeFile, Series series, string destinationFilename)
{ {
if (!_diskProvider.FileExists(episodeFile.Path)) if (!_diskProvider.FileExists(episodeFile.Path))
{ {
@ -73,6 +73,17 @@ private void MoveFile(EpisodeFile episodeFile, string destinationFilename)
_logger.Debug("Moving [{0}] > [{1}]", episodeFile.Path, destinationFilename); _logger.Debug("Moving [{0}] > [{1}]", episodeFile.Path, destinationFilename);
_diskProvider.MoveFile(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. //Wrapped in Try/Catch to prevent this from causing issues with remote NAS boxes, the move worked, which is more important.
try try
{ {

View File

@ -71,7 +71,7 @@ private static Series MapSeries(Show show)
series.ImdbId = show.imdb_id; series.ImdbId = show.imdb_id;
series.Title = show.title; series.Title = show.title;
series.CleanTitle = Parser.Parser.CleanSeriesTitle(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.FirstAired = FromIso(show.first_aired_iso);
series.Overview = show.overview; series.Overview = show.overview;
series.Runtime = show.runtime; series.Runtime = show.runtime;
@ -180,5 +180,14 @@ private static string GetSearchTerm(string phrase)
return 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;
}
} }
} }

View File

@ -234,12 +234,16 @@
<Compile Include="IndexerSearch\SeasonSearchService.cs" /> <Compile Include="IndexerSearch\SeasonSearchService.cs" />
<Compile Include="Indexers\BasicTorrentRssParser.cs" /> <Compile Include="Indexers\BasicTorrentRssParser.cs" />
<Compile Include="Indexers\DownloadProtocols.cs" /> <Compile Include="Indexers\DownloadProtocols.cs" />
<Compile Include="Indexers\Exceptions\ApiKeyException.cs" />
<Compile Include="Indexers\Eztv\Eztv.cs" /> <Compile Include="Indexers\Eztv\Eztv.cs" />
<Compile Include="Indexers\FetchAndParseRssService.cs" /> <Compile Include="Indexers\FetchAndParseRssService.cs" />
<Compile Include="Indexers\IIndexer.cs" /> <Compile Include="Indexers\IIndexer.cs" />
<Compile Include="Indexers\IndexerSettingUpdatedEvent.cs" /> <Compile Include="Indexers\IndexerSettingUpdatedEvent.cs" />
<Compile Include="Indexers\NewznabTestService.cs" />
<Compile Include="Indexers\IndexerWithSetting.cs" /> <Compile Include="Indexers\IndexerWithSetting.cs" />
<Compile Include="Indexers\IParseFeed.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\Newznab\SizeParsingException.cs" />
<Compile Include="Indexers\NullSetting.cs" /> <Compile Include="Indexers\NullSetting.cs" />
<Compile Include="Indexers\RssSyncCommand.cs" /> <Compile Include="Indexers\RssSyncCommand.cs" />
@ -546,6 +550,8 @@
<Compile Include="Tv\RefreshSeriesService.cs" /> <Compile Include="Tv\RefreshSeriesService.cs" />
<Compile Include="Update\Commands\ApplicationUpdateCommand.cs" /> <Compile Include="Update\Commands\ApplicationUpdateCommand.cs" />
<Compile Include="Update\InstallUpdateService.cs" /> <Compile Include="Update\InstallUpdateService.cs" />
<Compile Include="Update\RecentUpdateProvider.cs" />
<Compile Include="Update\UpdateChanges.cs" />
<Compile Include="Update\UpdatePackageAvailable.cs" /> <Compile Include="Update\UpdatePackageAvailable.cs" />
<Compile Include="Update\UpdatePackageProvider.cs" /> <Compile Include="Update\UpdatePackageProvider.cs" />
<Compile Include="Update\UpdatePackage.cs" /> <Compile Include="Update\UpdatePackage.cs" />

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv.Events; using NzbDrone.Core.Tv.Events;
@ -36,8 +37,9 @@ public void RefreshEpisodeInfo(Series series, IEnumerable<Episode> remoteEpisode
var updateList = new List<Episode>(); var updateList = new List<Episode>();
var newList = 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 try
{ {

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

View 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>();
}
}
}

View File

@ -13,5 +13,7 @@ public class UpdatePackage
public DateTime ReleaseDate { get; set; } public DateTime ReleaseDate { get; set; }
public String FileName { get; set; } public String FileName { get; set; }
public String Url { get; set; } public String Url { get; set; }
public UpdateChanges Changes { get; set; }
} }
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using NzbDrone.Common; using NzbDrone.Common;
using RestSharp; using RestSharp;
using NzbDrone.Core.Rest; using NzbDrone.Core.Rest;
@ -8,6 +9,7 @@ namespace NzbDrone.Core.Update
public interface IUpdatePackageProvider public interface IUpdatePackageProvider
{ {
UpdatePackage GetLatestUpdate(string branch, Version currentVersion); UpdatePackage GetLatestUpdate(string branch, Version currentVersion);
List<UpdatePackage> GetRecentUpdates(string branch);
} }
public class UpdatePackageProvider : IUpdatePackageProvider public class UpdatePackageProvider : IUpdatePackageProvider
@ -27,5 +29,18 @@ public UpdatePackage GetLatestUpdate(string branch, Version currentVersion)
return update.UpdatePackage; 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;
}
} }
} }

View File

@ -26,18 +26,21 @@ public void MakeAccessible()
{ {
if (IsFirewallEnabled()) if (IsFirewallEnabled())
{ {
if (IsNzbDronePortOpen()) if (!IsNzbDronePortOpen(_configFileProvider.Port))
{ {
_logger.Trace("NzbDrone port is already open, skipping."); _logger.Trace("Opening Port for NzbDrone: {0}", _configFileProvider.Port);
return; 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(int port)
private bool IsNzbDronePortOpen()
{ {
try try
{ {
@ -52,7 +55,7 @@ private bool IsNzbDronePortOpen()
foreach (INetFwOpenPort p in ports) foreach (INetFwOpenPort p in ports)
{ {
if (p.Port == _configFileProvider.Port) if (p.Port == port)
return true; return true;
} }
} }
@ -63,8 +66,6 @@ private bool IsNzbDronePortOpen()
return false; return false;
} }
private void OpenFirewallPort(int portNumber) private void OpenFirewallPort(int portNumber)
{ {
try try

View File

@ -36,7 +36,12 @@ public void Register()
return; 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); _netshProvider.Run(arguments);
} }

View File

@ -14,10 +14,10 @@ namespace NzbDrone.Integration.Test.Client
{ {
private readonly IRestClient _restClient; private readonly IRestClient _restClient;
private readonly string _resource; private readonly string _resource;
private readonly string _apiKey;
private readonly Logger _logger; private readonly Logger _logger;
public ClientBase(IRestClient restClient, string resource = null) public ClientBase(IRestClient restClient, string apiKey, string resource = null)
{ {
if (resource == null) if (resource == null)
{ {
@ -26,6 +26,7 @@ public ClientBase(IRestClient restClient, string resource = null)
_restClient = restClient; _restClient = restClient;
_resource = resource; _resource = resource;
_apiKey = apiKey;
_logger = LogManager.GetLogger("REST"); _logger = LogManager.GetLogger("REST");
} }
@ -88,10 +89,14 @@ public List<dynamic> InvalidPost(TResource body)
public RestRequest BuildRequest(string command = "") 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() public T Get<T>(IRestRequest request, HttpStatusCode statusCode = HttpStatusCode.OK) where T : class, new()

View File

@ -6,8 +6,8 @@ namespace NzbDrone.Integration.Test.Client
{ {
public class EpisodeClient : ClientBase<EpisodeResource> public class EpisodeClient : ClientBase<EpisodeResource>
{ {
public EpisodeClient(IRestClient restClient) public EpisodeClient(IRestClient restClient, string apiKey)
: base(restClient, "episodes") : base(restClient, apiKey, "episodes")
{ {
} }

View File

@ -5,12 +5,9 @@ namespace NzbDrone.Integration.Test.Client
{ {
public class IndexerClient : ClientBase<IndexerResource> public class IndexerClient : ClientBase<IndexerResource>
{ {
public IndexerClient(IRestClient restClient) public IndexerClient(IRestClient restClient, string apiKey)
: base(restClient) : base(restClient, apiKey)
{ {
} }
} }
} }

View File

@ -5,12 +5,9 @@ namespace NzbDrone.Integration.Test.Client
{ {
public class ReleaseClient : ClientBase<ReleaseResource> public class ReleaseClient : ClientBase<ReleaseResource>
{ {
public ReleaseClient(IRestClient restClient) public ReleaseClient(IRestClient restClient, string apiKey)
: base(restClient) : base(restClient, apiKey)
{ {
} }
} }
} }

View File

@ -7,8 +7,8 @@ namespace NzbDrone.Integration.Test.Client
{ {
public class SeriesClient : ClientBase<SeriesResource> public class SeriesClient : ClientBase<SeriesResource>
{ {
public SeriesClient(IRestClient restClient) public SeriesClient(IRestClient restClient, string apiKey)
: base(restClient) : base(restClient, apiKey)
{ {
} }
@ -27,14 +27,11 @@ public SeriesResource Get(string slug, HttpStatusCode statusCode = HttpStatusCod
} }
public class SystemInfoClient : ClientBase<SeriesResource> public class SystemInfoClient : ClientBase<SeriesResource>
{ {
public SystemInfoClient(IRestClient restClient) public SystemInfoClient(IRestClient restClient, string apiKey)
: base(restClient) : base(restClient, apiKey)
{ {
} }
} }
} }

View File

@ -47,22 +47,21 @@ public void SmokeTestSetup()
_runner = new NzbDroneRunner(); _runner = new NzbDroneRunner();
_runner.KillAll(); _runner.KillAll();
InitRestClients();
_runner.Start(); _runner.Start();
InitRestClients();
} }
private void InitRestClients() private void InitRestClients()
{ {
RestClient = new RestClient("http://localhost:8989/api"); RestClient = new RestClient("http://localhost:8989/api");
Series = new SeriesClient(RestClient); Series = new SeriesClient(RestClient, _runner.ApiKey);
Releases = new ReleaseClient(RestClient); Releases = new ReleaseClient(RestClient, _runner.ApiKey);
RootFolders = new ClientBase<RootFolderResource>(RestClient); RootFolders = new ClientBase<RootFolderResource>(RestClient, _runner.ApiKey);
Commands = new ClientBase<CommandResource>(RestClient); Commands = new ClientBase<CommandResource>(RestClient, _runner.ApiKey);
History = new ClientBase<HistoryResource>(RestClient); History = new ClientBase<HistoryResource>(RestClient, _runner.ApiKey);
Indexers = new IndexerClient(RestClient); Indexers = new IndexerClient(RestClient, _runner.ApiKey);
Episodes = new EpisodeClient(RestClient); Episodes = new EpisodeClient(RestClient, _runner.ApiKey);
NamingConfig = new ClientBase<NamingConfigResource>(RestClient, "config/naming"); NamingConfig = new ClientBase<NamingConfigResource>(RestClient, _runner.ApiKey, "config/naming");
} }
//[TestFixtureTearDown] //[TestFixtureTearDown]

View File

@ -1,11 +1,14 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
using System.Threading; using System.Threading;
using System.Xml.Linq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Processes; using NzbDrone.Common.Processes;
using NzbDrone.Core.Configuration;
using RestSharp; using RestSharp;
namespace NzbDrone.Integration.Test namespace NzbDrone.Integration.Test
@ -16,16 +19,18 @@ public class NzbDroneRunner
private readonly IRestClient _restClient; private readonly IRestClient _restClient;
private Process _nzbDroneProcess; private Process _nzbDroneProcess;
public string AppData { get; private set; }
public string ApiKey { get; private set; }
public NzbDroneRunner(int port = 8989) public NzbDroneRunner(int port = 8989)
{ {
_processProvider = new ProcessProvider(); _processProvider = new ProcessProvider();
_restClient = new RestClient("http://localhost:8989/api"); _restClient = new RestClient("http://localhost:8989/api");
} }
public void Start() 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"; var nzbdroneConsoleExe = "NzbDrone.Console.exe";
@ -34,7 +39,6 @@ public void Start()
nzbdroneConsoleExe = "NzbDrone.exe"; nzbdroneConsoleExe = "NzbDrone.exe";
} }
if (BuildInfo.IsDebug) if (BuildInfo.IsDebug)
{ {
@ -54,8 +58,12 @@ public void Start()
Assert.Fail("Process has exited"); 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) if (statusCall.ResponseStatus == ResponseStatus.Completed)
{ {
@ -77,7 +85,7 @@ public void KillAll()
private void Start(string outputNzbdroneConsoleExe) private void Start(string outputNzbdroneConsoleExe)
{ {
var args = "-nobrowser -data=\"" + AppDate + "\""; var args = "-nobrowser -data=\"" + AppData + "\"";
_nzbDroneProcess = _processProvider.Start(outputNzbdroneConsoleExe, args, OnOutputDataReceived, OnOutputDataReceived); _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;
}
} }
} }

View File

@ -1,25 +1,21 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Debug - Chrome" type="JavascriptDebugSession" factoryName="Remote" singleton="true"> <configuration default="false" name="Debug - Chrome" type="JavascriptDebugType" factoryName="JavaScript Debug" singleton="true" uri="http://localhost:8989">
<JSRemoteDebuggerConfigurationSettings> <mapping url="http://localhost:8989/Calendar" local-file="$PROJECT_DIR$/Calendar" />
<option name="engineId" value="chrome" /> <mapping url="http://localhost:8989/MainMenuView.js" local-file="$PROJECT_DIR$/MainMenuView.js" />
<option name="fileUrl" value="http://localhost:8989" /> <mapping url="http://localhost:8989/Settings" local-file="$PROJECT_DIR$/Settings" />
<mapping url="http://localhost:8989/Calendar" local-file="$PROJECT_DIR$/Calendar" /> <mapping url="http://localhost:8989/Upcoming" local-file="$PROJECT_DIR$/Upcoming" />
<mapping url="http://localhost:8989/MainMenuView.js" local-file="$PROJECT_DIR$/MainMenuView.js" /> <mapping url="http://localhost:8989/app.js" local-file="$PROJECT_DIR$/app.js" />
<mapping url="http://localhost:8989/Settings" local-file="$PROJECT_DIR$/Settings" /> <mapping url="http://localhost:8989/Mixins" local-file="$PROJECT_DIR$/Mixins" />
<mapping url="http://localhost:8989/Upcoming" local-file="$PROJECT_DIR$/Upcoming" /> <mapping url="http://localhost:8989/Missing" local-file="$PROJECT_DIR$/Missing" />
<mapping url="http://localhost:8989/app.js" local-file="$PROJECT_DIR$/app.js" /> <mapping url="http://localhost:8989/Quality" local-file="$PROJECT_DIR$/Quality" />
<mapping url="http://localhost:8989/Mixins" local-file="$PROJECT_DIR$/Mixins" /> <mapping url="http://localhost:8989/Config.js" local-file="$PROJECT_DIR$/Config.js" />
<mapping url="http://localhost:8989/Missing" local-file="$PROJECT_DIR$/Missing" /> <mapping url="http://localhost:8989/Shared" local-file="$PROJECT_DIR$/Shared" />
<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/Config.js" local-file="$PROJECT_DIR$/Config.js" /> <mapping url="http://localhost:8989/HeaderView.js" local-file="$PROJECT_DIR$/HeaderView.js" />
<mapping url="http://localhost:8989/Shared" local-file="$PROJECT_DIR$/Shared" /> <mapping url="http://localhost:8989" local-file="$PROJECT_DIR$" />
<mapping url="http://localhost:8989/AddSeries" local-file="$PROJECT_DIR$/AddSeries" /> <mapping url="http://localhost:8989/Routing.js" local-file="$PROJECT_DIR$/Routing.js" />
<mapping url="http://localhost:8989/HeaderView.js" local-file="$PROJECT_DIR$/HeaderView.js" /> <mapping url="http://localhost:8989/Controller.js" local-file="$PROJECT_DIR$/Controller.js" />
<mapping url="http://localhost:8989" local-file="$PROJECT_DIR$" /> <mapping url="http://localhost:8989/Series" local-file="$PROJECT_DIR$/Series" />
<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>
<RunnerSettings RunnerId="JavascriptDebugRunner" /> <RunnerSettings RunnerId="JavascriptDebugRunner" />
<ConfigurationWrapper RunnerId="JavascriptDebugRunner" /> <ConfigurationWrapper RunnerId="JavascriptDebugRunner" />
<method /> <method />

View File

@ -1,25 +1,21 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Debug - Firefox" type="JavascriptDebugSession" factoryName="Remote" singleton="true"> <configuration default="false" name="Debug - Firefox" type="JavascriptDebugType" factoryName="JavaScript Debug" singleton="true" engineId="firefox" uri="http://localhost:8989">
<JSRemoteDebuggerConfigurationSettings> <mapping url="http://localhost:8989/Calendar" local-file="$PROJECT_DIR$/Calendar" />
<option name="engineId" value="firefox" /> <mapping url="http://localhost:8989/MainMenuView.js" local-file="$PROJECT_DIR$/MainMenuView.js" />
<option name="fileUrl" value="http://localhost:8989" /> <mapping url="http://localhost:8989/Settings" local-file="$PROJECT_DIR$/Settings" />
<mapping url="http://localhost:8989/Calendar" local-file="$PROJECT_DIR$/Calendar" /> <mapping url="http://localhost:8989/Upcoming" local-file="$PROJECT_DIR$/Upcoming" />
<mapping url="http://localhost:8989/MainMenuView.js" local-file="$PROJECT_DIR$/MainMenuView.js" /> <mapping url="http://localhost:8989/app.js" local-file="$PROJECT_DIR$/app.js" />
<mapping url="http://localhost:8989/Settings" local-file="$PROJECT_DIR$/Settings" /> <mapping url="http://localhost:8989/Mixins" local-file="$PROJECT_DIR$/Mixins" />
<mapping url="http://localhost:8989/Upcoming" local-file="$PROJECT_DIR$/Upcoming" /> <mapping url="http://localhost:8989/Missing" local-file="$PROJECT_DIR$/Missing" />
<mapping url="http://localhost:8989/app.js" local-file="$PROJECT_DIR$/app.js" /> <mapping url="http://localhost:8989/Config.js" local-file="$PROJECT_DIR$/Config.js" />
<mapping url="http://localhost:8989/Mixins" local-file="$PROJECT_DIR$/Mixins" /> <mapping url="http://localhost:8989/Quality" local-file="$PROJECT_DIR$/Quality" />
<mapping url="http://localhost:8989/Missing" local-file="$PROJECT_DIR$/Missing" /> <mapping url="http://localhost:8989/AddSeries" local-file="$PROJECT_DIR$/AddSeries" />
<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/Quality" local-file="$PROJECT_DIR$/Quality" /> <mapping url="http://localhost:8989/HeaderView.js" local-file="$PROJECT_DIR$/HeaderView.js" />
<mapping url="http://localhost:8989/AddSeries" local-file="$PROJECT_DIR$/AddSeries" /> <mapping url="http://localhost:8989" local-file="$PROJECT_DIR$" />
<mapping url="http://localhost:8989/Shared" local-file="$PROJECT_DIR$/Shared" /> <mapping url="http://localhost:8989/Routing.js" local-file="$PROJECT_DIR$/Routing.js" />
<mapping url="http://localhost:8989/HeaderView.js" local-file="$PROJECT_DIR$/HeaderView.js" /> <mapping url="http://localhost:8989/Controller.js" local-file="$PROJECT_DIR$/Controller.js" />
<mapping url="http://localhost:8989" local-file="$PROJECT_DIR$" /> <mapping url="http://localhost:8989/Series" local-file="$PROJECT_DIR$/Series" />
<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>
<RunnerSettings RunnerId="JavascriptDebugRunner" /> <RunnerSettings RunnerId="JavascriptDebugRunner" />
<ConfigurationWrapper RunnerId="JavascriptDebugRunner" /> <ConfigurationWrapper RunnerId="JavascriptDebugRunner" />
<method /> <method />

View File

@ -4,7 +4,7 @@ define(
'app', 'app',
'marionette', 'marionette',
'AddSeries/RootFolders/Layout', 'AddSeries/RootFolders/Layout',
'AddSeries/Existing/CollectionView', 'AddSeries/Existing/AddExistingSeriesCollectionView',
'AddSeries/AddSeriesView', 'AddSeries/AddSeriesView',
'Quality/QualityProfileCollection', 'Quality/QualityProfileCollection',
'AddSeries/RootFolders/Collection', 'AddSeries/RootFolders/Collection',
@ -15,8 +15,7 @@ define(
ExistingSeriesCollectionView, ExistingSeriesCollectionView,
AddSeriesView, AddSeriesView,
QualityProfileCollection, QualityProfileCollection,
RootFolderCollection, RootFolderCollection) {
SeriesCollection) {
return Marionette.Layout.extend({ return Marionette.Layout.extend({
template: 'AddSeries/AddSeriesLayoutTemplate', template: 'AddSeries/AddSeriesLayoutTemplate',
@ -35,8 +34,6 @@ define(
}, },
initialize: function () { initialize: function () {
SeriesCollection.fetch();
QualityProfileCollection.fetch(); QualityProfileCollection.fetch();
RootFolderCollection.promise = RootFolderCollection.fetch(); RootFolderCollection.promise = RootFolderCollection.fetch();
}, },

View File

@ -3,14 +3,14 @@ define(
[ [
'app', 'app',
'marionette', 'marionette',
'AddSeries/Collection', 'AddSeries/AddSeriesCollection',
'AddSeries/SearchResultCollectionView', 'AddSeries/SearchResultCollectionView',
'AddSeries/NotFoundView', 'AddSeries/NotFoundView',
'Shared/LoadingView', 'Shared/LoadingView',
'underscore' 'underscore'
], function (App, Marionette, AddSeriesCollection, SearchResultCollectionView, NotFoundView, LoadingView, _) { ], function (App, Marionette, AddSeriesCollection, SearchResultCollectionView, NotFoundView, LoadingView, _) {
return Marionette.Layout.extend({ return Marionette.Layout.extend({
template: 'AddSeries/AddSeriesTemplate', template: 'AddSeries/AddSeriesViewTemplate',
regions: { regions: {
searchResult: '#search-result' searchResult: '#search-result'
@ -36,12 +36,12 @@ define(
if (this.isExisting) { if (this.isExisting) {
this.className = 'existing-series'; this.className = 'existing-series';
this.listenTo(App.vent, App.Events.SeriesAdded, this._onSeriesAdded);
} }
else { else {
this.className = 'new-series'; this.className = 'new-series';
} }
this.listenTo(App.vent, App.Events.SeriesAdded, this._onSeriesAdded);
this.listenTo(this.collection, 'sync', this._showResults); this.listenTo(this.collection, 'sync', this._showResults);
this.resultCollectionView = new SearchResultCollectionView({ this.resultCollectionView = new SearchResultCollectionView({
@ -52,21 +52,6 @@ define(
this.throttledSearch = _.debounce(this.search, 1000, {trailing: true}).bind(this); 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 () { onRender: function () {
var self = this; var self = this;
@ -77,7 +62,7 @@ define(
self._abortExistingSearch(); self._abortExistingSearch();
self.throttledSearch({ self.throttledSearch({
term: self.ui.seriesSearch.val() term: self.ui.seriesSearch.val()
}) });
}); });
if (this.isExisting) { if (this.isExisting) {
@ -87,6 +72,7 @@ define(
onShow: function () { onShow: function () {
this.searchResult.show(this.resultCollectionView); this.searchResult.show(this.resultCollectionView);
this.ui.seriesSearch.focus();
}, },
search: function (options) { search: function (options) {
@ -106,6 +92,28 @@ define(
return this.currentSearchPromise; 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 () { _showResults: function () {
if (!this.isClosed) { if (!this.isClosed) {

View File

@ -29,9 +29,11 @@ define(
this.addItemView(model, this.getItemView(), index); this.addItemView(model, this.getItemView(), index);
this.children.findByModel(model) this.children.findByModel(model)
.search({term: folderName}) .search({term: folderName})
.always((function () { .always(function () {
self._showAndSearch(currentIndex + 1); if (!self.isClosed) {
})); self._showAndSearch(currentIndex + 1);
}
});
} }
}, },

View File

@ -2,8 +2,7 @@
define( define(
[ [
'marionette', 'marionette',
'AddSeries/SearchResultView', 'AddSeries/SearchResultView'
], function (Marionette, SearchResultView) { ], function (Marionette, SearchResultView) {
return Marionette.CollectionView.extend({ return Marionette.CollectionView.extend({

View File

@ -2,6 +2,7 @@
define( define(
[ [
'app', 'app',
'underscore',
'marionette', 'marionette',
'Quality/QualityProfileCollection', 'Quality/QualityProfileCollection',
'AddSeries/RootFolders/Collection', 'AddSeries/RootFolders/Collection',
@ -11,7 +12,7 @@ define(
'Shared/Messenger', 'Shared/Messenger',
'Mixins/AsValidatedView', 'Mixins/AsValidatedView',
'jquery.dotdotdot' '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({ var view = Marionette.ItemView.extend({
@ -37,6 +38,9 @@ define(
throw 'model is required'; throw 'model is required';
} }
this.templateHelpers = {};
this._configureTemplateHelpers();
this.listenTo(App.vent, Config.Events.ConfigUpdatedEvent, this._onConfigUpdated); this.listenTo(App.vent, Config.Events.ConfigUpdatedEvent, this._onConfigUpdated);
this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'change', this.render);
this.listenTo(RootFolders, 'all', this.render); this.listenTo(RootFolders, 'all', this.render);
@ -71,22 +75,18 @@ define(
}); });
}, },
serializeData: function () { _configureTemplateHelpers: function () {
var data = this.model.toJSON();
var existingSeries = SeriesCollection.where({tvdbId: this.model.get('tvdbId')}); var existingSeries = SeriesCollection.where({tvdbId: this.model.get('tvdbId')});
if (existingSeries.length > 0) { 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) { if (!this.model.get('isExisting')) {
data.rootFolders = RootFolders.toJSON(); this.templateHelpers.rootFolders = RootFolders.toJSON();
} }
return data;
}, },
_onConfigUpdated: function (options) { _onConfigUpdated: function (options) {
@ -134,17 +134,23 @@ define(
SeriesCollection.add(this.model); SeriesCollection.add(this.model);
this.model.save().done(function () {
var promise = this.model.save();
promise.done(function () {
self.close(); self.close();
icon.removeClass('icon-spin icon-spinner disabled').addClass('icon-search'); icon.removeClass('icon-spin icon-spinner disabled').addClass('icon-search');
Messenger.show({ Messenger.show({
message: 'Added: ' + self.model.get('title') message: 'Added: ' + self.model.get('title')
}); });
App.vent.trigger(App.Events.SeriesAdded, { series: self.model }); 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');
});
} }
}); });

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

View File

@ -1,56 +1,66 @@
'use strict'; 'use strict';
define( define(
[ [
'app',
'Commands/CommandModel', 'Commands/CommandModel',
'Commands/CommandCollection', 'Commands/CommandCollection',
'underscore', 'underscore',
'jQuery/jquery.spin' '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 () { var commandModel = new CommandModel(attr);
CommandCollection.add(commandModel);
});
},
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) { var existingCommand = CommandCollection.findCommand(options.command);
this._bindToCommandModel.call(this, existingCommand, options);
}
CommandCollection.bind('add sync', function (model) { if (existingCommand) {
if (model.isSameCommand(options.command)) { this._bindToCommandModel.call(this, existingCommand, options);
self._bindToCommandModel.call(self, model, 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()) { _bindToCommandModel: function bindToCommand(model, options) {
options.element.stopSpin();
return;
}
model.bind('change:state', function (model) {
if (!model.isActive()) { if (!model.isActive()) {
options.element.stopSpin(); 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();
}); });

View File

@ -11,13 +11,9 @@ define(
return response; return response;
}, },
isActive: function () {
return this.get('state') !== 'completed' && this.get('state') !== 'failed';
},
isSameCommand: function (command) { isSameCommand: function (command) {
if (command.name.toLocaleLowerCase() != this.get('name').toLocaleLowerCase()) { if (command.name.toLocaleLowerCase() !== this.get('name').toLocaleLowerCase()) {
return false; return false;
} }
@ -28,6 +24,14 @@ define(
} }
return true; return true;
},
isActive: function () {
return this.get('state') !== 'completed' && this.get('state') !== 'failed';
},
isComplete: function () {
return this.get('state') === 'completed';
} }
}); });
}); });

View File

@ -12,6 +12,10 @@ define(
DefaultRootFolderId: 'DefaultRootFolderId' DefaultRootFolderId: 'DefaultRootFolderId'
}, },
getValueBoolean: function (key, defaultValue) {
return this.getValue(key, defaultValue) === 'true';
},
getValue: function (key, defaultValue) { getValue: function (key, defaultValue) {
var storeValue = localStorage.getItem(key); var storeValue = localStorage.getItem(key);
@ -35,6 +39,5 @@ define(
App.vent.trigger(this.Events.ConfigUpdatedEvent, {key: key, value: value}); App.vent.trigger(this.Events.ConfigUpdatedEvent, {key: key, value: value});
} }
}; };
}); });

View File

@ -5,7 +5,7 @@
.slide-button { .slide-button {
.buttonBackground(@btnDangerBackground, @btnDangerBackgroundHighlight); .buttonBackground(@btnDangerBackground, @btnDangerBackgroundHighlight);
&.btn-danger { &.btn-danger, &.btn-warning {
.buttonBackground(@btnInverseBackground, @btnInverseBackgroundHighlight); .buttonBackground(@btnInverseBackground, @btnInverseBackgroundHighlight);
} }
} }
@ -16,5 +16,9 @@
&.btn-danger { &.btn-danger {
.buttonBackground(@btnDangerBackground, @btnDangerBackgroundHighlight); .buttonBackground(@btnDangerBackground, @btnDangerBackgroundHighlight);
} }
&.btn-warning {
.buttonBackground(@btnWarningBackground, @btnWarningBackgroundHighlight);
}
} }
} }

View File

@ -162,6 +162,10 @@ footer {
color : @successText; color : @successText;
} }
.status-warning {
color : @warningText;
}
.status-danger { .status-danger {
color : @errorText; color : @errorText;
} }

View File

@ -15,11 +15,12 @@ define(
'Logs/Files/Layout', 'Logs/Files/Layout',
'Release/Layout', 'Release/Layout',
'System/Layout', 'System/Layout',
'SeasonPass/Layout', 'SeasonPass/SeasonPassLayout',
'Update/UpdateLayout',
'Shared/NotFoundView', 'Shared/NotFoundView',
'Shared/Modal/Region' 'Shared/Modal/Region'
], function (App, Marionette, HistoryLayout, SettingsLayout, AddSeriesLayout, SeriesIndexLayout, SeriesDetailsLayout, SeriesCollection, MissingLayout, CalendarLayout, ], 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({ return Marionette.Controller.extend({
series: function () { series: function () {
@ -94,6 +95,11 @@ define(
App.mainRegion.show(new SeasonPassLayout()); App.mainRegion.show(new SeasonPassLayout());
}, },
update: function () {
this._setTitle('Updates');
App.mainRegion.show(new UpdateLayout());
},
notFound: function () { notFound: function () {
this._setTitle('Not Found'); this._setTitle('Not Found');
App.mainRegion.show(new NotFoundView(this)); App.mainRegion.show(new NotFoundView(this));

View File

@ -6,9 +6,10 @@ define(
'Cells/FileSizeCell', 'Cells/FileSizeCell',
'Cells/QualityCell', 'Cells/QualityCell',
'Cells/ApprovalStatusCell', '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({ return Marionette.Layout.extend({
template: 'Episode/Search/ManualLayoutTemplate', template: 'Episode/Search/ManualLayoutTemplate',
@ -44,10 +45,11 @@ define(
cell : FileSizeCell cell : FileSizeCell
}, },
{ {
name : 'quality', name : 'quality',
label : 'Quality', label : 'Quality',
sortable: true, sortable : true,
cell : QualityCell cell : QualityCell,
headerCell: QualityHeaderCell
}, },
{ {

View File

@ -57,10 +57,10 @@ define(
} }
if (seasonCount === 1) { 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 () { Handlebars.registerHelper('titleWithYear', function () {

View 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 '';
});
});

View File

@ -9,6 +9,7 @@ define(
'Handlebars/Helpers/Episode', 'Handlebars/Helpers/Episode',
'Handlebars/Helpers/Series', 'Handlebars/Helpers/Series',
'Handlebars/Helpers/Quality', 'Handlebars/Helpers/Quality',
'Handlebars/Helpers/Version',
'Handlebars/Handlebars.Debug' 'Handlebars/Handlebars.Debug'
], function (Templates) { ], function (Templates) {
return function () { return function () {

View File

@ -60,9 +60,10 @@ define(
cell : EpisodeTitleCell cell : EpisodeTitleCell
}, },
{ {
name : 'quality', name : 'quality',
label: 'Quality', label : 'Quality',
cell : QualityCell cell : QualityCell,
sortable: false
}, },
{ {
name : 'date', name : 'date',

View File

@ -13,7 +13,7 @@
var filename = a.pathname.split('/').pop(); var filename = a.pathname.split('/').pop();
//Suppress Firefox debug errors when console window is closed //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; return false;
} }

View File

@ -20,9 +20,12 @@ define(function () {
delete xhr.data; delete xhr.data;
} }
if (xhr) {
xhr.headers = xhr.headers || {};
xhr.headers['Authorization'] = window.NzbDrone.ApiKey;
}
return original.apply(this, arguments); return original.apply(this, arguments);
}; };
}; };
}); });

View File

@ -50,7 +50,7 @@
</a> </a>
</li> </li>
<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> <i class="icon-nd-donate"></i>
<br> <br>
Donate Donate

View File

@ -4,12 +4,21 @@ define(
'app', 'app',
'Series/SeriesCollection' 'Series/SeriesCollection'
], function (App, 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 () { $.fn.bindSearch = function () {
$(this).typeahead({ $(this).typeahead({
source : function () { source : function () {
return SeriesCollection.map(function (model) { return SeriesCollection.pluck('title');
return model.get('title');
});
}, },
sorter: function (items) { sorter: function (items) {
@ -17,9 +26,7 @@ define(
}, },
updater: function (item) { updater: function (item) {
var series = SeriesCollection.find(function (model) { var series = SeriesCollection.findWhere({ title: item });
return model.get('title') === item;
});
this.$element.blur(); this.$element.blur();
App.Router.navigate('/series/{0}'.format(series.get('titleSlug')), { trigger: true }); App.Router.navigate('/series/{0}'.format(series.get('titleSlug')), { trigger: true });

View File

@ -1,15 +1,13 @@
'use strict'; 'use strict';
define( define(
[ [
'Release/Model', 'backbone',
'backbone.pageable' 'Release/Model'
], function (ReleaseModel, PagableCollection) { ], function (Backbone, ReleaseModel) {
return PagableCollection.extend({ return Backbone.Collection.extend({
url : window.NzbDrone.ApiRoot + '/release', url : window.NzbDrone.ApiRoot + '/release',
model: ReleaseModel, model: ReleaseModel,
mode: 'client',
state: { state: {
pageSize: 2000 pageSize: 2000
}, },

View File

@ -31,6 +31,7 @@ require(
'rss' : 'rss', 'rss' : 'rss',
'system' : 'system', 'system' : 'system',
'seasonpass' : 'seasonPass', 'seasonpass' : 'seasonPass',
'update' : 'update',
':whatever' : 'notFound' ':whatever' : 'notFound'
} }
}); });

View File

@ -12,7 +12,7 @@ define(
SeriesCollectionView, SeriesCollectionView,
LoadingView) { LoadingView) {
return Marionette.Layout.extend({ return Marionette.Layout.extend({
template: 'SeasonPass/LayoutTemplate', template: 'SeasonPass/SeasonPassLayoutTemplate',
regions: { regions: {
series: '#x-series' series: '#x-series'

View File

@ -1,24 +1,28 @@
'use strict'; 'use strict';
define( define(
[ [
'underscore',
'marionette', 'marionette',
'backgrid', 'backgrid',
'Series/SeasonCollection' 'Series/SeasonCollection'
], function (Marionette, Backgrid, SeasonCollection) { ], function (_, Marionette, Backgrid, SeasonCollection) {
return Marionette.Layout.extend({ return Marionette.Layout.extend({
template: 'SeasonPass/SeriesLayoutTemplate', template: 'SeasonPass/SeriesLayoutTemplate',
ui: { ui: {
seasonSelect: '.x-season-select', seasonSelect : '.x-season-select',
expander : '.x-expander', expander : '.x-expander',
seasonGrid : '.x-season-grid' seasonGrid : '.x-season-grid',
seriesMonitored: '.x-series-monitored'
}, },
events: { events: {
'change .x-season-select': '_seasonSelected', 'change .x-season-select' : '_seasonSelected',
'click .x-expander' : '_expand', 'click .x-expander' : '_expand',
'click .x-latest' : '_latest', 'click .x-latest' : '_latest',
'click .x-monitored' : '_toggleSeasonMonitored' 'click .x-all' : '_all',
'click .x-monitored' : '_toggleSeasonMonitored',
'click .x-series-monitored': '_toggleSeriesMonitored'
}, },
regions: { regions: {
@ -26,6 +30,7 @@ define(
}, },
initialize: function () { initialize: function () {
this.listenTo(this.model, 'sync', this._setSeriesMonitoredState);
this.seasonCollection = new SeasonCollection(this.model.get('seasons')); this.seasonCollection = new SeasonCollection(this.model.get('seasons'));
this.expanded = false; this.expanded = false;
}, },
@ -36,16 +41,17 @@ define(
} }
this._setExpanderIcon(); this._setExpanderIcon();
this._setSeriesMonitoredState();
}, },
_seasonSelected: function () { _seasonSelected: function () {
var seasonNumber = parseInt(this.ui.seasonSelect.val()); var seasonNumber = parseInt(this.ui.seasonSelect.val());
if (seasonNumber == -1 || isNaN(seasonNumber)) { if (seasonNumber === -1 || isNaN(seasonNumber)) {
return; return;
} }
this._setMonitored(seasonNumber) this._setSeasonMonitored(seasonNumber);
}, },
_expand: function () { _expand: function () {
@ -79,10 +85,16 @@ define(
return s.seasonNumber; 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; var self = this;
this.model.setSeasonPass(seasonNumber); this.model.setSeasonPass(seasonNumber);
@ -118,6 +130,29 @@ define(
_afterToggleSeasonMonitored: function () { _afterToggleSeasonMonitored: function () {
this.render(); 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);
} }
}); });
}); });

View File

@ -1,8 +1,8 @@
<div class="seasonpass-series"> <div class="seasonpass-series">
<div class="row"> <div class="row">
<div class="span11"> <div class="span12">
<i class="icon-chevron-right x-expander expander pull-left"/> <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"> <span class="title span5">
<a href="{{route}}"> <a href="{{route}}">
{{title}} {{title}}
@ -26,10 +26,20 @@
</span> </span>
</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"> <span class="help-inline">
<i class="icon-question-sign" title="Will quickly select the latest season as first monitored"/> <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> </span>
</div> </div>
</div> </div>

View File

@ -9,13 +9,17 @@
{{/if}} {{/if}}
{{#if_eq episodeCount compare=0}} {{#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}} {{else}}
{{#if_eq percentOfEpisodes compare=100}} {{#if_eq percentOfEpisodes compare=100}}
<i class="icon-nd-status season-status status-success" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded"/> <i class="icon-nd-status season-status status-success" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded"/>
{{else}} {{else}}
<i class="icon-nd-status season-status status-danger" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded"/> <i class="icon-nd-status season-status status-danger" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded"/>
{{/if_eq}} {{/if_eq}}
{{/if_eq}} {{/if_eq}}
<span class="season-actions pull-right"> <span class="season-actions pull-right">

View File

@ -44,6 +44,8 @@ define(
this.listenTo(this.model, 'change:monitored', this._setMonitoredState); this.listenTo(this.model, 'change:monitored', this._setMonitoredState);
this.listenTo(App.vent, App.Events.SeriesDeleted, this._onSeriesDeleted); this.listenTo(App.vent, App.Events.SeriesDeleted, this._onSeriesDeleted);
this.listenTo(App.vent, App.Events.SeasonRenamed, this._onSeasonRenamed); this.listenTo(App.vent, App.Events.SeasonRenamed, this._onSeasonRenamed);
App.vent.on(App.Events.CommandComplete, this._commandComplete, this);
}, },
onShow: function () { onShow: function () {
@ -195,6 +197,16 @@ define(
if (this.model.get('id') === event.series.get('id')) { if (this.model.get('id') === event.series.get('id')) {
this.episodeFileCollection.fetch(); 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();
}
}
} }
}); });
}); });

View File

@ -2,7 +2,7 @@
<div class="span11"> <div class="span11">
<div class="row"> <div class="row">
<h1> <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}} {{title}}
<div class="series-actions pull-right"> <div class="series-actions pull-right">
<div class="x-refresh"> <div class="x-refresh">

View File

@ -274,3 +274,16 @@
font-size : 16px; font-size : 16px;
vertical-align : middle !important; vertical-align : middle !important;
} }
.seasonpass-series {
.season-pass-button {
display: inline-block;
width: 120px;
}
.series-monitor-toggle {
font-size: 24px;
margin-top: 3px;
}
}

View File

@ -1,10 +1,12 @@
window.NzbDrone = {};
window.NzbDrone.ApiRoot = '/api'; window.NzbDrone.ApiRoot = '/api';
var statusText = $.ajax({ var statusText = $.ajax({
type : 'GET', type : 'GET',
url : window.NzbDrone.ApiRoot + '/system/status', url : window.NzbDrone.ApiRoot + '/system/status',
async: false async: false,
headers: {
Authorization: window.NzbDrone.ApiKey
}
}).responseText; }).responseText;
window.NzbDrone.ServerStatus = JSON.parse(statusText); window.NzbDrone.ServerStatus = JSON.parse(statusText);

View File

@ -7,11 +7,49 @@
<div class="controls"> <div class="controls">
<input type="number" placeholder="8989" name="port"/> <input type="number" placeholder="8989" name="port"/>
<span> <span>
<i class="icon-nd-form-warning" title="Requires restart to take effect"/> <i class="icon-nd-form-warning" title="Requires restart to take effect"/>
</span> </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>
<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>
<div class="control-group"> <div class="control-group">
@ -29,9 +67,9 @@
<div class="btn btn-primary slide-button"/> <div class="btn btn-primary slide-button"/>
</label> </label>
<span class="help-inline-checkbox"> <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"/> <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> </span>
</div> </div>
</div> </div>
</fieldset> </fieldset>
@ -51,7 +89,7 @@
</label> </label>
<span class="help-inline-checkbox"> <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> </span>
</div> </div>
</div> </div>
@ -91,8 +129,7 @@
</div> </div>
</fieldset> </fieldset>
{{#unless_eq branch compare="master"}} <fieldset class="advanced-setting">
<fieldset>
<legend>Development</legend> <legend>Development</legend>
<div class="alert"> <div class="alert">
<i class="icon-nd-warning"></i> <i class="icon-nd-warning"></i>
@ -106,5 +143,4 @@
</div> </div>
</div> </div>
</fieldset> </fieldset>
{{/unless_eq}}
</div> </div>

View File

@ -5,38 +5,56 @@ define(
'Mixins/AsModelBoundView' 'Mixins/AsModelBoundView'
], function (Marionette, AsModelBoundView) { ], function (Marionette, AsModelBoundView) {
var view = Marionette.ItemView.extend({ var view = Marionette.ItemView.extend({
template: 'Settings/General/GeneralTemplate', template: 'Settings/General/GeneralTemplate',
events: { events: {
'change .x-auth': '_setAuthOptionsVisibility' 'change .x-auth': '_setAuthOptionsVisibility',
}, 'change .x-ssl': '_setSslOptionsVisibility'
},
ui: { ui: {
authToggle : '.x-auth', authToggle : '.x-auth',
authOptions: '.x-auth-options' authOptions: '.x-auth-options',
}, sslToggle : '.x-ssl',
sslOptions: '.x-ssl-options'
},
onRender: function(){
onRender: function(){ if(!this.ui.authToggle.prop('checked')){
if(!this.ui.authToggle.prop('checked')){ this.ui.authOptions.hide();
this.ui.authOptions.hide();
}
},
_setAuthOptionsVisibility: function () {
var showAuthOptions = this.ui.authToggle.prop('checked');
if (showAuthOptions) {
this.ui.authOptions.slideDown();
}
else {
this.ui.authOptions.slideUp();
}
} }
}); 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); return AsModelBoundView.call(view);
}); });

View File

@ -39,6 +39,9 @@
{{#if id}} {{#if id}}
<button class="btn btn-danger pull-left x-remove">delete</button> <button class="btn btn-danger pull-left x-remove">delete</button>
{{/if}} {{/if}}
<span class="x-activity"></span>
<button class="btn" data-dismiss="modal">cancel</button> <button class="btn" data-dismiss="modal">cancel</button>
<div class="btn-group"> <div class="btn-group">

View File

@ -11,6 +11,10 @@ define(
var view = Marionette.ItemView.extend({ var view = Marionette.ItemView.extend({
template: 'Settings/Indexers/EditTemplate', template: 'Settings/Indexers/EditTemplate',
ui : {
activity: '.x-activity'
},
events: { events: {
'click .x-save' : '_save', 'click .x-save' : '_save',
'click .x-save-and-add': '_saveAndAdd' 'click .x-save-and-add': '_saveAndAdd'
@ -21,6 +25,8 @@ define(
}, },
_save: function () { _save: function () {
this.ui.activity.html('<i class="icon-nd-spinner"></i>');
var self = this; var self = this;
var promise = this.model.saveSettings(); var promise = this.model.saveSettings();
@ -29,10 +35,16 @@ define(
self.indexerCollection.add(self.model, { merge: true }); self.indexerCollection.add(self.model, { merge: true });
App.vent.trigger(App.Commands.CloseModalCommand); App.vent.trigger(App.Commands.CloseModalCommand);
}); });
promise.fail(function () {
self.ui.activity.empty();
});
} }
}, },
_saveAndAdd: function () { _saveAndAdd: function () {
this.ui.activity.html('<i class="icon-nd-spinner"></i>');
var self = this; var self = this;
var promise = this.model.saveSettings(); var promise = this.model.saveSettings();
@ -50,6 +62,10 @@ define(
self.model.set('fields.' + key + '.value', ''); self.model.set('fields.' + key + '.value', '');
}); });
}); });
promise.fail(function () {
self.ui.activity.empty();
});
} }
} }
}); });

View File

@ -4,10 +4,10 @@ define(
[ [
'marionette', 'marionette',
'Settings/Indexers/CollectionView', 'Settings/Indexers/CollectionView',
'Settings/Indexers/Options/View' 'Settings/Indexers/Options/IndexerOptionsView'
], function (Marionette, CollectionView, OptionsView) { ], function (Marionette, CollectionView, OptionsView) {
return Marionette.Layout.extend({ return Marionette.Layout.extend({
template: 'Settings/Indexers/LayoutTemplate', template: 'Settings/Indexers/IndexerLayoutTemplate',
regions: { regions: {
indexersRegion : '#indexers-collection', indexersRegion : '#indexers-collection',

View File

@ -6,7 +6,7 @@ define(
], function (Marionette, AsModelBoundView) { ], function (Marionette, AsModelBoundView) {
var view = Marionette.ItemView.extend({ var view = Marionette.ItemView.extend({
template: 'Settings/MediaManagement/FileManagement/ViewTemplate' template: 'Settings/Indexers/Options/IndexerOptionsViewTemplate'
}); });
return AsModelBoundView.call(view); return AsModelBoundView.call(view);

View File

@ -9,7 +9,7 @@
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group advanced-setting">
<label class="control-label">RSS Sync Interval</label> <label class="control-label">RSS Sync Interval</label>
<div class="controls"> <div class="controls">
@ -21,7 +21,7 @@
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group advanced-setting">
<label class="control-label">Release Restrictions</label> <label class="control-label">Release Restrictions</label>
<div class="controls"> <div class="controls">

View File

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

View File

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

View File

@ -1,4 +1,4 @@
<fieldset> <fieldset class="advanced-setting">
<legend>File Management</legend> <legend>File Management</legend>
<div class="control-group"> <div class="control-group">
@ -40,4 +40,15 @@
</span> </span>
</div> </div>
</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> </fieldset>

View File

@ -5,10 +5,10 @@ define(
'marionette', 'marionette',
'Settings/MediaManagement/Naming/View', 'Settings/MediaManagement/Naming/View',
'Settings/MediaManagement/Sorting/View', 'Settings/MediaManagement/Sorting/View',
'Settings/MediaManagement/FileManagement/View' 'Settings/MediaManagement/FileManagement/FileManagementView'
], function (Marionette, NamingView, SortingView, FileManagementView) { ], function (Marionette, NamingView, SortingView, FileManagementView) {
return Marionette.Layout.extend({ return Marionette.Layout.extend({
template: 'Settings/MediaManagement/LayoutTemplate', template: 'Settings/MediaManagement/MediaManagementLayoutTemplate',
regions: { regions: {
episodeNaming : '#episode-naming', episodeNaming : '#episode-naming',

View File

@ -6,15 +6,16 @@ define(
'Settings/SettingsModel', 'Settings/SettingsModel',
'Settings/General/GeneralSettingsModel', 'Settings/General/GeneralSettingsModel',
'Settings/MediaManagement/Naming/Model', 'Settings/MediaManagement/Naming/Model',
'Settings/MediaManagement/Layout', 'Settings/MediaManagement/MediaManagementLayout',
'Settings/Quality/QualityLayout', 'Settings/Quality/QualityLayout',
'Settings/Indexers/Layout', 'Settings/Indexers/IndexerLayout',
'Settings/Indexers/Collection', 'Settings/Indexers/Collection',
'Settings/DownloadClient/Layout', 'Settings/DownloadClient/Layout',
'Settings/Notifications/CollectionView', 'Settings/Notifications/CollectionView',
'Settings/Notifications/Collection', 'Settings/Notifications/Collection',
'Settings/General/GeneralView', 'Settings/General/GeneralView',
'Shared/LoadingView' 'Shared/LoadingView',
'Config'
], function (App, ], function (App,
Marionette, Marionette,
SettingsModel, SettingsModel,
@ -28,7 +29,8 @@ define(
NotificationCollectionView, NotificationCollectionView,
NotificationCollection, NotificationCollection,
GeneralView, GeneralView,
LoadingView) { LoadingView,
Config) {
return Marionette.Layout.extend({ return Marionette.Layout.extend({
template: 'Settings/SettingsLayoutTemplate', template: 'Settings/SettingsLayoutTemplate',
@ -48,7 +50,8 @@ define(
indexersTab : '.x-indexers-tab', indexersTab : '.x-indexers-tab',
downloadClientTab : '.x-download-client-tab', downloadClientTab : '.x-download-client-tab',
notificationsTab : '.x-notifications-tab', notificationsTab : '.x-notifications-tab',
generalTab : '.x-general-tab' generalTab : '.x-general-tab',
advancedSettings : '.x-advanced-settings'
}, },
events: { events: {
@ -58,7 +61,67 @@ define(
'click .x-download-client-tab' : '_showDownloadClient', 'click .x-download-client-tab' : '_showDownloadClient',
'click .x-notifications-tab' : '_showNotifications', 'click .x-notifications-tab' : '_showNotifications',
'click .x-general-tab' : '_showGeneral', '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) { _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 () { _save: function () {
App.vent.trigger(App.Commands.SaveSettings); 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');
}
} }
}); });
}); });

View File

@ -6,6 +6,19 @@
<li><a href="#notifications" class="x-notifications-tab no-router">Connect</a></li> <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><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"><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> </ul>
<div class="tab-content"> <div class="tab-content">

View File

@ -1,5 +1,5 @@
@import "../Content/Bootstrap/variables";
@import "../Shared/Styles/clickable.less"; @import "../Shared/Styles/clickable.less";
@import "Indexers/indexers"; @import "Indexers/indexers";
@import "Quality/quality"; @import "Quality/quality";
@import "Notifications/notifications"; @import "Notifications/notifications";
@ -43,4 +43,38 @@ li.save-and-add:hover {
.naming-example { .naming-example {
display: inline-block; display: inline-block;
margin-top: 5px; 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;
}
} }

View File

@ -23,7 +23,7 @@ define(
var leftVal = left.get(columnName); var leftVal = left.get(columnName);
var rightVal = right.get(columnName); var rightVal = right.get(columnName);
return self._comparator(leftVal, rightVal) return self._comparator(leftVal, rightVal);
}); });
} }
else { else {
@ -31,7 +31,7 @@ define(
var leftVal = left.get(columnName); var leftVal = left.get(columnName);
var rightVal = right.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) { _comparator: function (leftVal, rightVal) {
if (!leftVal && !rightVal) { if (!leftVal && !rightVal) {
return 0 return 0;
} }
if (!leftVal) { if (!leftVal) {
@ -47,7 +47,7 @@ define(
} }
if (!rightVal) { if (!rightVal) {
return 1 return 1;
} }
if (leftVal === rightVal) { if (leftVal === rightVal) {

View File

@ -33,9 +33,9 @@ define(
route: 'logs' route: 'logs'
}, },
{ {
title : 'Check for Update', title : 'Updates',
icon : 'icon-nd-update', icon : 'icon-upload-alt',
command: 'applicationUpdate' route : 'update'
} }
] ]
}, },

View 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