Merge branch 'develop'

pull/4/head
Mark McDowall 11 years ago
commit 3e545a9d62

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

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

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

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

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Nancy;
namespace NzbDrone.Api.Extensions
{
public static class RequestExtensions
{
public static bool IsApiRequest(this Request request)
{
return request.Path.StartsWith("/api/", StringComparison.InvariantCultureIgnoreCase);
}
public static bool IsSignalRRequest(this Request request)
{
return request.Path.StartsWith("/signalr/", StringComparison.InvariantCultureIgnoreCase);
}
public static bool IsLocalRequest(this Request request)
{
return (request.UserHostAddress.Equals("localhost") ||
request.UserHostAddress.Equals("127.0.0.1") ||
request.UserHostAddress.Equals("::1"));
}
}
}

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

@ -24,13 +24,10 @@ namespace NzbDrone.Api.Frontend.Mappers
{
_caseSensitive = true;
}
}
protected abstract string Map(string resourceUrl);
public abstract bool CanHandle(string resourceUrl);
public virtual Response GetResponse(string resourceUrl)

@ -30,7 +30,6 @@ namespace NzbDrone.Api
RegisterPipelines(pipelines);
container.Resolve<DatabaseTarget>().Register();
container.Resolve<IEnableBasicAuthInNancy>().Register(pipelines);
container.Resolve<IEventAggregator>().PublishEvent(new ApplicationStartedEvent());
ApplicationPipelines.OnError.AddItemToEndOfPipeline(container.Resolve<NzbDroneErrorPipeline>().HandleException);

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

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

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

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

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

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

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

