diff --git a/Gruntfile.js b/Gruntfile.js index d78fac4da..7025f8c6b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -73,6 +73,7 @@ module.exports = function (grunt) { 'UI/Cells/cells.less', 'UI/Logs/logs.less', 'UI/Settings/settings.less', + 'UI/Update/update.less' ], dest : outputRoot, ext: '.css' diff --git a/NzbDrone.Api/Authentication/AuthenticationService.cs b/NzbDrone.Api/Authentication/AuthenticationService.cs index 17d7e5022..961dc22e4 100644 --- a/NzbDrone.Api/Authentication/AuthenticationService.cs +++ b/NzbDrone.Api/Authentication/AuthenticationService.cs @@ -1,4 +1,6 @@ -using Nancy.Authentication.Basic; +using System; +using Nancy; +using Nancy.Authentication.Basic; using Nancy.Security; using NzbDrone.Core.Configuration; @@ -7,6 +9,7 @@ namespace NzbDrone.Api.Authentication public interface IAuthenticationService : IUserValidator { bool Enabled { get; } + bool IsAuthenticated(NancyContext context); } public class AuthenticationService : IAuthenticationService @@ -44,5 +47,12 @@ namespace NzbDrone.Api.Authentication return _configFileProvider.AuthenticationEnabled; } } + + public bool IsAuthenticated(NancyContext context) + { + if (context.CurrentUser == null && _configFileProvider.AuthenticationEnabled) return false; + + return true; + } } } diff --git a/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs b/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs index 48ae43b2a..c5622eb75 100644 --- a/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs +++ b/NzbDrone.Api/Authentication/EnableBasicAuthInNancy.cs @@ -1,15 +1,12 @@ using Nancy; using Nancy.Authentication.Basic; using Nancy.Bootstrapper; +using NzbDrone.Api.Extensions; +using NzbDrone.Api.Extensions.Pipelines; namespace NzbDrone.Api.Authentication { - public interface IEnableBasicAuthInNancy - { - void Register(IPipelines pipelines); - } - - public class EnableBasicAuthInNancy : IEnableBasicAuthInNancy + public class EnableBasicAuthInNancy : IRegisterNancyPipeline { private readonly IAuthenticationService _authenticationService; @@ -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 }; } diff --git a/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs b/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs new file mode 100644 index 000000000..bbdf22e85 --- /dev/null +++ b/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/NzbDrone.Api/Extensions/RequestExtensions.cs b/NzbDrone.Api/Extensions/RequestExtensions.cs new file mode 100644 index 000000000..3d0329522 --- /dev/null +++ b/NzbDrone.Api/Extensions/RequestExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Nancy; + +namespace NzbDrone.Api.Extensions +{ + public static class RequestExtensions + { + public static bool IsApiRequest(this Request request) + { + return request.Path.StartsWith("/api/", StringComparison.InvariantCultureIgnoreCase); + } + + public static bool IsSignalRRequest(this Request request) + { + return request.Path.StartsWith("/signalr/", StringComparison.InvariantCultureIgnoreCase); + } + + public static bool IsLocalRequest(this Request request) + { + return (request.UserHostAddress.Equals("localhost") || + request.UserHostAddress.Equals("127.0.0.1") || + request.UserHostAddress.Equals("::1")); + } + } +} diff --git a/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs b/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs index b15167619..ae950aae0 100644 --- a/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs +++ b/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs @@ -1,20 +1,28 @@ +using System; using System.IO; using Nancy; +using Nancy.Responses; using NLog; using NzbDrone.Common; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; namespace NzbDrone.Api.Frontend.Mappers { public class IndexHtmlMapper : StaticResourceMapperBase { private readonly IDiskProvider _diskProvider; + private readonly IConfigFileProvider _configFileProvider; private readonly string _indexPath; - public IndexHtmlMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, Logger logger) + public IndexHtmlMapper(IAppFolderInfo appFolderInfo, + IDiskProvider diskProvider, + IConfigFileProvider configFileProvider, + Logger logger) : base(diskProvider, logger) { _diskProvider = diskProvider; + _configFileProvider = configFileProvider; _indexPath = Path.Combine(appFolderInfo.StartUpFolder, "UI", "index.html"); } @@ -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; } - } } \ No newline at end of file diff --git a/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs b/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs index eae69b9de..4cc42f49f 100644 --- a/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs +++ b/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs @@ -24,13 +24,10 @@ namespace NzbDrone.Api.Frontend.Mappers { _caseSensitive = true; } - - } protected abstract string Map(string resourceUrl); - public abstract bool CanHandle(string resourceUrl); public virtual Response GetResponse(string resourceUrl) diff --git a/NzbDrone.Api/NancyBootstrapper.cs b/NzbDrone.Api/NancyBootstrapper.cs index 29535d272..bee581921 100644 --- a/NzbDrone.Api/NancyBootstrapper.cs +++ b/NzbDrone.Api/NancyBootstrapper.cs @@ -30,7 +30,6 @@ namespace NzbDrone.Api RegisterPipelines(pipelines); container.Resolve().Register(); - container.Resolve().Register(pipelines); container.Resolve().PublishEvent(new ApplicationStartedEvent()); ApplicationPipelines.OnError.AddItemToEndOfPipeline(container.Resolve().HandleException); diff --git a/NzbDrone.Api/NzbDrone.Api.csproj b/NzbDrone.Api/NzbDrone.Api.csproj index 331591808..b88c2277d 100644 --- a/NzbDrone.Api/NzbDrone.Api.csproj +++ b/NzbDrone.Api/NzbDrone.Api.csproj @@ -74,6 +74,7 @@ Properties\SharedAssemblyInfo.cs + @@ -97,6 +98,7 @@ + diff --git a/NzbDrone.Api/Series/SeriesResource.cs b/NzbDrone.Api/Series/SeriesResource.cs index 170fa8301..dc7cc3479 100644 --- a/NzbDrone.Api/Series/SeriesResource.cs +++ b/NzbDrone.Api/Series/SeriesResource.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using NzbDrone.Api.REST; using NzbDrone.Core.MediaCover; using NzbDrone.Core.Tv; @@ -19,9 +20,9 @@ namespace NzbDrone.Api.Series { get { - if (Seasons != null) return Seasons.Count; + if (Seasons == null) return 0; - return 0; + return Seasons.Where(s => s.SeasonNumber > 0).Count(); } } diff --git a/NzbDrone.Api/Update/UpdateModule.cs b/NzbDrone.Api/Update/UpdateModule.cs index fbe13bd8e..e4a8cbf3a 100644 --- a/NzbDrone.Api/Update/UpdateModule.cs +++ b/NzbDrone.Api/Update/UpdateModule.cs @@ -10,25 +10,33 @@ namespace NzbDrone.Api.Update public class UpdateModule : NzbDroneRestModule { private readonly ICheckUpdateService _checkUpdateService; + private readonly IRecentUpdateProvider _recentUpdateProvider; - public UpdateModule(ICheckUpdateService checkUpdateService) + public UpdateModule(ICheckUpdateService checkUpdateService, + IRecentUpdateProvider recentUpdateProvider) { _checkUpdateService = checkUpdateService; - GetResourceAll = GetAvailableUpdate; + _recentUpdateProvider = recentUpdateProvider; + GetResourceAll = GetRecentUpdates; } - private List GetAvailableUpdate() + private UpdateResource GetAvailableUpdate() { var update = _checkUpdateService.AvailableUpdate(); - var response = new List(); + var response = new UpdateResource(); if (update != null) { - response.Add(update.InjectTo()); + return update.InjectTo(); } return response; } + + private List GetRecentUpdates() + { + return ToListResource(_recentUpdateProvider.GetRecentUpdatePackages); + } } public class UpdateResource : RestResource @@ -40,5 +48,7 @@ namespace NzbDrone.Api.Update public DateTime ReleaseDate { get; set; } public String FileName { get; set; } public String Url { get; set; } + + public UpdateChanges Changes { get; set; } } } \ No newline at end of file diff --git a/NzbDrone.Common/DiskProvider.cs b/NzbDrone.Common/DiskProvider.cs index 56180cdda..5860d95cd 100644 --- a/NzbDrone.Common/DiskProvider.cs +++ b/NzbDrone.Common/DiskProvider.cs @@ -39,6 +39,7 @@ namespace NzbDrone.Common string GetPathRoot(string path); void SetPermissions(string filename, WellKnownSidType accountSid, FileSystemRights rights, AccessControlType controlType); bool IsParent(string parentPath, string childPath); + void 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) { diff --git a/NzbDrone.Core.Test/Files/Indexers/Newznab/unauthorized.xml b/NzbDrone.Core.Test/Files/Indexers/Newznab/unauthorized.xml new file mode 100644 index 000000000..33e46d9b6 --- /dev/null +++ b/NzbDrone.Core.Test/Files/Indexers/Newznab/unauthorized.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 22ee553f8..58b09e81e 100644 --- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -252,6 +252,7 @@ Always + Always @@ -351,7 +352,6 @@ - diff --git a/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs b/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs index a3cd0b10a..b02608d8d 100644 Binary files a/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs and b/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs differ diff --git a/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/NzbDrone.Core/Configuration/ConfigFileProvider.cs index 4a1e93518..400f03a56 100644 --- a/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -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 _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()); } } diff --git a/NzbDrone.Core/Exceptions/BadRequestException.cs b/NzbDrone.Core/Exceptions/BadRequestException.cs index 212b69e87..adf8bf57f 100644 --- a/NzbDrone.Core/Exceptions/BadRequestException.cs +++ b/NzbDrone.Core/Exceptions/BadRequestException.cs @@ -4,11 +4,11 @@ namespace NzbDrone.Core.Exceptions { public class BadRequestException : DownstreamException { - public BadRequestException(HttpStatusCode statusCode, string message) : base(statusCode, message) + public BadRequestException(string message) : base(HttpStatusCode.BadRequest, message) { } - public BadRequestException(HttpStatusCode statusCode, string message, params object[] args) : base(statusCode, message, args) + public BadRequestException(string message, params object[] args) : base(HttpStatusCode.BadRequest, message, args) { } } diff --git a/NzbDrone.Core/Exceptions/StatusCodeToExceptions.cs b/NzbDrone.Core/Exceptions/StatusCodeToExceptions.cs index 10513d9c6..8deb955bb 100644 --- a/NzbDrone.Core/Exceptions/StatusCodeToExceptions.cs +++ b/NzbDrone.Core/Exceptions/StatusCodeToExceptions.cs @@ -15,7 +15,7 @@ namespace NzbDrone.Core.Exceptions switch (statusCode) { case HttpStatusCode.BadRequest: - throw new BadRequestException(statusCode, message); + throw new BadRequestException(message); case HttpStatusCode.Unauthorized: throw new UnauthorizedAccessException(message); diff --git a/NzbDrone.Core/Indexers/Exceptions/ApiKeyException.cs b/NzbDrone.Core/Indexers/Exceptions/ApiKeyException.cs new file mode 100644 index 000000000..ff97425f8 --- /dev/null +++ b/NzbDrone.Core/Indexers/Exceptions/ApiKeyException.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Indexers.Exceptions +{ + public class ApiKeyException : NzbDroneException + { + public ApiKeyException(string message, params object[] args) : base(message, args) + { + } + + public ApiKeyException(string message) : base(message) + { + } + } +} diff --git a/NzbDrone.Core/Indexers/IndexerFetchService.cs b/NzbDrone.Core/Indexers/IndexerFetchService.cs index 9814adc0d..ba284c8b7 100644 --- a/NzbDrone.Core/Indexers/IndexerFetchService.cs +++ b/NzbDrone.Core/Indexers/IndexerFetchService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Net; using NLog; using NzbDrone.Common; +using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; using System.Linq; @@ -30,7 +31,6 @@ namespace NzbDrone.Core.Indexers _logger = logger; } - public virtual IList FetchRss(IIndexer indexer) { _logger.Debug("Fetching feeds from " + indexer.Name); @@ -53,7 +53,6 @@ namespace NzbDrone.Core.Indexers return result; } - private IList Fetch(IIndexer indexer, SeasonSearchCriteria searchCriteria, int offset) { _logger.Debug("Searching for {0} offset: {1}", searchCriteria, offset); @@ -117,15 +116,21 @@ namespace NzbDrone.Core.Indexers } catch (WebException webException) { - if (webException.Message.Contains("502") || webException.Message.Contains("503") || webException.Message.Contains("timed out")) + if (webException.Message.Contains("502") || webException.Message.Contains("503") || + webException.Message.Contains("timed out")) { - _logger.Warn("{0} server is currently unavailable. {1} {2}", indexer.Name, url, webException.Message); + _logger.Warn("{0} server is currently unavailable. {1} {2}", indexer.Name, url, + webException.Message); } else { _logger.Warn("{0} {1} {2}", indexer.Name, url, webException.Message); } } + catch (ApiKeyException) + { + _logger.Warn("Invalid API Key for {0} {1}", indexer.Name, url); + } catch (Exception feedEx) { feedEx.Data.Add("FeedUrl", url); diff --git a/NzbDrone.Core/Indexers/IndexerService.cs b/NzbDrone.Core/Indexers/IndexerService.cs index 2c87569e3..e1b49c2dd 100644 --- a/NzbDrone.Core/Indexers/IndexerService.cs +++ b/NzbDrone.Core/Indexers/IndexerService.cs @@ -37,14 +37,20 @@ namespace NzbDrone.Core.Indexers { private readonly IIndexerRepository _indexerRepository; private readonly IConfigFileProvider _configFileProvider; + private readonly INewznabTestService _newznabTestService; private readonly Logger _logger; private readonly List _indexers; - public IndexerService(IIndexerRepository indexerRepository, IEnumerable indexers, IConfigFileProvider configFileProvider, Logger logger) + public IndexerService(IIndexerRepository indexerRepository, + IEnumerable indexers, + IConfigFileProvider configFileProvider, + INewznabTestService newznabTestService, + Logger logger) { _indexerRepository = indexerRepository; _configFileProvider = configFileProvider; + _newznabTestService = newznabTestService; _logger = logger; @@ -104,6 +110,9 @@ namespace NzbDrone.Core.Indexers Settings = indexer.Settings.ToJson() }; + var instance = ToIndexer(definition).Instance; + _newznabTestService.Test(instance); + definition = _indexerRepository.Insert(definition); indexer.Id = definition.Id; diff --git a/NzbDrone.Core/Indexers/Newznab/Newznab.cs b/NzbDrone.Core/Indexers/Newznab/Newznab.cs index 4341c22f7..8a13d2282 100644 --- a/NzbDrone.Core/Indexers/Newznab/Newznab.cs +++ b/NzbDrone.Core/Indexers/Newznab/Newznab.cs @@ -114,7 +114,6 @@ namespace NzbDrone.Core.Indexers.Newznab return RecentFeed.Select(url => String.Format("{0}&limit=100&q={1}&season={2}&offset={3}", url, NewsnabifyTitle(seriesTitle), seasonNumber, offset)); } - public override string Name { get @@ -131,7 +130,6 @@ namespace NzbDrone.Core.Indexers.Newznab } } - private static string NewsnabifyTitle(string title) { return title.Replace("+", "%20"); diff --git a/NzbDrone.Core/Indexers/Newznab/NewznabException.cs b/NzbDrone.Core/Indexers/Newznab/NewznabException.cs new file mode 100644 index 000000000..df858ac24 --- /dev/null +++ b/NzbDrone.Core/Indexers/Newznab/NewznabException.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Indexers.Newznab +{ + public class NewznabException : NzbDroneException + { + public NewznabException(string message, params object[] args) : base(message, args) + { + } + + public NewznabException(string message) : base(message) + { + } + } +} diff --git a/NzbDrone.Core/Indexers/Newznab/NewznabParser.cs b/NzbDrone.Core/Indexers/Newznab/NewznabParser.cs index fc9e54d93..bd8096877 100644 --- a/NzbDrone.Core/Indexers/Newznab/NewznabParser.cs +++ b/NzbDrone.Core/Indexers/Newznab/NewznabParser.cs @@ -1,6 +1,8 @@ using System; using System.Linq; +using System.Xml; using System.Xml.Linq; +using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Indexers.Newznab @@ -46,5 +48,10 @@ namespace NzbDrone.Core.Indexers.Newznab return currentResult; } + + protected override void PreProcess(string source, string url) + { + NewznabPreProcessor.Process(source, url); + } } } \ No newline at end of file diff --git a/NzbDrone.Core/Indexers/Newznab/NewznabPreProcessor.cs b/NzbDrone.Core/Indexers/Newznab/NewznabPreProcessor.cs new file mode 100644 index 000000000..3977a2c75 --- /dev/null +++ b/NzbDrone.Core/Indexers/Newznab/NewznabPreProcessor.cs @@ -0,0 +1,24 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using NzbDrone.Core.Indexers.Exceptions; + +namespace NzbDrone.Core.Indexers.Newznab +{ + public static class NewznabPreProcessor + { + public static void Process(string source, string url) + { + var xdoc = XDocument.Parse(source); + var error = xdoc.Descendants("error").FirstOrDefault(); + + if (error == null) return; + + var code = Convert.ToInt32(error.Attribute("code").Value); + + if (code >= 100 && code <= 199) throw new ApiKeyException("Invalid API key: {0}"); + + throw new NewznabException("Newznab error detected: {0}", error.Attribute("description").Value); + } + } +} diff --git a/NzbDrone.Core/Indexers/NewznabTestService.cs b/NzbDrone.Core/Indexers/NewznabTestService.cs new file mode 100644 index 000000000..c56b9b464 --- /dev/null +++ b/NzbDrone.Core/Indexers/NewznabTestService.cs @@ -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 { 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 { failure }.ToArray()); + } + } + } +} diff --git a/NzbDrone.Core/Indexers/RssParserBase.cs b/NzbDrone.Core/Indexers/RssParserBase.cs index ca4bf91f8..7eae9a117 100644 --- a/NzbDrone.Core/Indexers/RssParserBase.cs +++ b/NzbDrone.Core/Indexers/RssParserBase.cs @@ -29,6 +29,8 @@ namespace NzbDrone.Core.Indexers public IEnumerable Process(string xml, string url) { + PreProcess(xml, url); + using (var xmlTextReader = XmlReader.Create(new StringReader(xml), new XmlReaderSettings { ProhibitDtd = false, IgnoreComments = true })) { @@ -103,6 +105,10 @@ namespace NzbDrone.Core.Indexers protected abstract long GetSize(XElement item); + protected virtual void PreProcess(string source, string url) + { + } + protected virtual ReleaseInfo PostProcessor(XElement item, ReleaseInfo currentResult) { return currentResult; diff --git a/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index f14a0ae44..e4e58c6ed 100644 --- a/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -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 { diff --git a/NzbDrone.Core/MetadataSource/TraktProxy.cs b/NzbDrone.Core/MetadataSource/TraktProxy.cs index 5ff74a23c..c176ccff5 100644 --- a/NzbDrone.Core/MetadataSource/TraktProxy.cs +++ b/NzbDrone.Core/MetadataSource/TraktProxy.cs @@ -71,7 +71,7 @@ namespace NzbDrone.Core.MetadataSource series.ImdbId = show.imdb_id; series.Title = show.title; series.CleanTitle = Parser.Parser.CleanSeriesTitle(show.title); - series.Year = show.year; + series.Year = GetYear(show.year, show.first_aired); series.FirstAired = FromIso(show.first_aired_iso); series.Overview = show.overview; series.Runtime = show.runtime; @@ -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; + } } } diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 08803f3fa..23d3657e3 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -234,12 +234,16 @@ + + + + @@ -546,6 +550,8 @@ + + diff --git a/NzbDrone.Core/Tv/RefreshEpisodeService.cs b/NzbDrone.Core/Tv/RefreshEpisodeService.cs index d714ddf3e..31a90761d 100644 --- a/NzbDrone.Core/Tv/RefreshEpisodeService.cs +++ b/NzbDrone.Core/Tv/RefreshEpisodeService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using NLog; +using NzbDrone.Common; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv.Events; @@ -36,8 +37,9 @@ namespace NzbDrone.Core.Tv var updateList = new List(); var newList = new List(); + var dupeFreeRemoteEpisodes = remoteEpisodes.DistinctBy(m => new { m.SeasonNumber, m.EpisodeNumber }).ToList(); - foreach (var episode in remoteEpisodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber)) + foreach (var episode in dupeFreeRemoteEpisodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber)) { try { diff --git a/NzbDrone.Core/Update/RecentUpdateProvider.cs b/NzbDrone.Core/Update/RecentUpdateProvider.cs new file mode 100644 index 000000000..feee0d34f --- /dev/null +++ b/NzbDrone.Core/Update/RecentUpdateProvider.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Core.Update +{ + public interface IRecentUpdateProvider + { + List GetRecentUpdatePackages(); + } + + public class RecentUpdateProvider : IRecentUpdateProvider + { + private readonly IConfigFileProvider _configFileProvider; + private readonly IUpdatePackageProvider _updatePackageProvider; + + public RecentUpdateProvider(IConfigFileProvider configFileProvider, + IUpdatePackageProvider updatePackageProvider) + { + _configFileProvider = configFileProvider; + _updatePackageProvider = updatePackageProvider; + } + + public List GetRecentUpdatePackages() + { + var branch = _configFileProvider.Branch; + return _updatePackageProvider.GetRecentUpdates(branch); + } + } +} diff --git a/NzbDrone.Core/Update/UpdateChanges.cs b/NzbDrone.Core/Update/UpdateChanges.cs new file mode 100644 index 000000000..a26bba93b --- /dev/null +++ b/NzbDrone.Core/Update/UpdateChanges.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.Update +{ + public class UpdateChanges + { + public List New { get; set; } + public List Fixed { get; set; } + + public UpdateChanges() + { + New = new List(); + Fixed = new List(); + } + } +} diff --git a/NzbDrone.Core/Update/UpdatePackage.cs b/NzbDrone.Core/Update/UpdatePackage.cs index f94a77e4b..f8159686a 100644 --- a/NzbDrone.Core/Update/UpdatePackage.cs +++ b/NzbDrone.Core/Update/UpdatePackage.cs @@ -13,5 +13,7 @@ namespace NzbDrone.Core.Update public DateTime ReleaseDate { get; set; } public String FileName { get; set; } public String Url { get; set; } + + public UpdateChanges Changes { get; set; } } } diff --git a/NzbDrone.Core/Update/UpdatePackageProvider.cs b/NzbDrone.Core/Update/UpdatePackageProvider.cs index c6b4b64c0..0dbae8e34 100644 --- a/NzbDrone.Core/Update/UpdatePackageProvider.cs +++ b/NzbDrone.Core/Update/UpdatePackageProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using NzbDrone.Common; using RestSharp; using NzbDrone.Core.Rest; @@ -8,6 +9,7 @@ namespace NzbDrone.Core.Update public interface IUpdatePackageProvider { UpdatePackage GetLatestUpdate(string branch, Version currentVersion); + List GetRecentUpdates(string branch); } public class UpdatePackageProvider : IUpdatePackageProvider @@ -27,5 +29,18 @@ namespace NzbDrone.Core.Update return update.UpdatePackage; } + + public List GetRecentUpdates(string branch) + { + var restClient = new RestClient(Services.RootUrl); + + var request = new RestRequest("/v1/update/{branch}/changes"); + + request.AddUrlSegment("branch", branch); + + var updates = restClient.ExecuteAndValidate>(request); + + return updates; + } } } \ No newline at end of file diff --git a/NzbDrone.Host/AccessControl/FirewallAdapter.cs b/NzbDrone.Host/AccessControl/FirewallAdapter.cs index 72d194446..3aa624d10 100644 --- a/NzbDrone.Host/AccessControl/FirewallAdapter.cs +++ b/NzbDrone.Host/AccessControl/FirewallAdapter.cs @@ -26,18 +26,21 @@ namespace NzbDrone.Host.AccessControl { if (IsFirewallEnabled()) { - if (IsNzbDronePortOpen()) + if (!IsNzbDronePortOpen(_configFileProvider.Port)) { - _logger.Trace("NzbDrone port is already open, skipping."); - return; + _logger.Trace("Opening Port for NzbDrone: {0}", _configFileProvider.Port); + OpenFirewallPort(_configFileProvider.Port); } - OpenFirewallPort(_configFileProvider.Port); + if (_configFileProvider.EnableSsl && !IsNzbDronePortOpen(_configFileProvider.SslPort)) + { + _logger.Trace("Opening SSL Port for NzbDrone: {0}", _configFileProvider.SslPort); + OpenFirewallPort(_configFileProvider.SslPort); + } } } - - private bool IsNzbDronePortOpen() + private bool IsNzbDronePortOpen(int port) { try { @@ -52,7 +55,7 @@ namespace NzbDrone.Host.AccessControl foreach (INetFwOpenPort p in ports) { - if (p.Port == _configFileProvider.Port) + if (p.Port == port) return true; } } @@ -63,8 +66,6 @@ namespace NzbDrone.Host.AccessControl return false; } - - private void OpenFirewallPort(int portNumber) { try diff --git a/NzbDrone.Host/AccessControl/SslAdapter.cs b/NzbDrone.Host/AccessControl/SslAdapter.cs index c94e307a3..ddb2e7201 100644 --- a/NzbDrone.Host/AccessControl/SslAdapter.cs +++ b/NzbDrone.Host/AccessControl/SslAdapter.cs @@ -36,7 +36,12 @@ namespace NzbDrone.Host.AccessControl return; } - var arguments = String.Format("netsh http add sslcert ipport=0.0.0.0:{0} certhash={1} appid={{{2}}", _configFileProvider.SslPort, _configFileProvider.SslCertHash, APP_ID); + var arguments = String.Format("http add sslcert ipport=0.0.0.0:{0} certhash={1} appid={{{2}}}", + _configFileProvider.SslPort, + _configFileProvider.SslCertHash, + APP_ID); + + //TODO: Validate that the cert was added properly, invisible spaces FTL _netshProvider.Run(arguments); } diff --git a/NzbDrone.Integration.Test/Client/ClientBase.cs b/NzbDrone.Integration.Test/Client/ClientBase.cs index 20e35117f..9cd0b4b7a 100644 --- a/NzbDrone.Integration.Test/Client/ClientBase.cs +++ b/NzbDrone.Integration.Test/Client/ClientBase.cs @@ -14,10 +14,10 @@ namespace NzbDrone.Integration.Test.Client { private readonly IRestClient _restClient; private readonly string _resource; - + private readonly string _apiKey; private readonly Logger _logger; - public ClientBase(IRestClient restClient, string resource = null) + public ClientBase(IRestClient restClient, string apiKey, string resource = null) { if (resource == null) { @@ -26,6 +26,7 @@ namespace NzbDrone.Integration.Test.Client _restClient = restClient; _resource = resource; + _apiKey = apiKey; _logger = LogManager.GetLogger("REST"); } @@ -88,10 +89,14 @@ namespace NzbDrone.Integration.Test.Client public RestRequest BuildRequest(string command = "") { - return new RestRequest(_resource + "/" + command.Trim('/')) + var request = new RestRequest(_resource + "/" + command.Trim('/')) { - RequestFormat = DataFormat.Json + RequestFormat = DataFormat.Json, }; + + request.AddHeader("Authorization", _apiKey); + + return request; } public T Get(IRestRequest request, HttpStatusCode statusCode = HttpStatusCode.OK) where T : class, new() diff --git a/NzbDrone.Integration.Test/Client/EpisodeClient.cs b/NzbDrone.Integration.Test/Client/EpisodeClient.cs index 8cc89af4e..437226c2f 100644 --- a/NzbDrone.Integration.Test/Client/EpisodeClient.cs +++ b/NzbDrone.Integration.Test/Client/EpisodeClient.cs @@ -6,8 +6,8 @@ namespace NzbDrone.Integration.Test.Client { public class EpisodeClient : ClientBase { - public EpisodeClient(IRestClient restClient) - : base(restClient, "episodes") + public EpisodeClient(IRestClient restClient, string apiKey) + : base(restClient, apiKey, "episodes") { } diff --git a/NzbDrone.Integration.Test/Client/IndexerClient.cs b/NzbDrone.Integration.Test/Client/IndexerClient.cs index 44f5d4c77..9d6f9b974 100644 --- a/NzbDrone.Integration.Test/Client/IndexerClient.cs +++ b/NzbDrone.Integration.Test/Client/IndexerClient.cs @@ -5,12 +5,9 @@ namespace NzbDrone.Integration.Test.Client { public class IndexerClient : ClientBase { - public IndexerClient(IRestClient restClient) - : base(restClient) + public IndexerClient(IRestClient restClient, string apiKey) + : base(restClient, apiKey) { } - - - } } \ No newline at end of file diff --git a/NzbDrone.Integration.Test/Client/ReleaseClient.cs b/NzbDrone.Integration.Test/Client/ReleaseClient.cs index fba274856..46a6db839 100644 --- a/NzbDrone.Integration.Test/Client/ReleaseClient.cs +++ b/NzbDrone.Integration.Test/Client/ReleaseClient.cs @@ -5,12 +5,9 @@ namespace NzbDrone.Integration.Test.Client { public class ReleaseClient : ClientBase { - public ReleaseClient(IRestClient restClient) - : base(restClient) + public ReleaseClient(IRestClient restClient, string apiKey) + : base(restClient, apiKey) { } - - - } } diff --git a/NzbDrone.Integration.Test/Client/SeriesClient.cs b/NzbDrone.Integration.Test/Client/SeriesClient.cs index 3dc88c969..1f0a572f9 100644 --- a/NzbDrone.Integration.Test/Client/SeriesClient.cs +++ b/NzbDrone.Integration.Test/Client/SeriesClient.cs @@ -7,8 +7,8 @@ namespace NzbDrone.Integration.Test.Client { public class SeriesClient : ClientBase { - public SeriesClient(IRestClient restClient) - : base(restClient) + public SeriesClient(IRestClient restClient, string apiKey) + : base(restClient, apiKey) { } @@ -27,14 +27,11 @@ namespace NzbDrone.Integration.Test.Client } - public class SystemInfoClient : ClientBase { - public SystemInfoClient(IRestClient restClient) - : base(restClient) + public SystemInfoClient(IRestClient restClient, string apiKey) + : base(restClient, apiKey) { } - - } } diff --git a/NzbDrone.Integration.Test/IntegrationTest.cs b/NzbDrone.Integration.Test/IntegrationTest.cs index a0b5b3bcd..f8556a58d 100644 --- a/NzbDrone.Integration.Test/IntegrationTest.cs +++ b/NzbDrone.Integration.Test/IntegrationTest.cs @@ -47,22 +47,21 @@ namespace NzbDrone.Integration.Test _runner = new NzbDroneRunner(); _runner.KillAll(); - InitRestClients(); - _runner.Start(); + InitRestClients(); } private void InitRestClients() { RestClient = new RestClient("http://localhost:8989/api"); - Series = new SeriesClient(RestClient); - Releases = new ReleaseClient(RestClient); - RootFolders = new ClientBase(RestClient); - Commands = new ClientBase(RestClient); - History = new ClientBase(RestClient); - Indexers = new IndexerClient(RestClient); - Episodes = new EpisodeClient(RestClient); - NamingConfig = new ClientBase(RestClient, "config/naming"); + Series = new SeriesClient(RestClient, _runner.ApiKey); + Releases = new ReleaseClient(RestClient, _runner.ApiKey); + RootFolders = new ClientBase(RestClient, _runner.ApiKey); + Commands = new ClientBase(RestClient, _runner.ApiKey); + History = new ClientBase(RestClient, _runner.ApiKey); + Indexers = new IndexerClient(RestClient, _runner.ApiKey); + Episodes = new EpisodeClient(RestClient, _runner.ApiKey); + NamingConfig = new ClientBase(RestClient, _runner.ApiKey, "config/naming"); } //[TestFixtureTearDown] diff --git a/NzbDrone.Integration.Test/NzbDroneRunner.cs b/NzbDrone.Integration.Test/NzbDroneRunner.cs index b4bb8fa85..375fdc0b9 100644 --- a/NzbDrone.Integration.Test/NzbDroneRunner.cs +++ b/NzbDrone.Integration.Test/NzbDroneRunner.cs @@ -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; + } } } \ No newline at end of file diff --git a/UI/.idea/runConfigurations/Debug___Chrome.xml b/UI/.idea/runConfigurations/Debug___Chrome.xml index 2323f096d..82eb4863d 100644 --- a/UI/.idea/runConfigurations/Debug___Chrome.xml +++ b/UI/.idea/runConfigurations/Debug___Chrome.xml @@ -1,25 +1,21 @@ - - - + + + + + + + + + + + + + + + + + diff --git a/UI/.idea/runConfigurations/Debug___Firefox.xml b/UI/.idea/runConfigurations/Debug___Firefox.xml index 1f0cbd78b..2e020afbc 100644 --- a/UI/.idea/runConfigurations/Debug___Firefox.xml +++ b/UI/.idea/runConfigurations/Debug___Firefox.xml @@ -1,25 +1,21 @@ - - - + + + + + + + + + + + + + + + + + diff --git a/UI/AddSeries/Collection.js b/UI/AddSeries/AddSeriesCollection.js similarity index 100% rename from UI/AddSeries/Collection.js rename to UI/AddSeries/AddSeriesCollection.js diff --git a/UI/AddSeries/AddSeriesLayout.js b/UI/AddSeries/AddSeriesLayout.js index ca577de9b..14f11c356 100644 --- a/UI/AddSeries/AddSeriesLayout.js +++ b/UI/AddSeries/AddSeriesLayout.js @@ -4,7 +4,7 @@ define( 'app', 'marionette', 'AddSeries/RootFolders/Layout', - 'AddSeries/Existing/CollectionView', + 'AddSeries/Existing/AddExistingSeriesCollectionView', 'AddSeries/AddSeriesView', 'Quality/QualityProfileCollection', 'AddSeries/RootFolders/Collection', @@ -15,8 +15,7 @@ define( ExistingSeriesCollectionView, AddSeriesView, QualityProfileCollection, - RootFolderCollection, - SeriesCollection) { + RootFolderCollection) { return Marionette.Layout.extend({ template: 'AddSeries/AddSeriesLayoutTemplate', @@ -35,8 +34,6 @@ define( }, initialize: function () { - - SeriesCollection.fetch(); QualityProfileCollection.fetch(); RootFolderCollection.promise = RootFolderCollection.fetch(); }, diff --git a/UI/AddSeries/addSeriesLayoutTemplate.html b/UI/AddSeries/AddSeriesLayoutTemplate.html similarity index 100% rename from UI/AddSeries/addSeriesLayoutTemplate.html rename to UI/AddSeries/AddSeriesLayoutTemplate.html diff --git a/UI/AddSeries/AddSeriesView.js b/UI/AddSeries/AddSeriesView.js index e66b1e891..957b2cabc 100644 --- a/UI/AddSeries/AddSeriesView.js +++ b/UI/AddSeries/AddSeriesView.js @@ -3,14 +3,14 @@ define( [ 'app', 'marionette', - 'AddSeries/Collection', + 'AddSeries/AddSeriesCollection', 'AddSeries/SearchResultCollectionView', 'AddSeries/NotFoundView', 'Shared/LoadingView', 'underscore' ], function (App, Marionette, AddSeriesCollection, SearchResultCollectionView, NotFoundView, LoadingView, _) { return Marionette.Layout.extend({ - template: 'AddSeries/AddSeriesTemplate', + template: 'AddSeries/AddSeriesViewTemplate', regions: { searchResult: '#search-result' @@ -36,12 +36,12 @@ define( if (this.isExisting) { this.className = 'existing-series'; - this.listenTo(App.vent, App.Events.SeriesAdded, this._onSeriesAdded); } else { this.className = 'new-series'; } + this.listenTo(App.vent, App.Events.SeriesAdded, this._onSeriesAdded); this.listenTo(this.collection, 'sync', this._showResults); this.resultCollectionView = new SearchResultCollectionView({ @@ -52,21 +52,6 @@ define( this.throttledSearch = _.debounce(this.search, 1000, {trailing: true}).bind(this); }, - _onSeriesAdded: function (options) { - if (this.isExisting && options.series.get('path') === this.model.get('folder').path) { - this.close(); - } - }, - - _onLoadMore: function () { - var showingAll = this.resultCollectionView.showMore(); - this.ui.searchBar.show(); - - if (showingAll) { - this.ui.loadMore.hide(); - } - }, - onRender: function () { var self = this; @@ -77,7 +62,7 @@ define( self._abortExistingSearch(); self.throttledSearch({ term: self.ui.seriesSearch.val() - }) + }); }); if (this.isExisting) { @@ -87,6 +72,7 @@ define( onShow: function () { this.searchResult.show(this.resultCollectionView); + this.ui.seriesSearch.focus(); }, search: function (options) { @@ -106,6 +92,28 @@ define( return this.currentSearchPromise; }, + _onSeriesAdded: function (options) { + if (this.isExisting && options.series.get('path') === this.model.get('folder').path) { + this.close(); + } + + else if (!this.isExisting) { + this.collection.reset(); + this.searchResult.show(this.resultCollectionView); + this.ui.seriesSearch.val(''); + this.ui.seriesSearch.focus(); + } + }, + + _onLoadMore: function () { + var showingAll = this.resultCollectionView.showMore(); + this.ui.searchBar.show(); + + if (showingAll) { + this.ui.loadMore.hide(); + } + }, + _showResults: function () { if (!this.isClosed) { diff --git a/UI/AddSeries/AddSeriesTemplate.html b/UI/AddSeries/AddSeriesViewTemplate.html similarity index 100% rename from UI/AddSeries/AddSeriesTemplate.html rename to UI/AddSeries/AddSeriesViewTemplate.html diff --git a/UI/AddSeries/Existing/CollectionView.js b/UI/AddSeries/Existing/AddExistingSeriesCollectionView.js similarity index 83% rename from UI/AddSeries/Existing/CollectionView.js rename to UI/AddSeries/Existing/AddExistingSeriesCollectionView.js index 74108d33d..45659ffd8 100644 --- a/UI/AddSeries/Existing/CollectionView.js +++ b/UI/AddSeries/Existing/AddExistingSeriesCollectionView.js @@ -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); + } + }); } }, diff --git a/UI/AddSeries/SearchResultCollectionView.js b/UI/AddSeries/SearchResultCollectionView.js index e1d912f01..53d0a8936 100644 --- a/UI/AddSeries/SearchResultCollectionView.js +++ b/UI/AddSeries/SearchResultCollectionView.js @@ -2,8 +2,7 @@ define( [ 'marionette', - 'AddSeries/SearchResultView', - + 'AddSeries/SearchResultView' ], function (Marionette, SearchResultView) { return Marionette.CollectionView.extend({ diff --git a/UI/AddSeries/SearchResultView.js b/UI/AddSeries/SearchResultView.js index e19bfecaf..218bf0ff0 100644 --- a/UI/AddSeries/SearchResultView.js +++ b/UI/AddSeries/SearchResultView.js @@ -2,6 +2,7 @@ define( [ 'app', + 'underscore', 'marionette', 'Quality/QualityProfileCollection', 'AddSeries/RootFolders/Collection', @@ -11,7 +12,7 @@ define( 'Shared/Messenger', 'Mixins/AsValidatedView', 'jquery.dotdotdot' - ], function (App, Marionette, QualityProfiles, RootFolders, RootFolderLayout, SeriesCollection, Config, Messenger, AsValidatedView) { + ], function (App, _, Marionette, QualityProfiles, RootFolders, RootFolderLayout, SeriesCollection, Config, Messenger, AsValidatedView) { var view = Marionette.ItemView.extend({ @@ -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'); + }); } }); diff --git a/UI/Cells/Header/QualityHeaderCell.js b/UI/Cells/Header/QualityHeaderCell.js new file mode 100644 index 000000000..533386cb2 --- /dev/null +++ b/UI/Cells/Header/QualityHeaderCell.js @@ -0,0 +1,69 @@ +'use strict'; + +define( + [ + 'backgrid', + 'Shared/Grid/HeaderCell' + ], function (Backgrid, NzbDroneHeaderCell) { + + Backgrid.QualityHeaderCell = NzbDroneHeaderCell.extend({ + events: { + 'click': 'onClick' + }, + + onClick: function (e) { + e.preventDefault(); + + var self = this; + var columnName = this.column.get('name'); + + if (this.column.get('sortable')) { + if (this.direction() === 'ascending') { + this.sort(columnName, 'descending', function (left, right) { + var leftVal = left.get(columnName); + var rightVal = right.get(columnName); + + return self._comparator(leftVal, rightVal); + }); + } + else { + this.sort(columnName, 'ascending', function (left, right) { + var leftVal = left.get(columnName); + var rightVal = right.get(columnName); + + return self._comparator(rightVal, leftVal); + }); + } + } + }, + + _comparator: function (leftVal, rightVal) { + var leftWeight = leftVal.quality.weight; + var rightWeight = rightVal.quality.weight; + + if (!leftWeight && !rightWeight) { + return 0; + } + + if (!leftWeight) { + return -1; + } + + if (!rightWeight) { + return 1; + } + + if (leftWeight === rightWeight) { + return 0; + } + + if (leftWeight > rightWeight) { + return -1; + } + + return 1; + } + }); + + return Backgrid.QualityHeaderCell; + }); diff --git a/UI/Commands/CommandController.js b/UI/Commands/CommandController.js index 650d8f67b..8e14512cf 100644 --- a/UI/Commands/CommandController.js +++ b/UI/Commands/CommandController.js @@ -1,56 +1,66 @@ 'use strict'; define( [ + 'app', 'Commands/CommandModel', 'Commands/CommandCollection', 'underscore', 'jQuery/jquery.spin' - ], function (CommandModel, CommandCollection, _) { + ], function (App, CommandModel, CommandCollection, _) { - return{ + var singleton = function () { - Execute: function (name, properties) { + return { - var attr = _.extend({name: name.toLocaleLowerCase()}, properties); + Execute: function (name, properties) { - var commandModel = new CommandModel(attr); + var attr = _.extend({name: name.toLocaleLowerCase()}, properties); - return commandModel.save().success(function () { - CommandCollection.add(commandModel); - }); - }, + var commandModel = new CommandModel(attr); - bindToCommand: function (options) { + return commandModel.save().success(function () { + CommandCollection.add(commandModel); + }); + }, - var self = this; + bindToCommand: function (options) { - var existingCommand = CommandCollection.findCommand(options.command); + var self = this; - if (existingCommand) { - this._bindToCommandModel.call(this, existingCommand, options); - } + var existingCommand = CommandCollection.findCommand(options.command); - CommandCollection.bind('add sync', function (model) { - if (model.isSameCommand(options.command)) { - self._bindToCommandModel.call(self, model, options); + if (existingCommand) { + this._bindToCommandModel.call(this, existingCommand, options); } - }); - }, - _bindToCommandModel: function bindToCommand(model, options) { + CommandCollection.bind('add sync', function (model) { + if (model.isSameCommand(options.command)) { + self._bindToCommandModel.call(self, model, options); + } + }); + }, - if (!model.isActive()) { - options.element.stopSpin(); - return; - } + _bindToCommandModel: function bindToCommand(model, options) { - model.bind('change:state', function (model) { if (!model.isActive()) { options.element.stopSpin(); + return; } - }); - options.element.startSpin(); - } - } + model.bind('change:state', function (model) { + if (!model.isActive()) { + options.element.stopSpin(); + + if (model.isComplete()) { + App.vent.trigger(App.Events.CommandComplete, { command: model, model: options.model }); + } + } + }); + + options.element.startSpin(); + } + }; + }; + + return singleton(); }); diff --git a/UI/Commands/CommandModel.js b/UI/Commands/CommandModel.js index fcb08dd6d..cdb492ca6 100644 --- a/UI/Commands/CommandModel.js +++ b/UI/Commands/CommandModel.js @@ -11,13 +11,9 @@ define( return response; }, - isActive: function () { - return this.get('state') !== 'completed' && this.get('state') !== 'failed'; - }, - isSameCommand: function (command) { - if (command.name.toLocaleLowerCase() != this.get('name').toLocaleLowerCase()) { + if (command.name.toLocaleLowerCase() !== this.get('name').toLocaleLowerCase()) { return false; } @@ -28,6 +24,14 @@ define( } return true; + }, + + isActive: function () { + return this.get('state') !== 'completed' && this.get('state') !== 'failed'; + }, + + isComplete: function () { + return this.get('state') === 'completed'; } }); }); diff --git a/UI/Config.js b/UI/Config.js index f0d8c4722..e5409f25b 100644 --- a/UI/Config.js +++ b/UI/Config.js @@ -12,6 +12,10 @@ define( DefaultRootFolderId: 'DefaultRootFolderId' }, + getValueBoolean: function (key, defaultValue) { + return this.getValue(key, defaultValue) === 'true'; + }, + getValue: function (key, defaultValue) { var storeValue = localStorage.getItem(key); @@ -35,6 +39,5 @@ define( App.vent.trigger(this.Events.ConfigUpdatedEvent, {key: key, value: value}); } - }; }); diff --git a/UI/Content/Overrides/bootstrap.toggle-switch.less b/UI/Content/Overrides/bootstrap.toggle-switch.less index 153ff546d..1c8da8516 100644 --- a/UI/Content/Overrides/bootstrap.toggle-switch.less +++ b/UI/Content/Overrides/bootstrap.toggle-switch.less @@ -5,7 +5,7 @@ .slide-button { .buttonBackground(@btnDangerBackground, @btnDangerBackgroundHighlight); - &.btn-danger { + &.btn-danger, &.btn-warning { .buttonBackground(@btnInverseBackground, @btnInverseBackgroundHighlight); } } @@ -16,5 +16,9 @@ &.btn-danger { .buttonBackground(@btnDangerBackground, @btnDangerBackgroundHighlight); } + + &.btn-warning { + .buttonBackground(@btnWarningBackground, @btnWarningBackgroundHighlight); + } } } \ No newline at end of file diff --git a/UI/Content/theme.less b/UI/Content/theme.less index debaf887c..2eb6e6481 100644 --- a/UI/Content/theme.less +++ b/UI/Content/theme.less @@ -162,6 +162,10 @@ footer { color : @successText; } +.status-warning { + color : @warningText; +} + .status-danger { color : @errorText; } diff --git a/UI/Controller.js b/UI/Controller.js index 190708195..a4bc4d1f9 100644 --- a/UI/Controller.js +++ b/UI/Controller.js @@ -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)); diff --git a/UI/Episode/Search/ManualLayout.js b/UI/Episode/Search/ManualLayout.js index 8647b4c2a..c782179df 100644 --- a/UI/Episode/Search/ManualLayout.js +++ b/UI/Episode/Search/ManualLayout.js @@ -6,9 +6,10 @@ define( 'Cells/FileSizeCell', 'Cells/QualityCell', 'Cells/ApprovalStatusCell', - 'Release/DownloadReportCell' + 'Release/DownloadReportCell', + 'Cells/Header/QualityHeaderCell' - ], function (Marionette, Backgrid, FileSizeCell, QualityCell, ApprovalStatusCell, DownloadReportCell) { + ], function (Marionette, Backgrid, FileSizeCell, QualityCell, ApprovalStatusCell, DownloadReportCell, QualityHeaderCell) { return Marionette.Layout.extend({ template: 'Episode/Search/ManualLayoutTemplate', @@ -44,10 +45,11 @@ define( cell : FileSizeCell }, { - name : 'quality', - label : 'Quality', - sortable: true, - cell : QualityCell + name : 'quality', + label : 'Quality', + sortable : true, + cell : QualityCell, + headerCell: QualityHeaderCell }, { diff --git a/UI/Handlebars/Helpers/Series.js b/UI/Handlebars/Helpers/Series.js index f9d849325..69022243c 100644 --- a/UI/Handlebars/Helpers/Series.js +++ b/UI/Handlebars/Helpers/Series.js @@ -57,10 +57,10 @@ define( } if (seasonCount === 1) { - return new Handlebars.SafeString('{0} Season'.format(seasonCount)) + return new Handlebars.SafeString('{0} Season'.format(seasonCount)); } - return new Handlebars.SafeString('{0} Seasons'.format(seasonCount)) + return new Handlebars.SafeString('{0} Seasons'.format(seasonCount)); }); Handlebars.registerHelper('titleWithYear', function () { diff --git a/UI/Handlebars/Helpers/Version.js b/UI/Handlebars/Helpers/Version.js new file mode 100644 index 000000000..ca6dad750 --- /dev/null +++ b/UI/Handlebars/Helpers/Version.js @@ -0,0 +1,18 @@ +'use strict'; + +define( + [ + 'handlebars' + ], function (Handlebars) { + + Handlebars.registerHelper('currentVersion', function (version) { + var currentVersion = window.NzbDrone.ServerStatus.version; + + if (currentVersion === version) + { + return new Handlebars.SafeString(''); + } + + return ''; + }); + }); diff --git a/UI/Handlebars/backbone.marionette.templates.js b/UI/Handlebars/backbone.marionette.templates.js index 3d477969b..928f692a5 100644 --- a/UI/Handlebars/backbone.marionette.templates.js +++ b/UI/Handlebars/backbone.marionette.templates.js @@ -9,6 +9,7 @@ define( 'Handlebars/Helpers/Episode', 'Handlebars/Helpers/Series', 'Handlebars/Helpers/Quality', + 'Handlebars/Helpers/Version', 'Handlebars/Handlebars.Debug' ], function (Templates) { return function () { diff --git a/UI/History/HistoryLayout.js b/UI/History/HistoryLayout.js index 160ef9227..bf3d9928b 100644 --- a/UI/History/HistoryLayout.js +++ b/UI/History/HistoryLayout.js @@ -60,9 +60,10 @@ define( cell : EpisodeTitleCell }, { - name : 'quality', - label: 'Quality', - cell : QualityCell + name : 'quality', + label : 'Quality', + cell : QualityCell, + sortable: false }, { name : 'date', diff --git a/UI/Instrumentation/ErrorHandler.js b/UI/Instrumentation/ErrorHandler.js index 475edc68f..92ccd4db1 100644 --- a/UI/Instrumentation/ErrorHandler.js +++ b/UI/Instrumentation/ErrorHandler.js @@ -13,7 +13,7 @@ var filename = a.pathname.split('/').pop(); //Suppress Firefox debug errors when console window is closed - if (filename.toLowerCase() === 'markupview.jsm') { + if (filename.toLowerCase() === 'markupview.jsm' || filename.toLowerCase() === 'markup-view.js') { return false; } diff --git a/UI/Mixins/backbone.ajax.js b/UI/Mixins/jquery.ajax.js similarity index 81% rename from UI/Mixins/backbone.ajax.js rename to UI/Mixins/jquery.ajax.js index 0544b2e28..e05a7d8fb 100644 --- a/UI/Mixins/backbone.ajax.js +++ b/UI/Mixins/jquery.ajax.js @@ -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); }; }; - }); diff --git a/UI/Navbar/NavbarTemplate.html b/UI/Navbar/NavbarTemplate.html index d9b1c83e6..57b62d37c 100644 --- a/UI/Navbar/NavbarTemplate.html +++ b/UI/Navbar/NavbarTemplate.html @@ -50,7 +50,7 @@
  • - +
    Donate diff --git a/UI/Navbar/Search.js b/UI/Navbar/Search.js index 35e0d733b..4e0e3b7d5 100644 --- a/UI/Navbar/Search.js +++ b/UI/Navbar/Search.js @@ -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 }); diff --git a/UI/Release/Collection.js b/UI/Release/Collection.js index 93ec60c15..fb2a2ec63 100644 --- a/UI/Release/Collection.js +++ b/UI/Release/Collection.js @@ -1,15 +1,13 @@ 'use strict'; define( [ - 'Release/Model', - 'backbone.pageable' - ], function (ReleaseModel, PagableCollection) { - return PagableCollection.extend({ + 'backbone', + 'Release/Model' + ], function (Backbone, ReleaseModel) { + return Backbone.Collection.extend({ url : window.NzbDrone.ApiRoot + '/release', model: ReleaseModel, - mode: 'client', - state: { pageSize: 2000 }, diff --git a/UI/Router.js b/UI/Router.js index de2111644..22c13b04f 100644 --- a/UI/Router.js +++ b/UI/Router.js @@ -31,6 +31,7 @@ require( 'rss' : 'rss', 'system' : 'system', 'seasonpass' : 'seasonPass', + 'update' : 'update', ':whatever' : 'notFound' } }); diff --git a/UI/SeasonPass/Layout.js b/UI/SeasonPass/SeasonPassLayout.js similarity index 93% rename from UI/SeasonPass/Layout.js rename to UI/SeasonPass/SeasonPassLayout.js index ebf2609cf..0a1e51d92 100644 --- a/UI/SeasonPass/Layout.js +++ b/UI/SeasonPass/SeasonPassLayout.js @@ -12,7 +12,7 @@ define( SeriesCollectionView, LoadingView) { return Marionette.Layout.extend({ - template: 'SeasonPass/LayoutTemplate', + template: 'SeasonPass/SeasonPassLayoutTemplate', regions: { series: '#x-series' diff --git a/UI/SeasonPass/LayoutTemplate.html b/UI/SeasonPass/SeasonPassLayoutTemplate.html similarity index 100% rename from UI/SeasonPass/LayoutTemplate.html rename to UI/SeasonPass/SeasonPassLayoutTemplate.html diff --git a/UI/SeasonPass/SeriesLayout.js b/UI/SeasonPass/SeriesLayout.js index 1eae0ce0e..34b26855a 100644 --- a/UI/SeasonPass/SeriesLayout.js +++ b/UI/SeasonPass/SeriesLayout.js @@ -1,24 +1,28 @@ 'use strict'; define( [ + 'underscore', 'marionette', 'backgrid', 'Series/SeasonCollection' - ], function (Marionette, Backgrid, SeasonCollection) { + ], function (_, Marionette, Backgrid, SeasonCollection) { return Marionette.Layout.extend({ template: 'SeasonPass/SeriesLayoutTemplate', ui: { - seasonSelect: '.x-season-select', - expander : '.x-expander', - seasonGrid : '.x-season-grid' + seasonSelect : '.x-season-select', + expander : '.x-expander', + seasonGrid : '.x-season-grid', + seriesMonitored: '.x-series-monitored' }, events: { - 'change .x-season-select': '_seasonSelected', - 'click .x-expander' : '_expand', - 'click .x-latest' : '_latest', - 'click .x-monitored' : '_toggleSeasonMonitored' + 'change .x-season-select' : '_seasonSelected', + 'click .x-expander' : '_expand', + 'click .x-latest' : '_latest', + 'click .x-all' : '_all', + 'click .x-monitored' : '_toggleSeasonMonitored', + 'click .x-series-monitored': '_toggleSeriesMonitored' }, regions: { @@ -26,6 +30,7 @@ define( }, initialize: function () { + this.listenTo(this.model, 'sync', this._setSeriesMonitoredState); this.seasonCollection = new SeasonCollection(this.model.get('seasons')); this.expanded = false; }, @@ -36,16 +41,17 @@ define( } this._setExpanderIcon(); + this._setSeriesMonitoredState(); }, _seasonSelected: function () { var seasonNumber = parseInt(this.ui.seasonSelect.val()); - if (seasonNumber == -1 || isNaN(seasonNumber)) { + if (seasonNumber === -1 || isNaN(seasonNumber)) { return; } - this._setMonitored(seasonNumber) + this._setSeasonMonitored(seasonNumber); }, _expand: function () { @@ -79,10 +85,16 @@ define( return s.seasonNumber; }); - this._setMonitored(season.seasonNumber); + this._setSeasonMonitored(season.seasonNumber); }, - _setMonitored: function (seasonNumber) { + _all: function () { + var minSeasonNotZero = _.min(_.reject(this.model.get('seasons'), { seasonNumber: 0 }), 'seasonNumber'); + + this._setSeasonMonitored(minSeasonNotZero.seasonNumber); + }, + + _setSeasonMonitored: function (seasonNumber) { var self = this; this.model.setSeasonPass(seasonNumber); @@ -118,6 +130,29 @@ define( _afterToggleSeasonMonitored: function () { this.render(); + }, + + _setSeriesMonitoredState: function () { + var monitored = this.model.get('monitored'); + + this.ui.seriesMonitored.removeAttr('data-idle-icon'); + + if (monitored) { + this.ui.seriesMonitored.addClass('icon-nd-monitored'); + this.ui.seriesMonitored.removeClass('icon-nd-unmonitored'); + } + else { + this.ui.seriesMonitored.addClass('icon-nd-unmonitored'); + this.ui.seriesMonitored.removeClass('icon-nd-monitored'); + } + }, + + _toggleSeriesMonitored: function (e) { + var savePromise = this.model.save('monitored', !this.model.get('monitored'), { + wait: true + }); + + this.ui.seriesMonitored.spinForPromise(savePromise); } }); }); diff --git a/UI/SeasonPass/SeriesLayoutTemplate.html b/UI/SeasonPass/SeriesLayoutTemplate.html index 9c12a4483..844307b51 100644 --- a/UI/SeasonPass/SeriesLayoutTemplate.html +++ b/UI/SeasonPass/SeriesLayoutTemplate.html @@ -1,8 +1,8 @@ 
    - diff --git a/UI/Series/Details/SeasonLayoutTemplate.html b/UI/Series/Details/SeasonLayoutTemplate.html index 4b401c2e6..827671ab2 100644 --- a/UI/Series/Details/SeasonLayoutTemplate.html +++ b/UI/Series/Details/SeasonLayoutTemplate.html @@ -9,13 +9,17 @@ {{/if}} {{#if_eq episodeCount compare=0}} - - {{else}} - {{#if_eq percentOfEpisodes compare=100}} - + {{#if monitored}} + + {{else}} + + {{/if}} {{else}} - - {{/if_eq}} + {{#if_eq percentOfEpisodes compare=100}} + + {{else}} + + {{/if_eq}} {{/if_eq}} diff --git a/UI/Series/Details/SeriesDetailsLayout.js b/UI/Series/Details/SeriesDetailsLayout.js index 238ffb00c..d08abac58 100644 --- a/UI/Series/Details/SeriesDetailsLayout.js +++ b/UI/Series/Details/SeriesDetailsLayout.js @@ -44,6 +44,8 @@ define( this.listenTo(this.model, 'change:monitored', this._setMonitoredState); this.listenTo(App.vent, App.Events.SeriesDeleted, this._onSeriesDeleted); this.listenTo(App.vent, App.Events.SeasonRenamed, this._onSeasonRenamed); + + App.vent.on(App.Events.CommandComplete, this._commandComplete, this); }, onShow: function () { @@ -195,6 +197,16 @@ define( if (this.model.get('id') === event.series.get('id')) { this.episodeFileCollection.fetch(); } + }, + + _commandComplete: function (options) { + if (options.command.get('name') === 'refreshseries' || options.command.get('name') === 'renameseries') { + if (options.command.get('seriesId') === this.model.get('id')) { + this._showSeasons(); + this._setMonitoredState(); + this._showInfo(); + } + } } }); }); diff --git a/UI/Series/Details/SeriesDetailsTemplate.html b/UI/Series/Details/SeriesDetailsTemplate.html index 1aa4ed9fe..c653d54cb 100644 --- a/UI/Series/Details/SeriesDetailsTemplate.html +++ b/UI/Series/Details/SeriesDetailsTemplate.html @@ -2,7 +2,7 @@

    - + {{title}}
    diff --git a/UI/Series/series.less b/UI/Series/series.less index e80f978b1..70fa26eaa 100644 --- a/UI/Series/series.less +++ b/UI/Series/series.less @@ -274,3 +274,16 @@ font-size : 16px; vertical-align : middle !important; } + + +.seasonpass-series { + .season-pass-button { + display: inline-block; + width: 120px; + } + + .series-monitor-toggle { + font-size: 24px; + margin-top: 3px; + } +} \ No newline at end of file diff --git a/UI/ServerStatus.js b/UI/ServerStatus.js index 1a5da687b..589d45fc6 100644 --- a/UI/ServerStatus.js +++ b/UI/ServerStatus.js @@ -1,10 +1,12 @@ -window.NzbDrone = {}; window.NzbDrone.ApiRoot = '/api'; var statusText = $.ajax({ type : 'GET', url : window.NzbDrone.ApiRoot + '/system/status', - async: false + async: false, + headers: { + Authorization: window.NzbDrone.ApiKey + } }).responseText; window.NzbDrone.ServerStatus = JSON.parse(statusText); diff --git a/UI/Settings/General/GeneralTemplate.html b/UI/Settings/General/GeneralTemplate.html index bb9de7626..44d9e3d11 100644 --- a/UI/Settings/General/GeneralTemplate.html +++ b/UI/Settings/General/GeneralTemplate.html @@ -7,11 +7,49 @@
    - - - + + +
    +
    + +
    + +
    +
    + +
    +
    + + +
    + +
    +
    + +
    + + +
    + +
    +
    @@ -29,9 +67,9 @@
    - - - + + +
    @@ -51,7 +89,7 @@ - +
    @@ -91,8 +129,7 @@

    - {{#unless_eq branch compare="master"}} -
    +
    Development
    @@ -106,5 +143,4 @@
    - {{/unless_eq}}
    diff --git a/UI/Settings/General/GeneralView.js b/UI/Settings/General/GeneralView.js index 56900fb50..6971dc5f5 100644 --- a/UI/Settings/General/GeneralView.js +++ b/UI/Settings/General/GeneralView.js @@ -5,38 +5,56 @@ define( 'Mixins/AsModelBoundView' ], function (Marionette, AsModelBoundView) { var view = Marionette.ItemView.extend({ - template: 'Settings/General/GeneralTemplate', + template: 'Settings/General/GeneralTemplate', + + events: { + 'change .x-auth': '_setAuthOptionsVisibility', + 'change .x-ssl': '_setSslOptionsVisibility' + }, + + ui: { + authToggle : '.x-auth', + authOptions: '.x-auth-options', + sslToggle : '.x-ssl', + sslOptions: '.x-ssl-options' + }, + + onRender: function(){ + if(!this.ui.authToggle.prop('checked')){ + this.ui.authOptions.hide(); + } - events: { - 'change .x-auth': '_setAuthOptionsVisibility' - }, + if(!this.ui.sslToggle.prop('checked')){ + this.ui.sslOptions.hide(); + } + }, - ui: { - authToggle : '.x-auth', - authOptions: '.x-auth-options' - }, + _setAuthOptionsVisibility: function () { + var showAuthOptions = this.ui.authToggle.prop('checked'); - onRender: function(){ - if(!this.ui.authToggle.prop('checked')){ - this.ui.authOptions.hide(); - } - }, + if (showAuthOptions) { + this.ui.authOptions.slideDown(); + } - _setAuthOptionsVisibility: function () { + else { + this.ui.authOptions.slideUp(); + } + }, - var showAuthOptions = this.ui.authToggle.prop('checked'); + _setSslOptionsVisibility: function () { - if (showAuthOptions) { - this.ui.authOptions.slideDown(); - } + var showSslOptions = this.ui.sslToggle.prop('checked'); - else { - this.ui.authOptions.slideUp(); - } + if (showSslOptions) { + this.ui.sslOptions.slideDown(); } - }); + else { + this.ui.sslOptions.slideUp(); + } + } + }); return AsModelBoundView.call(view); }); diff --git a/UI/Settings/Indexers/EditTemplate.html b/UI/Settings/Indexers/EditTemplate.html index e433b1a29..f9c3779ca 100644 --- a/UI/Settings/Indexers/EditTemplate.html +++ b/UI/Settings/Indexers/EditTemplate.html @@ -39,6 +39,9 @@ {{#if id}} {{/if}} + + +
    diff --git a/UI/Settings/Indexers/EditView.js b/UI/Settings/Indexers/EditView.js index 1d49366cb..4d0defee0 100644 --- a/UI/Settings/Indexers/EditView.js +++ b/UI/Settings/Indexers/EditView.js @@ -11,6 +11,10 @@ define( var view = Marionette.ItemView.extend({ template: 'Settings/Indexers/EditTemplate', + ui : { + activity: '.x-activity' + }, + events: { 'click .x-save' : '_save', 'click .x-save-and-add': '_saveAndAdd' @@ -21,6 +25,8 @@ define( }, _save: function () { + this.ui.activity.html(''); + var self = this; var promise = this.model.saveSettings(); @@ -29,10 +35,16 @@ define( self.indexerCollection.add(self.model, { merge: true }); App.vent.trigger(App.Commands.CloseModalCommand); }); + + promise.fail(function () { + self.ui.activity.empty(); + }); } }, _saveAndAdd: function () { + this.ui.activity.html(''); + var self = this; var promise = this.model.saveSettings(); @@ -50,6 +62,10 @@ define( self.model.set('fields.' + key + '.value', ''); }); }); + + promise.fail(function () { + self.ui.activity.empty(); + }); } } }); diff --git a/UI/Settings/Indexers/Layout.js b/UI/Settings/Indexers/IndexerLayout.js similarity index 86% rename from UI/Settings/Indexers/Layout.js rename to UI/Settings/Indexers/IndexerLayout.js index 6cbcd5352..a5b200ed2 100644 --- a/UI/Settings/Indexers/Layout.js +++ b/UI/Settings/Indexers/IndexerLayout.js @@ -4,10 +4,10 @@ define( [ 'marionette', 'Settings/Indexers/CollectionView', - 'Settings/Indexers/Options/View' + 'Settings/Indexers/Options/IndexerOptionsView' ], function (Marionette, CollectionView, OptionsView) { return Marionette.Layout.extend({ - template: 'Settings/Indexers/LayoutTemplate', + template: 'Settings/Indexers/IndexerLayoutTemplate', regions: { indexersRegion : '#indexers-collection', diff --git a/UI/Settings/Indexers/LayoutTemplate.html b/UI/Settings/Indexers/IndexerLayoutTemplate.html similarity index 100% rename from UI/Settings/Indexers/LayoutTemplate.html rename to UI/Settings/Indexers/IndexerLayoutTemplate.html diff --git a/UI/Settings/MediaManagement/FileManagement/View.js b/UI/Settings/Indexers/Options/IndexerOptionsView.js similarity index 80% rename from UI/Settings/MediaManagement/FileManagement/View.js rename to UI/Settings/Indexers/Options/IndexerOptionsView.js index 7c4a89638..2fa8f3a59 100644 --- a/UI/Settings/MediaManagement/FileManagement/View.js +++ b/UI/Settings/Indexers/Options/IndexerOptionsView.js @@ -6,7 +6,7 @@ define( ], function (Marionette, AsModelBoundView) { var view = Marionette.ItemView.extend({ - template: 'Settings/MediaManagement/FileManagement/ViewTemplate' + template: 'Settings/Indexers/Options/IndexerOptionsViewTemplate' }); return AsModelBoundView.call(view); diff --git a/UI/Settings/Indexers/Options/ViewTemplate.html b/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.html similarity index 92% rename from UI/Settings/Indexers/Options/ViewTemplate.html rename to UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.html index 1830fd4b2..616a66f76 100644 --- a/UI/Settings/Indexers/Options/ViewTemplate.html +++ b/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.html @@ -9,7 +9,7 @@
    -
    +
    @@ -21,7 +21,7 @@
    -
    +
    diff --git a/UI/Settings/Indexers/Options/View.js b/UI/Settings/Indexers/Options/View.js deleted file mode 100644 index ff1b67f51..000000000 --- a/UI/Settings/Indexers/Options/View.js +++ /dev/null @@ -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); - }); diff --git a/UI/Settings/MediaManagement/FileManagement/FileManagementView.js b/UI/Settings/MediaManagement/FileManagement/FileManagementView.js new file mode 100644 index 000000000..af28961b3 --- /dev/null +++ b/UI/Settings/MediaManagement/FileManagement/FileManagementView.js @@ -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); + }); diff --git a/UI/Settings/MediaManagement/FileManagement/ViewTemplate.html b/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html similarity index 75% rename from UI/Settings/MediaManagement/FileManagement/ViewTemplate.html rename to UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html index 5f5b5c052..02938c4e0 100644 --- a/UI/Settings/MediaManagement/FileManagement/ViewTemplate.html +++ b/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html @@ -1,4 +1,4 @@ -
    +
    File Management
    @@ -40,4 +40,15 @@
    + +
    + + +
    + + + + +
    +
    diff --git a/UI/Settings/MediaManagement/Layout.js b/UI/Settings/MediaManagement/MediaManagementLayout.js similarity index 86% rename from UI/Settings/MediaManagement/Layout.js rename to UI/Settings/MediaManagement/MediaManagementLayout.js index 7b7a4e42c..47d670a40 100644 --- a/UI/Settings/MediaManagement/Layout.js +++ b/UI/Settings/MediaManagement/MediaManagementLayout.js @@ -5,10 +5,10 @@ define( 'marionette', 'Settings/MediaManagement/Naming/View', 'Settings/MediaManagement/Sorting/View', - 'Settings/MediaManagement/FileManagement/View' + 'Settings/MediaManagement/FileManagement/FileManagementView' ], function (Marionette, NamingView, SortingView, FileManagementView) { return Marionette.Layout.extend({ - template: 'Settings/MediaManagement/LayoutTemplate', + template: 'Settings/MediaManagement/MediaManagementLayoutTemplate', regions: { episodeNaming : '#episode-naming', diff --git a/UI/Settings/MediaManagement/LayoutTemplate.html b/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.html similarity index 100% rename from UI/Settings/MediaManagement/LayoutTemplate.html rename to UI/Settings/MediaManagement/MediaManagementLayoutTemplate.html diff --git a/UI/Settings/SettingsLayout.js b/UI/Settings/SettingsLayout.js index f7c10c7a2..cad4cfaa9 100644 --- a/UI/Settings/SettingsLayout.js +++ b/UI/Settings/SettingsLayout.js @@ -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'); + } } }); }); diff --git a/UI/Settings/SettingsLayoutTemplate.html b/UI/Settings/SettingsLayoutTemplate.html index 6264503ac..c5f22dd23 100644 --- a/UI/Settings/SettingsLayoutTemplate.html +++ b/UI/Settings/SettingsLayoutTemplate.html @@ -6,6 +6,19 @@
  • Connect
  • General
  • +
  • +
  • diff --git a/UI/Settings/settings.less b/UI/Settings/settings.less index e1d594cc2..5addfd8fb 100644 --- a/UI/Settings/settings.less +++ b/UI/Settings/settings.less @@ -1,5 +1,5 @@ +@import "../Content/Bootstrap/variables"; @import "../Shared/Styles/clickable.less"; - @import "Indexers/indexers"; @import "Quality/quality"; @import "Notifications/notifications"; @@ -43,4 +43,38 @@ li.save-and-add:hover { .naming-example { display: inline-block; margin-top: 5px; +} + +.advanced-settings-toggle { + margin-right: 40px; + + .checkbox { + width : 100px; + margin-left : 0px; + display : inline-block; + padding-top : 0px; + margin-bottom : 0px; + margin-top : -1px; + } + + .help-inline-checkbox { + display : inline-block; + margin-top : -23px; + margin-bottom : 0; + vertical-align : middle; + } +} + +.advanced-setting { + display: none; + + .control-label { + color: @warningText; + } +} + +.show-advanced-settings { + .advanced-setting { + display: block; + } } \ No newline at end of file diff --git a/UI/Shared/Grid/DateHeaderCell.js b/UI/Shared/Grid/DateHeaderCell.js index 5473f1093..fcc3e2ba4 100644 --- a/UI/Shared/Grid/DateHeaderCell.js +++ b/UI/Shared/Grid/DateHeaderCell.js @@ -23,7 +23,7 @@ define( var leftVal = left.get(columnName); var rightVal = right.get(columnName); - return self._comparator(leftVal, rightVal) + return self._comparator(leftVal, rightVal); }); } else { @@ -31,7 +31,7 @@ define( var leftVal = left.get(columnName); var rightVal = right.get(columnName); - return self._comparator(rightVal, leftVal) + return self._comparator(rightVal, leftVal); }); } } @@ -39,7 +39,7 @@ define( _comparator: function (leftVal, rightVal) { if (!leftVal && !rightVal) { - return 0 + return 0; } if (!leftVal) { @@ -47,7 +47,7 @@ define( } if (!rightVal) { - return 1 + return 1; } if (leftVal === rightVal) { diff --git a/UI/System/Layout.js b/UI/System/Layout.js index 390ce48d8..5f4778c59 100644 --- a/UI/System/Layout.js +++ b/UI/System/Layout.js @@ -33,9 +33,9 @@ define( route: 'logs' }, { - title : 'Check for Update', - icon : 'icon-nd-update', - command: 'applicationUpdate' + title : 'Updates', + icon : 'icon-upload-alt', + route : 'update' } ] }, diff --git a/UI/Update/UpdateCollection.js b/UI/Update/UpdateCollection.js new file mode 100644 index 000000000..b1e7bcf60 --- /dev/null +++ b/UI/Update/UpdateCollection.js @@ -0,0 +1,11 @@ +'use strict'; +define( + [ + 'backbone', + 'Update/UpdateModel' + ], function (Backbone, UpdateModel) { + return Backbone.Collection.extend({ + url : window.NzbDrone.ApiRoot + '/update', + model: UpdateModel + }); + }); diff --git a/UI/Update/UpdateCollectionView.js b/UI/Update/UpdateCollectionView.js new file mode 100644 index 000000000..267af7f85 --- /dev/null +++ b/UI/Update/UpdateCollectionView.js @@ -0,0 +1,10 @@ +'use strict'; +define( + [ + 'marionette', + 'Update/UpdateItemView' + ], function (Marionette, UpdateItemView) { + return Marionette.CollectionView.extend({ + itemView: UpdateItemView + }); + }); diff --git a/UI/Update/UpdateItemView.js b/UI/Update/UpdateItemView.js new file mode 100644 index 000000000..8d478f038 --- /dev/null +++ b/UI/Update/UpdateItemView.js @@ -0,0 +1,11 @@ +'use strict'; + +define( + [ + 'app', + 'marionette' + ], function (App, Marionette) { + return Marionette.ItemView.extend({ + template: 'Update/UpdateItemViewTemplate' + }); + }); diff --git a/UI/Update/UpdateItemViewTemplate.html b/UI/Update/UpdateItemViewTemplate.html new file mode 100644 index 000000000..6c9df0a15 --- /dev/null +++ b/UI/Update/UpdateItemViewTemplate.html @@ -0,0 +1,23 @@ +
    +
    + {{version}} - {{ShortDate releaseDate}} {{currentVersion version}} + + {{#with changes}} + {{#each new}} +
    + New {{this}} +
    + {{/each}} + + {{#each fixed}} +
    + Fixed {{this}} +
    + {{/each}} + {{/with}} + + {{#unless changes}} + No notable changes + {{/unless}} +
    +
    diff --git a/UI/Update/UpdateLayout.js b/UI/Update/UpdateLayout.js new file mode 100644 index 000000000..4794f761c --- /dev/null +++ b/UI/Update/UpdateLayout.js @@ -0,0 +1,58 @@ +'use strict'; +define( + [ + 'marionette', + 'backgrid', + 'Update/UpdateCollection', + 'Update/UpdateCollectionView', + 'Shared/Toolbar/ToolbarLayout', + 'Shared/LoadingView' + ], function (Marionette, Backgrid, UpdateCollection, UpdateCollectionView, ToolbarLayout, LoadingView) { + return Marionette.Layout.extend({ + template: 'Update/UpdateLayoutTemplate', + + regions: { + updates: '#x-updates', + toolbar: '#x-toolbar' + }, + + leftSideButtons: { + type : 'default', + storeState: false, + items : + [ + { + title : 'Check for Update', + icon : 'icon-nd-update', + command: 'applicationUpdate' + } + ] + }, + + initialize: function () { + this.updateCollection = new UpdateCollection(); + }, + + onRender: function () { + this.updates.show(new LoadingView()); + this._showToolbar(); + + var self = this; + var promise = this.updateCollection.fetch(); + + promise.done(function (){ + self.updates.show(new UpdateCollectionView({ collection: self.updateCollection })); + }); + }, + + _showToolbar: function () { + this.toolbar.show(new ToolbarLayout({ + left : + [ + this.leftSideButtons + ], + context: this + })); + } + }); + }); diff --git a/UI/Update/UpdateLayoutTemplate.html b/UI/Update/UpdateLayoutTemplate.html new file mode 100644 index 000000000..c405cb562 --- /dev/null +++ b/UI/Update/UpdateLayoutTemplate.html @@ -0,0 +1,6 @@ +
    +
    +
    +
    +
    +
    diff --git a/UI/Update/UpdateModel.js b/UI/Update/UpdateModel.js new file mode 100644 index 000000000..530a080c6 --- /dev/null +++ b/UI/Update/UpdateModel.js @@ -0,0 +1,9 @@ +'use strict'; +define( + [ + 'backbone' + ], function (Backbone) { + return Backbone.Model.extend({ + + }); + }); diff --git a/UI/Update/update.less b/UI/Update/update.less new file mode 100644 index 000000000..e25b23b70 --- /dev/null +++ b/UI/Update/update.less @@ -0,0 +1,25 @@ +.update { + margin-bottom: 30px; + + legend { + margin-bottom: 5px; + line-height: 30px; + + .date { + font-size: 16px; + } + } + + .changes-header { + font-size: 18px; + } + + .label { + width: 40px; + text-align: center; + } + .change { + margin-bottom: 2px; + font-size: 13px; + } +} \ No newline at end of file diff --git a/UI/app.js b/UI/app.js index 73d8be5e5..e1f0ad981 100644 --- a/UI/app.js +++ b/UI/app.js @@ -25,7 +25,6 @@ require.config({ 'jquery.knob' : 'JsLibraries/jquery.knob', 'jquery.dotdotdot' : 'JsLibraries/jquery.dotdotdot', 'libs' : 'JsLibraries/' - }, shim: { @@ -33,11 +32,18 @@ require.config({ $: { exports: '$', - init: function () { + deps : + [ + 'Mixins/jquery.ajax' + ], + + init: function (AjaxMixin) { require( [ 'jQuery/ToTheTop' ]); + + AjaxMixin.apply($); } }, @@ -74,14 +80,10 @@ require.config({ backbone: { deps : [ - 'Mixins/backbone.ajax', 'underscore', '$' ], - exports: 'Backbone', - init : function (AjaxMixin) { - AjaxMixin.apply(Backbone); - } + exports: 'Backbone' }, @@ -187,9 +189,10 @@ define( var app = new Marionette.Application(); app.Events = { - SeriesAdded : 'series:added', - SeriesDeleted: 'series:deleted', - SeasonRenamed: 'season:renamed' + SeriesAdded : 'series:added', + SeriesDeleted : 'series:deleted', + SeasonRenamed : 'season:renamed', + CommandComplete: 'command:complete' }; app.Commands = { diff --git a/UI/index.html b/UI/index.html index 178c5beb2..093473a8a 100644 --- a/UI/index.html +++ b/UI/index.html @@ -15,6 +15,7 @@ + @@ -60,6 +61,12 @@
    + + +