@ -4,11 +4,11 @@ namespace NzbDrone.Core.Exceptions
{
public class BadRequestException : DownstreamException
{
public BadRequestException(HttpStatusCode statusCode, string message) : base(statusCode, message)
public BadRequestException(string message) : base(HttpStatusCode.BadRequest, message)
{
}
public BadRequestException(HttpStatusCode statusCode, string message, params object[] args) : base(statusCode, message, args)
public BadRequestException(string message, params object[] args) : base(HttpStatusCode.BadRequest, message, args)
{
}
}

@ -15,7 +15,7 @@ namespace NzbDrone.Core.Exceptions
switch (statusCode)
{
case HttpStatusCode.BadRequest:
throw new BadRequestException(statusCode, message);
throw new BadRequestException(message);
case HttpStatusCode.Unauthorized:
throw new UnauthorizedAccessException(message);

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Indexers.Exceptions
{
public class ApiKeyException : NzbDroneException
{
public ApiKeyException(string message, params object[] args) : base(message, args)
{
}
public ApiKeyException(string message) : base(message)
{
}
}
}

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Net;
using NLog;
using NzbDrone.Common;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
using System.Linq;
@ -30,7 +31,6 @@ namespace NzbDrone.Core.Indexers
_logger = logger;
}
public virtual IList<ReleaseInfo> FetchRss(IIndexer indexer)
{
_logger.Debug("Fetching feeds from " + indexer.Name);
@ -53,7 +53,6 @@ namespace NzbDrone.Core.Indexers
return result;
}
private IList<ReleaseInfo> Fetch(IIndexer indexer, SeasonSearchCriteria searchCriteria, int offset)
{
_logger.Debug("Searching for {0} offset: {1}", searchCriteria, offset);
@ -117,15 +116,21 @@ namespace NzbDrone.Core.Indexers
}
catch (WebException webException)
{
if (webException.Message.Contains("502") || webException.Message.Contains("503") || webException.Message.Contains("timed out"))
if (webException.Message.Contains("502") || webException.Message.Contains("503") ||
webException.Message.Contains("timed out"))
{
_logger.Warn("{0} server is currently unavailable. {1} {2}", indexer.Name, url, webException.Message);
_logger.Warn("{0} server is currently unavailable. {1} {2}", indexer.Name, url,
webException.Message);
}
else
{
_logger.Warn("{0} {1} {2}", indexer.Name, url, webException.Message);
}
}
catch (ApiKeyException)
{
_logger.Warn("Invalid API Key for {0} {1}", indexer.Name, url);
}
catch (Exception feedEx)
{
feedEx.Data.Add("FeedUrl", url);

@ -37,14 +37,20 @@ namespace NzbDrone.Core.Indexers
{
private readonly IIndexerRepository _indexerRepository;
private readonly IConfigFileProvider _configFileProvider;
private readonly INewznabTestService _newznabTestService;
private readonly Logger _logger;
private readonly List<IIndexer> _indexers;
public IndexerService(IIndexerRepository indexerRepository, IEnumerable<IIndexer> indexers, IConfigFileProvider configFileProvider, Logger logger)
public IndexerService(IIndexerRepository indexerRepository,
IEnumerable<IIndexer> indexers,
IConfigFileProvider configFileProvider,
INewznabTestService newznabTestService,
Logger logger)
{
_indexerRepository = indexerRepository;
_configFileProvider = configFileProvider;
_newznabTestService = newznabTestService;
_logger = logger;
@ -104,6 +110,9 @@ namespace NzbDrone.Core.Indexers
Settings = indexer.Settings.ToJson()
};
var instance = ToIndexer(definition).Instance;
_newznabTestService.Test(instance);
definition = _indexerRepository.Insert(definition);
indexer.Id = definition.Id;

@ -114,7 +114,6 @@ namespace NzbDrone.Core.Indexers.Newznab
return RecentFeed.Select(url => String.Format("{0}&limit=100&q={1}&season={2}&offset={3}", url, NewsnabifyTitle(seriesTitle), seasonNumber, offset));
}
public override string Name
{
get
@ -131,7 +130,6 @@ namespace NzbDrone.Core.Indexers.Newznab
}
}
private static string NewsnabifyTitle(string title)
{
return title.Replace("+", "%20");

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Indexers.Newznab
{
public class NewznabException : NzbDroneException
{
public NewznabException(string message, params object[] args) : base(message, args)
{
}
public NewznabException(string message) : base(message)
{
}
}
}

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

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

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Indexers.Newznab;
namespace NzbDrone.Core.Indexers
{
public interface INewznabTestService
{
void Test(IIndexer indexer);
}
public class NewznabTestService : INewznabTestService
{
private readonly IFetchFeedFromIndexers _feedFetcher;
private readonly IHttpProvider _httpProvider;
private readonly Logger _logger;
public NewznabTestService(IFetchFeedFromIndexers feedFetcher, IHttpProvider httpProvider, Logger logger)
{
_feedFetcher = feedFetcher;
_httpProvider = httpProvider;
_logger = logger;
}
public void Test(IIndexer indexer)
{
var releases = _feedFetcher.FetchRss(indexer);
if (releases.Any()) return;
try
{
var url = indexer.RecentFeed.First();
var xml = _httpProvider.DownloadString(url);
NewznabPreProcessor.Process(xml, url);
}
catch (ApiKeyException apiKeyException)
{
_logger.Warn("Indexer returned result for Newznab RSS URL, API Key appears to be invalid");
var apiKeyFailure = new ValidationFailure("ApiKey", "Invalid API Key");
throw new ValidationException(new List<ValidationFailure> { apiKeyFailure }.ToArray());
}
catch (Exception ex)
{
_logger.Warn("Indexer doesn't appear to be Newznab based");
var failure = new ValidationFailure("Url", "Invalid Newznab URL entered");
throw new ValidationException(new List<ValidationFailure> { failure }.ToArray());
}
}
}
}

@ -29,6 +29,8 @@ namespace NzbDrone.Core.Indexers
public IEnumerable<ReleaseInfo> Process(string xml, string url)
{
PreProcess(xml, url);
using (var xmlTextReader = XmlReader.Create(new StringReader(xml), new XmlReaderSettings { ProhibitDtd = false, IgnoreComments = true }))
{
@ -103,6 +105,10 @@ namespace NzbDrone.Core.Indexers
protected abstract long GetSize(XElement item);
protected virtual void PreProcess(string source, string url)
{
}
protected virtual ReleaseInfo PostProcessor(XElement item, ReleaseInfo currentResult)
{
return currentResult;

@ -42,7 +42,7 @@ namespace NzbDrone.Core.MediaFiles
var episodes = _episodeService.GetEpisodesByFileId(episodeFile.Id);
var newFileName = _buildFileNames.BuildFilename(episodes, series, episodeFile);
var filePath = _buildFileNames.BuildFilePath(series, episodes.First().SeasonNumber, newFileName, Path.GetExtension(episodeFile.Path));
MoveFile(episodeFile, filePath);
MoveFile(episodeFile, series, filePath);
return filePath;
}
@ -51,12 +51,12 @@ namespace NzbDrone.Core.MediaFiles
{
var newFileName = _buildFileNames.BuildFilename(localEpisode.Episodes, localEpisode.Series, episodeFile);
var filePath = _buildFileNames.BuildFilePath(localEpisode.Series, localEpisode.SeasonNumber, newFileName, Path.GetExtension(episodeFile.Path));
MoveFile(episodeFile, filePath);
MoveFile(episodeFile, localEpisode.Series, filePath);
return filePath;
}
private void MoveFile(EpisodeFile episodeFile, string destinationFilename)
private void MoveFile(EpisodeFile episodeFile, Series series, string destinationFilename)
{
if (!_diskProvider.FileExists(episodeFile.Path))
{
@ -73,6 +73,17 @@ namespace NzbDrone.Core.MediaFiles
_logger.Debug("Moving [{0}] > [{1}]", episodeFile.Path, destinationFilename);
_diskProvider.MoveFile(episodeFile.Path, destinationFilename);
_logger.Trace("Setting last write time on series folder: {0}", series.Path);
_diskProvider.SetFolderWriteTime(series.Path, episodeFile.DateAdded);
if (series.SeasonFolder)
{
var seasonFolder = Path.GetDirectoryName(destinationFilename);
_logger.Trace("Setting last write time on season folder: {0}", seasonFolder);
_diskProvider.SetFolderWriteTime(seasonFolder, episodeFile.DateAdded);
}
//Wrapped in Try/Catch to prevent this from causing issues with remote NAS boxes, the move worked, which is more important.
try
{

@ -71,7 +71,7 @@ namespace NzbDrone.Core.MetadataSource
series.ImdbId = show.imdb_id;
series.Title = show.title;
series.CleanTitle = Parser.Parser.CleanSeriesTitle(show.title);
series.Year = show.year;
series.Year = GetYear(show.year, show.first_aired);
series.FirstAired = FromIso(show.first_aired_iso);
series.Overview = show.overview;
series.Runtime = show.runtime;
@ -180,5 +180,14 @@ namespace NzbDrone.Core.MetadataSource
return phrase;
}
private static int GetYear(int year, int firstAired)
{
if (year > 1969) return year;
if (firstAired == 0) return DateTime.Today.Year;
return year;
}
}
}

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

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv.Events;
@ -36,8 +37,9 @@ namespace NzbDrone.Core.Tv
var updateList = new List<Episode>();
var newList = new List<Episode>();
var dupeFreeRemoteEpisodes = remoteEpisodes.DistinctBy(m => new { m.SeasonNumber, m.EpisodeNumber }).ToList();
foreach (var episode in remoteEpisodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber))
foreach (var episode in dupeFreeRemoteEpisodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber))
{
try
{

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

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
namespace NzbDrone.Core.Update
{
public class UpdateChanges
{
public List<String> New { get; set; }
public List<String> Fixed { get; set; }
public UpdateChanges()
{
New = new List<String>();
Fixed = new List<String>();
}
}
}

@ -13,5 +13,7 @@ namespace NzbDrone.Core.Update
public DateTime ReleaseDate { get; set; }
public String FileName { get; set; }
public String Url { get; set; }
public UpdateChanges Changes { get; set; }
}
}

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using NzbDrone.Common;
using RestSharp;
using NzbDrone.Core.Rest;
@ -8,6 +9,7 @@ namespace NzbDrone.Core.Update
public interface IUpdatePackageProvider
{
UpdatePackage GetLatestUpdate(string branch, Version currentVersion);
List<UpdatePackage> GetRecentUpdates(string branch);
}
public class UpdatePackageProvider : IUpdatePackageProvider
@ -27,5 +29,18 @@ namespace NzbDrone.Core.Update
return update.UpdatePackage;
}
public List<UpdatePackage> GetRecentUpdates(string branch)
{
var restClient = new RestClient(Services.RootUrl);
var request = new RestRequest("/v1/update/{branch}/changes");
request.AddUrlSegment("branch", branch);
var updates = restClient.ExecuteAndValidate<List<UpdatePackage>>(request);
return updates;
}
}
}

@ -26,18 +26,21 @@ namespace NzbDrone.Host.AccessControl
{
if (IsFirewallEnabled())
{
if (IsNzbDronePortOpen())
if (!IsNzbDronePortOpen(_configFileProvider.Port))
{
_logger.Trace("NzbDrone port is already open, skipping.");
return;
_logger.Trace("Opening Port for NzbDrone: {0}", _configFileProvider.Port);
OpenFirewallPort(_configFileProvider.Port);
}
OpenFirewallPort(_configFileProvider.Port);
if (_configFileProvider.EnableSsl && !IsNzbDronePortOpen(_configFileProvider.SslPort))
{
_logger.Trace("Opening SSL Port for NzbDrone: {0}", _configFileProvider.SslPort);
OpenFirewallPort(_configFileProvider.SslPort);
}
}
}
private bool IsNzbDronePortOpen()
private bool IsNzbDronePortOpen(int port)
{
try
{
@ -52,7 +55,7 @@ namespace NzbDrone.Host.AccessControl
foreach (INetFwOpenPort p in ports)
{
if (p.Port == _configFileProvider.Port)
if (p.Port == port)
return true;
}
}
@ -63,8 +66,6 @@ namespace NzbDrone.Host.AccessControl
return false;
}
private void OpenFirewallPort(int portNumber)
{
try

@ -36,7 +36,12 @@ namespace NzbDrone.Host.AccessControl
return;
}
var arguments = String.Format("netsh http add sslcert ipport=0.0.0.0:{0} certhash={1} appid={{{2}}", _configFileProvider.SslPort, _configFileProvider.SslCertHash, APP_ID);
var arguments = String.Format("http add sslcert ipport=0.0.0.0:{0} certhash={1} appid={{{2}}}",
_configFileProvider.SslPort,
_configFileProvider.SslCertHash,
APP_ID);
//TODO: Validate that the cert was added properly, invisible spaces FTL
_netshProvider.Run(arguments);
}

@ -14,10 +14,10 @@ namespace NzbDrone.Integration.Test.Client
{
private readonly IRestClient _restClient;
private readonly string _resource;
private readonly string _apiKey;
private readonly Logger _logger;
public ClientBase(IRestClient restClient, string resource = null)
public ClientBase(IRestClient restClient, string apiKey, string resource = null)
{
if (resource == null)
{
@ -26,6 +26,7 @@ namespace NzbDrone.Integration.Test.Client
_restClient = restClient;
_resource = resource;
_apiKey = apiKey;
_logger = LogManager.GetLogger("REST");
}
@ -88,10 +89,14 @@ namespace NzbDrone.Integration.Test.Client
public RestRequest BuildRequest(string command = "")
{
return new RestRequest(_resource + "/" + command.Trim('/'))
var request = new RestRequest(_resource + "/" + command.Trim('/'))
{
RequestFormat = DataFormat.Json
RequestFormat = DataFormat.Json,
};
request.AddHeader("Authorization", _apiKey);
return request;
}
public T Get<T>(IRestRequest request, HttpStatusCode statusCode = HttpStatusCode.OK) where T : class, new()

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

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

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

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

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

@ -1,11 +1,14 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Processes;
using NzbDrone.Core.Configuration;
using RestSharp;
namespace NzbDrone.Integration.Test
@ -16,16 +19,18 @@ namespace NzbDrone.Integration.Test
private readonly IRestClient _restClient;
private Process _nzbDroneProcess;
public string AppData { get; private set; }
public string ApiKey { get; private set; }
public NzbDroneRunner(int port = 8989)
{
_processProvider = new ProcessProvider();
_restClient = new RestClient("http://localhost:8989/api");
}
public void Start()
{
AppDate = Path.Combine(Directory.GetCurrentDirectory(), "_intg_" + DateTime.Now.Ticks);
AppData = Path.Combine(Directory.GetCurrentDirectory(), "_intg_" + DateTime.Now.Ticks);
var nzbdroneConsoleExe = "NzbDrone.Console.exe";
@ -34,7 +39,6 @@ namespace NzbDrone.Integration.Test
nzbdroneConsoleExe = "NzbDrone.exe";
}
if (BuildInfo.IsDebug)
{
@ -54,8 +58,12 @@ namespace NzbDrone.Integration.Test
Assert.Fail("Process has exited");
}
SetApiKey();
var request = new RestRequest("system/status");
request.AddHeader("Authorization", ApiKey);
var statusCall = _restClient.Get(new RestRequest("system/status"));
var statusCall = _restClient.Get(request);
if (statusCall.ResponseStatus == ResponseStatus.Completed)
{
@ -77,7 +85,7 @@ namespace NzbDrone.Integration.Test
private void Start(string outputNzbdroneConsoleExe)
{
var args = "-nobrowser -data=\"" + AppDate + "\"";
var args = "-nobrowser -data=\"" + AppData + "\"";
_nzbDroneProcess = _processProvider.Start(outputNzbdroneConsoleExe, args, OnOutputDataReceived, OnOutputDataReceived);
}
@ -92,7 +100,16 @@ namespace NzbDrone.Integration.Test
}
}
private void SetApiKey()
{
var configFile = Path.Combine(AppData, "config.xml");
if (!String.IsNullOrWhiteSpace(ApiKey)) return;
if (!File.Exists(configFile)) return;
public string AppDate { get; private set; }
var xDoc = XDocument.Load(configFile);
var config = xDoc.Descendants(ConfigFileProvider.CONFIG_ELEMENT_NAME).Single();
ApiKey = config.Descendants("ApiKey").Single().Value;
}
}
}

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

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

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

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

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

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

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

@ -0,0 +1,69 @@
'use strict';
define(
[
'backgrid',
'Shared/Grid/HeaderCell'
], function (Backgrid, NzbDroneHeaderCell) {
Backgrid.QualityHeaderCell = NzbDroneHeaderCell.extend({
events: {
'click': 'onClick'
},
onClick: function (e) {
e.preventDefault();
var self = this;
var columnName = this.column.get('name');
if (this.column.get('sortable')) {
if (this.direction() === 'ascending') {
this.sort(columnName, 'descending', function (left, right) {
var leftVal = left.get(columnName);
var rightVal = right.get(columnName);
return self._comparator(leftVal, rightVal);
});
}
else {
this.sort(columnName, 'ascending', function (left, right) {
var leftVal = left.get(columnName);
var rightVal = right.get(columnName);
return self._comparator(rightVal, leftVal);
});
}
}
},
_comparator: function (leftVal, rightVal) {
var leftWeight = leftVal.quality.weight;
var rightWeight = rightVal.quality.weight;
if (!leftWeight && !rightWeight) {
return 0;
}
if (!leftWeight) {
return -1;
}
if (!rightWeight) {
return 1;
}
if (leftWeight === rightWeight) {
return 0;
}
if (leftWeight > rightWeight) {
return -1;
}
return 1;
}
});
return Backgrid.QualityHeaderCell;
});

@ -1,56 +1,66 @@
'use strict';
define(
[
'app',
'Commands/CommandModel',
'Commands/CommandCollection',
'underscore',
'jQuery/jquery.spin'
], function (CommandModel, CommandCollection, _) {
], function (App, CommandModel, CommandCollection, _) {
return{
var singleton = function () {
Execute: function (name, properties) {
return {
var attr = _.extend({name: name.toLocaleLowerCase()}, properties);
Execute: function (name, properties) {
var commandModel = new CommandModel(attr);
var attr = _.extend({name: name.toLocaleLowerCase()}, properties);
return commandModel.save().success(function () {
CommandCollection.add(commandModel);
});
},
var commandModel = new CommandModel(attr);
bindToCommand: function (options) {
return commandModel.save().success(function () {
CommandCollection.add(commandModel);
});
},
var self = this;
bindToCommand: function (options) {
var existingCommand = CommandCollection.findCommand(options.command);
var self = this;
if (existingCommand) {
this._bindToCommandModel.call(this, existingCommand, options);
}
var existingCommand = CommandCollection.findCommand(options.command);
CommandCollection.bind('add sync', function (model) {
if (model.isSameCommand(options.command)) {
self._bindToCommandModel.call(self, model, options);
if (existingCommand) {
this._bindToCommandModel.call(this, existingCommand, options);
}
});
},
_bindToCommandModel: function bindToCommand(model, options) {
CommandCollection.bind('add sync', function (model) {
if (model.isSameCommand(options.command)) {
self._bindToCommandModel.call(self, model, options);
}
});
},
if (!model.isActive()) {
options.element.stopSpin();
return;
}
_bindToCommandModel: function bindToCommand(model, options) {
model.bind('change:state', function (model) {
if (!model.isActive()) {
options.element.stopSpin();
return;
}
});
options.element.startSpin();
}
}
model.bind('change:state', function (model) {
if (!model.isActive()) {
options.element.stopSpin();
if (model.isComplete()) {
App.vent.trigger(App.Events.CommandComplete, { command: model, model: options.model });
}
}
});
options.element.startSpin();
}
};
};
return singleton();
});

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

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

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

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

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

@ -6,9 +6,10 @@ define(
'Cells/FileSizeCell',
'Cells/QualityCell',
'Cells/ApprovalStatusCell',
'Release/DownloadReportCell'
'Release/DownloadReportCell',
'Cells/Header/QualityHeaderCell'
], function (Marionette, Backgrid, FileSizeCell, QualityCell, ApprovalStatusCell, DownloadReportCell) {
], function (Marionette, Backgrid, FileSizeCell, QualityCell, ApprovalStatusCell, DownloadReportCell, QualityHeaderCell) {
return Marionette.Layout.extend({
template: 'Episode/Search/ManualLayoutTemplate',
@ -44,10 +45,11 @@ define(
cell : FileSizeCell
},
{
name : 'quality',
label : 'Quality',
sortable: true,
cell : QualityCell
name : 'quality',
label : 'Quality',
sortable : true,
cell : QualityCell,
headerCell: QualityHeaderCell
},
{

@ -57,10 +57,10 @@ define(
}
if (seasonCount === 1) {
return new Handlebars.SafeString('<span class="label label-info">{0} Season</span>'.format(seasonCount))
return new Handlebars.SafeString('<span class="label label-info">{0} Season</span>'.format(seasonCount));
}
return new Handlebars.SafeString('<span class="label label-info">{0} Seasons</span>'.format(seasonCount))
return new Handlebars.SafeString('<span class="label label-info">{0} Seasons</span>'.format(seasonCount));
});
Handlebars.registerHelper('titleWithYear', function () {

@ -0,0 +1,18 @@
'use strict';
define(
[
'handlebars'
], function (Handlebars) {
Handlebars.registerHelper('currentVersion', function (version) {
var currentVersion = window.NzbDrone.ServerStatus.version;
if (currentVersion === version)
{
return new Handlebars.SafeString('<i class="icon-ok" title="Installed"></i>');
}
return '';
});
});

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

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

@ -13,7 +13,7 @@
var filename = a.pathname.split('/').pop();
//Suppress Firefox debug errors when console window is closed
if (filename.toLowerCase() === 'markupview.jsm') {
if (filename.toLowerCase() === 'markupview.jsm' || filename.toLowerCase() === 'markup-view.js') {
return false;
}

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

@ -50,7 +50,7 @@
</a>
</li>
<li>
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=KRTE52U3XJDSQ" target="_blank">
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HGGGM7JT5YVSS" target="_blank">
<i class="icon-nd-donate"></i>
<br>
Donate

@ -4,12 +4,21 @@ define(
'app',
'Series/SeriesCollection'
], function (App, SeriesCollection) {
$(document).on('keydown', function (e){
if ($(e.target).is('input')) {
return;
}
if (e.keyCode === 84) {
$('.x-series-search').focus();
e.preventDefault();
}
});
$.fn.bindSearch = function () {
$(this).typeahead({
source : function () {
return SeriesCollection.map(function (model) {
return model.get('title');
});
return SeriesCollection.pluck('title');
},
sorter: function (items) {
@ -17,9 +26,7 @@ define(
},
updater: function (item) {
var series = SeriesCollection.find(function (model) {
return model.get('title') === item;
});
var series = SeriesCollection.findWhere({ title: item });
this.$element.blur();
App.Router.navigate('/series/{0}'.format(series.get('titleSlug')), { trigger: true });

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

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

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

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

@ -1,8 +1,8 @@
<div class="seasonpass-series">
<div class="row">
<div class="span11">
<div class="span12">
<i class="icon-chevron-right x-expander expander pull-left"/>
<i class="x-series-monitored series-monitor-toggle pull-left" title="Toggle monitored state for entire series"/>
<span class="title span5">
<a href="{{route}}">
{{title}}
@ -26,10 +26,20 @@
</span>
</span>
<button class="btn x-latest">Latest</button>
<span class="season-pass-button">
<button class="btn x-latest">Latest</button>
<span class="help-inline">
<i class="icon-question-sign" title="Will quickly select the latest season as first monitored"/>
<span class="help-inline">
<i class="icon-question-sign" title="Will quickly select the latest season as first monitored"/>
</span>
</span>
<span class="season-pass-button">
<button class="btn x-all">All</button>
<span class="help-inline">
<i class="icon-question-sign" title="Will quickly select all seasons except for specials to be monitored"/>
</span>
</span>
</div>
</div>

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

@ -44,6 +44,8 @@ define(
this.listenTo(this.model, 'change:monitored', this._setMonitoredState);
this.listenTo(App.vent, App.Events.SeriesDeleted, this._onSeriesDeleted);
this.listenTo(App.vent, App.Events.SeasonRenamed, this._onSeasonRenamed);
App.vent.on(App.Events.CommandComplete, this._commandComplete, this);
},
onShow: function () {
@ -195,6 +197,16 @@ define(
if (this.model.get('id') === event.series.get('id')) {
this.episodeFileCollection.fetch();
}
},
_commandComplete: function (options) {
if (options.command.get('name') === 'refreshseries' || options.command.get('name') === 'renameseries') {
if (options.command.get('seriesId') === this.model.get('id')) {
this._showSeasons();
this._setMonitoredState();
this._showInfo();
}
}
}
});
});

@ -2,7 +2,7 @@
<div class="span11">
<div class="row">
<h1>
<i class="x-monitored clickable series-monitor-toggle" title="Toggle monitored state for entire series"/>
<i class="x-monitored" title="Toggle monitored state for entire series"/>
{{title}}
<div class="series-actions pull-right">
<div class="x-refresh">

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

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

@ -7,11 +7,49 @@
<div class="controls">
<input type="number" placeholder="8989" name="port"/>
<span>
<i class="icon-nd-form-warning" title="Requires restart to take effect"/>
</span>
<span>
<i class="icon-nd-form-warning" title="Requires restart to take effect"/>
</span>
</div>
</div>
<div class="control-group advanced-setting">
<label class="control-label">Enable SSL</label>
<div class="controls">
<label class="checkbox toggle well">
<input type="checkbox" name="enableSsl" class="x-ssl"/>
<p>
<span>Yes</span>
<span>No</span>
</p>
<div class="btn btn-primary slide-button"/>
</label>
<span class="help-inline-checkbox">
<i class="icon-nd-form-warning" title="Requires restart running as administrator to take effect"/>
</span>
</div>
</div>
<div class="x-ssl-options">
<div class="control-group advanced-setting">
<label class="control-label">SSL Port Number</label>
<div class="controls">
<input type="number" placeholder="8989" name="sslPort"/>
</div>
</div>
<div class="control-group advanced-setting">
<label class="control-label">SSL Cert Hash</label>
<div class="controls">
<input type="text" name="sslCertHash"/>
</div>
</div>
</div>
<div class="control-group">
@ -29,9 +67,9 @@
<div class="btn btn-primary slide-button"/>
</label>
<span class="help-inline-checkbox">
<i class="icon-question-sign" title="Open a web browser and navigate to NzbDrone homepage on app start. Has no effect if installed as a windows service"/>
</span>
<span class="help-inline-checkbox">
<i class="icon-nd-form-info" title="Open a web browser and navigate to NzbDrone homepage on app start. Has no effect if installed as a windows service"/>
</span>
</div>
</div>
</fieldset>
@ -51,7 +89,7 @@
</label>
<span class="help-inline-checkbox">
<i class="icon-question-sign" title="Require Username and Password to access Nzbdrone"/>
<i class="icon-nd-form-info" title="Require Username and Password to access Nzbdrone"/>
</span>
</div>
</div>
@ -91,8 +129,7 @@
</div>
</fieldset>
{{#unless_eq branch compare="master"}}
<fieldset>
<fieldset class="advanced-setting">
<legend>Development</legend>
<div class="alert">
<i class="icon-nd-warning"></i>
@ -106,5 +143,4 @@
</div>
</div>
</fieldset>
{{/unless_eq}}
</div>

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

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

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

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

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

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

@ -1,13 +0,0 @@
'use strict';
define(
[
'marionette',
'Mixins/AsModelBoundView'
], function (Marionette, AsModelBoundView) {
var view = Marionette.ItemView.extend({
template: 'Settings/Indexers/Options/ViewTemplate'
});
return AsModelBoundView.call(view);
});

@ -0,0 +1,22 @@
'use strict';
define(
[
'marionette',
'Mixins/AsModelBoundView',
'Mixins/AutoComplete'
], function (Marionette, AsModelBoundView) {
var view = Marionette.ItemView.extend({
template: 'Settings/MediaManagement/FileManagement/FileManagementViewTemplate',
ui: {
recyclingBin: '.x-path'
},
onShow: function () {
this.ui.recyclingBin.autoComplete('/directories');
}
});
return AsModelBoundView.call(view);
});

@ -1,4 +1,4 @@
<fieldset>
<fieldset class="advanced-setting">
<legend>File Management</legend>
<div class="control-group">
@ -40,4 +40,15 @@
</span>
</div>
</div>
<div class="control-group">
<label class="control-label">Recycling Bin</label>
<div class="controls">
<input type="text" name="recycleBin" class="x-path"/>
<span class="help-inline">
<i class="icon-nd-form-info" title="Episode files will go here when deleted instead of being permanently deleted"/>
</span>
</div>
</div>
</fieldset>

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

@ -6,15 +6,16 @@ define(
'Settings/SettingsModel',
'Settings/General/GeneralSettingsModel',
'Settings/MediaManagement/Naming/Model',
'Settings/MediaManagement/Layout',
'Settings/MediaManagement/MediaManagementLayout',
'Settings/Quality/QualityLayout',
'Settings/Indexers/Layout',
'Settings/Indexers/IndexerLayout',
'Settings/Indexers/Collection',
'Settings/DownloadClient/Layout',
'Settings/Notifications/CollectionView',
'Settings/Notifications/Collection',
'Settings/General/GeneralView',
'Shared/LoadingView'
'Shared/LoadingView',
'Config'
], function (App,
Marionette,
SettingsModel,
@ -28,7 +29,8 @@ define(
NotificationCollectionView,
NotificationCollection,
GeneralView,
LoadingView) {
LoadingView,
Config) {
return Marionette.Layout.extend({
template: 'Settings/SettingsLayoutTemplate',
@ -48,7 +50,8 @@ define(
indexersTab : '.x-indexers-tab',
downloadClientTab : '.x-download-client-tab',
notificationsTab : '.x-notifications-tab',
generalTab : '.x-general-tab'
generalTab : '.x-general-tab',
advancedSettings : '.x-advanced-settings'
},
events: {
@ -58,7 +61,67 @@ define(
'click .x-download-client-tab' : '_showDownloadClient',
'click .x-notifications-tab' : '_showNotifications',
'click .x-general-tab' : '_showGeneral',
'click .x-save-settings' : '_save'
'click .x-save-settings' : '_save',
'change .x-advanced-settings' : '_toggleAdvancedSettings'
},
initialize: function (options) {
if (options.action) {
this.action = options.action.toLowerCase();
}
},
onRender: function () {
this.loading.show(new LoadingView());
var self = this;
this.settings = new SettingsModel();
this.generalSettings = new GeneralSettingsModel();
this.namingSettings = new NamingModel();
this.indexerSettings = new IndexerCollection();
this.notificationSettings = new NotificationCollection();
$.when(this.settings.fetch(),
this.generalSettings.fetch(),
this.namingSettings.fetch(),
this.indexerSettings.fetch(),
this.notificationSettings.fetch()
).done(function () {
self.loading.$el.hide();
self.mediaManagement.show(new MediaManagementLayout({ settings: self.settings, namingSettings: self.namingSettings }));
self.quality.show(new QualityLayout({ settings: self.settings }));
self.indexers.show(new IndexerLayout({ settings: self.settings, indexersCollection: self.indexerSettings }));
self.downloadClient.show(new DownloadClientLayout({ model: self.settings }));
self.notifications.show(new NotificationCollectionView({ collection: self.notificationSettings }));
self.general.show(new GeneralView({ model: self.generalSettings }));
});
this._setAdvancedSettingsState();
},
onShow: function () {
switch (this.action) {
case 'quality':
this._showQuality();
break;
case 'indexers':
this._showIndexers();
break;
case 'downloadclient':
this._showDownloadClient();
break;
case 'connect':
this._showNotifications();
break;
case 'notifications':
this._showNotifications();
break;
case 'general':
this._showGeneral();
break;
default:
this._showMediaManagement();
}
},
_showMediaManagement: function (e) {
@ -121,65 +184,30 @@ define(
});
},
initialize: function (options) {
if (options.action) {
this.action = options.action.toLowerCase();
}
_save: function () {
App.vent.trigger(App.Commands.SaveSettings);
},
onRender: function () {
this.loading.show(new LoadingView());
var self = this;
this.settings = new SettingsModel();
this.generalSettings = new GeneralSettingsModel();
this.namingSettings = new NamingModel();
this.indexerSettings = new IndexerCollection();
this.notificationSettings = new NotificationCollection();
_setAdvancedSettingsState: function () {
var checked = Config.getValueBoolean('advancedSettings');
this.ui.advancedSettings.prop('checked', checked);
$.when(this.settings.fetch(),
this.generalSettings.fetch(),
this.namingSettings.fetch(),
this.indexerSettings.fetch(),
this.notificationSettings.fetch()
).done(function () {
self.loading.$el.hide();
self.mediaManagement.show(new MediaManagementLayout({ settings: self.settings, namingSettings: self.namingSettings }));
self.quality.show(new QualityLayout({settings: self.settings}));
self.indexers.show(new IndexerLayout({ settings: self.settings, indexersCollection: self.indexerSettings }));
self.downloadClient.show(new DownloadClientLayout({model: self.settings}));
self.notifications.show(new NotificationCollectionView({collection: self.notificationSettings}));
self.general.show(new GeneralView({model: self.generalSettings}));
});
if (checked) {
this.$el.addClass('show-advanced-settings');
}
},
onShow: function () {
switch (this.action) {
case 'quality':
this._showQuality();
break;
case 'indexers':
this._showIndexers();
break;
case 'downloadclient':
this._showDownloadClient();
break;
case 'connect':
this._showNotifications();
break;
case 'notifications':
this._showNotifications();
break;
case 'general':
this._showGeneral();
break;
default:
this._showMediaManagement();
_toggleAdvancedSettings: function () {
var checked = this.ui.advancedSettings.prop('checked');
Config.setValue('advancedSettings', checked);
if (checked) {
this.$el.addClass('show-advanced-settings');
}
},
_save: function () {
App.vent.trigger(App.Commands.SaveSettings);
else {
this.$el.removeClass('show-advanced-settings');
}
}
});
});

@ -6,6 +6,19 @@
<li><a href="#notifications" class="x-notifications-tab no-router">Connect</a></li>
<li><a href="#general" class="x-general-tab no-router">General</a></li>
<li class="pull-right"><button class="btn btn-primary x-save-settings">Save</button></li>
<li class="pull-right advanced-settings-toggle">
<label class="checkbox toggle well">
<input type="checkbox" class="x-advanced-settings"/>
<p>
<span>Show</span>
<span>Hide</span>
</p>
<div class="btn btn-warning slide-button"/>
</label>
<span class="help-inline-checkbox">
<i class="icon-nd-form-info" title="Show advanced options"/>
</span>
</li>
</ul>
<div class="tab-content">

@ -1,5 +1,5 @@
@import "../Content/Bootstrap/variables";
@import "../Shared/Styles/clickable.less";
@import "Indexers/indexers";
@import "Quality/quality";
@import "Notifications/notifications";
@ -43,4 +43,38 @@ li.save-and-add:hover {
.naming-example {
display: inline-block;
margin-top: 5px;
}
.advanced-settings-toggle {
margin-right: 40px;
.checkbox {
width : 100px;
margin-left : 0px;
display : inline-block;
padding-top : 0px;
margin-bottom : 0px;
margin-top : -1px;
}
.help-inline-checkbox {
display : inline-block;
margin-top : -23px;
margin-bottom : 0;
vertical-align : middle;
}
}
.advanced-setting {
display: none;
.control-label {
color: @warningText;
}
}
.show-advanced-settings {
.advanced-setting {
display: block;
}
}

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

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

@ -0,0 +1,11 @@
'use strict';
define(
[
'backbone',
'Update/UpdateModel'
], function (Backbone, UpdateModel) {
return Backbone.Collection.extend({
url : window.NzbDrone.ApiRoot + '/update',
model: UpdateModel
});
});

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save