From 0ee8b75b54eee04d16c47b7417db76ab245c5f6d Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Wed, 11 Jan 2017 15:42:37 -0500 Subject: [PATCH 01/83] initial autoimporter commit --- .../AutoImport.derp/AutoImportBase.cs | 35 +++++++ .../AutoImport.derp/AutoImportDefinition.cs | 18 ++++ .../AutoImport.derp/AutoImportRepository.cs | 20 ++++ .../AutoImport.derp/IAutoImport.cs | 12 +++ .../IMDbWatchList/IMDbWatchList.cs | 43 +++++++++ .../IMDbWatchList/IMDbWatchListProxy.cs | 79 ++++++++++++++++ .../IMDbWatchList/IMDbWatchListSettings.cs | 30 ++++++ .../AutoImporter/AutoImporterBase.cs | 91 +++++++++++++++++++ .../AutoImporter/AutoImporterDefinition.cs | 17 ++++ .../AutoImporter/IAutoImporter.cs | 15 +++ src/NzbDrone.Core/NzbDrone.Core.csproj | 10 ++ 11 files changed, 370 insertions(+) create mode 100644 src/NzbDrone.Core/AutoImport.derp/AutoImportBase.cs create mode 100644 src/NzbDrone.Core/AutoImport.derp/AutoImportDefinition.cs create mode 100644 src/NzbDrone.Core/AutoImport.derp/AutoImportRepository.cs create mode 100644 src/NzbDrone.Core/AutoImport.derp/IAutoImport.cs create mode 100644 src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchList.cs create mode 100644 src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchListProxy.cs create mode 100644 src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchListSettings.cs create mode 100644 src/NzbDrone.Core/AutoImporter/AutoImporterBase.cs create mode 100644 src/NzbDrone.Core/AutoImporter/AutoImporterDefinition.cs create mode 100644 src/NzbDrone.Core/AutoImporter/IAutoImporter.cs diff --git a/src/NzbDrone.Core/AutoImport.derp/AutoImportBase.cs b/src/NzbDrone.Core/AutoImport.derp/AutoImportBase.cs new file mode 100644 index 000000000..9dc51714c --- /dev/null +++ b/src/NzbDrone.Core/AutoImport.derp/AutoImportBase.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using FluentValidation.Results; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.AutoImport +{ + public abstract class AutoImportBase : IAutoImport where TSettings : IProviderConfig, new() + { + public abstract string Name { get; } + + public Type ConfigContract => typeof(TSettings); + + public virtual ProviderMessage Message => null; + + public IEnumerable DefaultDefinitions => new List(); + + public ProviderDefinition Definition { get; set; } + public abstract ValidationResult Test(); + + public abstract string Link { get; } + + protected TSettings Settings => (TSettings)Definition.Settings; + + public override string ToString() + { + return GetType().Name; + } + + public abstract bool Enabled { get; } + + public virtual object RequestAction(string action, IDictionary query) { return null; } + } +} diff --git a/src/NzbDrone.Core/AutoImport.derp/AutoImportDefinition.cs b/src/NzbDrone.Core/AutoImport.derp/AutoImportDefinition.cs new file mode 100644 index 000000000..f1d53e72e --- /dev/null +++ b/src/NzbDrone.Core/AutoImport.derp/AutoImportDefinition.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.AutoImport +{ + public class AutoImportDefinition : ProviderDefinition + { + public AutoImportDefinition() + { + Tags = new HashSet(); + } + + public bool Enabled { get; set; } + public HashSet Tags { get; set; } + + public override bool Enable => Enabled; + } +} diff --git a/src/NzbDrone.Core/AutoImport.derp/AutoImportRepository.cs b/src/NzbDrone.Core/AutoImport.derp/AutoImportRepository.cs new file mode 100644 index 000000000..398bcaf99 --- /dev/null +++ b/src/NzbDrone.Core/AutoImport.derp/AutoImportRepository.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.ThingiProvider; + + +namespace NzbDrone.Core.AutoImport +{ + public interface IAutoImportRepository : IProviderRepository + { + + } + + public class AutoImportRepository : ProviderRepository, IAutoImportRepository + { + public AutoImportRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/AutoImport.derp/IAutoImport.cs b/src/NzbDrone.Core/AutoImport.derp/IAutoImport.cs new file mode 100644 index 000000000..5597ddee8 --- /dev/null +++ b/src/NzbDrone.Core/AutoImport.derp/IAutoImport.cs @@ -0,0 +1,12 @@ +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.AutoImport +{ + public interface IAutoImport : IProvider + { + string Link { get; } + bool Enabled { get; } + + // void OnGrab(GrabMessage grabMessage); + } +} diff --git a/src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchList.cs b/src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchList.cs new file mode 100644 index 000000000..d060e1362 --- /dev/null +++ b/src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchList.cs @@ -0,0 +1,43 @@ + +using System.Collections.Generic; +using FluentValidation.Results; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Tv; +using NzbDrone.Core.AutoImport; +using NzbDrone.Core.AutoImport.IMDbWatchList; +using System; + +namespace NzbDrone.Core.AutoImpoter.IMDbWatchList +{ + public class IMDbWatchList : AutoImportBase + { + public override bool Enabled + { + get + { + throw new NotImplementedException(); + } + } + + //private readonly INotifyMyAndroidProxy _proxy; + + //public NotifyMyAndroid(INotifyMyAndroidProxy proxy) + //{ + // _proxy = proxy; + //} + + public override string Link => "http://rss.imdb.com/"; + + public override string Name => "IMDb Public Watchlist"; + + + public override ValidationResult Test() + { + var failures = new List(); + + // failures.AddIfNotNull(_proxy.Test(Settings)); + + return new ValidationResult(failures); + } + } +} diff --git a/src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchListProxy.cs b/src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchListProxy.cs new file mode 100644 index 000000000..28561ef66 --- /dev/null +++ b/src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchListProxy.cs @@ -0,0 +1,79 @@ +using System; +using System.Linq; +using System.Net; +using System.Xml.Linq; +using FluentValidation.Results; +using NLog; +using NzbDrone.Core.Exceptions; +using RestSharp; +using NzbDrone.Core.Rest; + +namespace NzbDrone.Core.AutoImport.IMDbWatchList +{ + public interface IIMDbWatchListProxy + { + void ImportMovies(string url); + ValidationFailure Test(IMDbWatchListSettings settings); + } + + public class IMDbWatchListProxy : IIMDbWatchListProxy + { + private readonly Logger _logger; + private const string URL = "http://rss.imdb.com"; + + public IMDbWatchListProxy(Logger logger) + { + _logger = logger; + } + + public void ImportMovies(string id) + { + var client = RestClientFactory.BuildClient(URL); + var request = new RestRequest("/list/{id}", Method.GET); + request.RequestFormat = DataFormat.Xml; + request.AddParameter("id", id, ParameterType.UrlSegment); + + var response = client.ExecuteAndValidate(request); + ValidateResponse(response); + } + + private void Verify(string id) + { + var client = RestClientFactory.BuildClient(URL); + var request = new RestRequest("/list/{id}", Method.GET); + request.RequestFormat = DataFormat.Xml; + request.AddParameter("id", id, ParameterType.UrlSegment); + + var response = client.ExecuteAndValidate(request); + ValidateResponse(response); + } + + private void ValidateResponse(IRestResponse response) + { + var xDoc = XDocument.Parse(response.Content); + var nma = xDoc.Descendants("nma").Single(); + var error = nma.Descendants("error").SingleOrDefault(); + + if (error != null) + { + ((HttpStatusCode)Convert.ToInt32(error.Attribute("code").Value)).VerifyStatusCode(error.Value); + } + } + + public ValidationFailure Test(IMDbWatchListSettings settings) + { + try + { + Verify(settings.IMDbWatchListId); + ImportMovies(settings.IMDbWatchListId); + } + catch (Exception ex) + { + _logger.Error(ex, "Unable to import movies: " + ex.Message); + return new ValidationFailure("IMDbWatchListId", "Unable to import movies"); + } + + return null; + } + } +} diff --git a/src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchListSettings.cs b/src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchListSettings.cs new file mode 100644 index 000000000..4ecd75c72 --- /dev/null +++ b/src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchListSettings.cs @@ -0,0 +1,30 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.AutoImport.IMDbWatchList +{ + public class IMDbWatchListSettingsValidator : AbstractValidator + { + public IMDbWatchListSettingsValidator() + { + RuleFor(c => c.IMDbWatchListId).NotEmpty(); + } + } + + public class IMDbWatchListSettings : IProviderConfig + { + private static readonly IMDbWatchListSettingsValidator Validator = new IMDbWatchListSettingsValidator(); + + [FieldDefinition(0, Label = "Watch List Id", HelpLink = "http://rss.imdb.com/list/")] + public string IMDbWatchListId { get; set; } + + public bool IsValid => !string.IsNullOrWhiteSpace(IMDbWatchListId); + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/AutoImporter/AutoImporterBase.cs b/src/NzbDrone.Core/AutoImporter/AutoImporterBase.cs new file mode 100644 index 000000000..23f6c4aba --- /dev/null +++ b/src/NzbDrone.Core/AutoImporter/AutoImporterBase.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.AutoImporter +{ + public abstract class AutoImporterBase : IAutoImporter + where TSettings : IProviderConfig, new() + { + // protected readonly IAutoImporterStatusService _autoImporterStatusService; + protected readonly IConfigService _configService; + protected readonly IParsingService _parsingService; + protected readonly Logger _logger; + + public abstract string Name { get; } + // public abstract DownloadProtocol Protocol { get; } + public abstract string Link { get; } + + public abstract bool Enabled { get; } + // public abstract bool SupportsSearch { get; } + + public AutoImporterBase(/*IAutoImporterStatusService autoImporterStatusService, */IConfigService configService, IParsingService parsingService, Logger logger) + { + //_autoImporterStatusService = autoImporterStatusService; + _configService = configService; + _parsingService = parsingService; + _logger = logger; + } + + public Type ConfigContract => typeof(TSettings); + + public virtual ProviderMessage Message => null; + + public virtual IEnumerable DefaultDefinitions + { + get + { + var config = (IProviderConfig)new TSettings(); + + yield return new AutoImporterDefinition + { + Name = GetType().Name, + Link = Link, + Enabled = config.Validate().IsValid && Enabled, + Implementation = GetType().Name, + Settings = config + }; + } + } + + public virtual ProviderDefinition Definition { get; set; } + + public virtual object RequestAction(string action, IDictionary query) { return null; } + + protected TSettings Settings => (TSettings)Definition.Settings; + + public abstract IList Fetch(); + + public ValidationResult Test() + { + var failures = new List(); + + try + { + Test(failures); + } + catch (Exception ex) + { + _logger.Error(ex, "Test aborted due to exception"); + failures.Add(new ValidationFailure(string.Empty, "Test was aborted due to an error: " + ex.Message)); + } + + return new ValidationResult(failures); + } + + protected abstract void Test(List failures); + + public override string ToString() + { + return Definition.Name; + } + } +} diff --git a/src/NzbDrone.Core/AutoImporter/AutoImporterDefinition.cs b/src/NzbDrone.Core/AutoImporter/AutoImporterDefinition.cs new file mode 100644 index 000000000..2446fb817 --- /dev/null +++ b/src/NzbDrone.Core/AutoImporter/AutoImporterDefinition.cs @@ -0,0 +1,17 @@ +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.AutoImporter +{ + public class AutoImporterDefinition : ProviderDefinition + { + public bool Enabled { get; set; } + public string Link { get; set; } + //public DownloadProtocol Protocol { get; set; } + //public bool SupportsRss { get; set; } + //public bool SupportsSearch { get; set; } + + public override bool Enable => Enabled; + + // public IndexerStatus Status { get; set; } + } +} diff --git a/src/NzbDrone.Core/AutoImporter/IAutoImporter.cs b/src/NzbDrone.Core/AutoImporter/IAutoImporter.cs new file mode 100644 index 000000000..246e94f92 --- /dev/null +++ b/src/NzbDrone.Core/AutoImporter/IAutoImporter.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.AutoImporter +{ + public interface IAutoImporter : IProvider + { + string Link { get; } + bool Enabled { get; } + + IList Fetch(); + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 3ea6a0a76..3a6153c86 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -122,6 +122,16 @@ + + + + + + + + + + From d123ca606308183806d6874529ca6be5bf7fa432 Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Wed, 11 Jan 2017 16:12:17 -0500 Subject: [PATCH 02/83] updates --- src/NzbDrone.Core/AutoImporter/AutoImporterDefinition.cs | 6 ------ src/NzbDrone.Core/AutoImporter/IAutoImporter.cs | 3 ++- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/NzbDrone.Core/AutoImporter/AutoImporterDefinition.cs b/src/NzbDrone.Core/AutoImporter/AutoImporterDefinition.cs index 2446fb817..b3fad1178 100644 --- a/src/NzbDrone.Core/AutoImporter/AutoImporterDefinition.cs +++ b/src/NzbDrone.Core/AutoImporter/AutoImporterDefinition.cs @@ -6,12 +6,6 @@ namespace NzbDrone.Core.AutoImporter { public bool Enabled { get; set; } public string Link { get; set; } - //public DownloadProtocol Protocol { get; set; } - //public bool SupportsRss { get; set; } - //public bool SupportsSearch { get; set; } - public override bool Enable => Enabled; - - // public IndexerStatus Status { get; set; } } } diff --git a/src/NzbDrone.Core/AutoImporter/IAutoImporter.cs b/src/NzbDrone.Core/AutoImporter/IAutoImporter.cs index 246e94f92..f78f90141 100644 --- a/src/NzbDrone.Core/AutoImporter/IAutoImporter.cs +++ b/src/NzbDrone.Core/AutoImporter/IAutoImporter.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.AutoImporter { @@ -10,6 +11,6 @@ namespace NzbDrone.Core.AutoImporter string Link { get; } bool Enabled { get; } - IList Fetch(); + IList Fetch(); } } \ No newline at end of file From 4ded288c5d9b9057dab02a87ec59440c6faa2a91 Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Thu, 12 Jan 2017 18:33:30 -0500 Subject: [PATCH 03/83] few changes --- src/NzbDrone.Core/AutoImporter/AutoImporterBase.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/NzbDrone.Core/AutoImporter/AutoImporterBase.cs b/src/NzbDrone.Core/AutoImporter/AutoImporterBase.cs index 23f6c4aba..7d95bdfe8 100644 --- a/src/NzbDrone.Core/AutoImporter/AutoImporterBase.cs +++ b/src/NzbDrone.Core/AutoImporter/AutoImporterBase.cs @@ -9,27 +9,24 @@ using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.AutoImporter { public abstract class AutoImporterBase : IAutoImporter where TSettings : IProviderConfig, new() { - // protected readonly IAutoImporterStatusService _autoImporterStatusService; protected readonly IConfigService _configService; protected readonly IParsingService _parsingService; protected readonly Logger _logger; public abstract string Name { get; } - // public abstract DownloadProtocol Protocol { get; } public abstract string Link { get; } public abstract bool Enabled { get; } - // public abstract bool SupportsSearch { get; } - public AutoImporterBase(/*IAutoImporterStatusService autoImporterStatusService, */IConfigService configService, IParsingService parsingService, Logger logger) + public AutoImporterBase(IConfigService configService, IParsingService parsingService, Logger logger) { - //_autoImporterStatusService = autoImporterStatusService; _configService = configService; _parsingService = parsingService; _logger = logger; @@ -62,7 +59,7 @@ namespace NzbDrone.Core.AutoImporter protected TSettings Settings => (TSettings)Definition.Settings; - public abstract IList Fetch(); + public abstract IList Fetch(); public ValidationResult Test() { @@ -87,5 +84,6 @@ namespace NzbDrone.Core.AutoImporter { return Definition.Name; } + } } From ec1c81e3ed1ea29ab8b350ac77c3cb8ff952594d Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Sun, 15 Jan 2017 15:28:35 -0500 Subject: [PATCH 04/83] updates and compile-able --- .../AutoImport.derp/AutoImportBase.cs | 35 ------------ .../AutoImport.derp/AutoImportDefinition.cs | 18 ------ .../AutoImport.derp/AutoImportRepository.cs | 20 ------- .../AutoImport.derp/IAutoImport.cs | 12 ---- .../IMDbWatchList/IMDbWatchList.cs | 43 -------------- .../Migration/119_create_netimport_table.cs | 18 ++++++ .../Exceptions/NetImportException.cs | 23 ++++++++ .../NetImport/HttpNetImportBase.cs | 56 +++++++++++++++++++ .../NetImport/IMDbWatchList/IMDbWatchList.cs | 35 ++++++++++++ .../IMDbWatchList/IMDbWatchListAPI.cs | 40 +++++++++++++ .../IMDbWatchList/IMDbWatchListParser.cs | 54 ++++++++++++++++++ .../IMDbWatchList/IMDbWatchListProxy.cs | 6 +- .../IMDbWatchListRequestGenerator.cs | 34 +++++++++++ .../IMDbWatchList/IMDbWatchListSettings.cs | 15 +++-- .../INetImport.cs} | 4 +- .../NetImport/INetImportRequestGenerator.cs | 9 +++ .../NetImport/IProcessNetImportResponse.cs | 11 ++++ .../NetImportBase.cs} | 8 +-- .../NetImportDefinition.cs} | 4 +- .../NetImport/NetImportPageableRequest.cs | 25 +++++++++ .../NetImportPageableRequestChain.cs | 48 ++++++++++++++++ .../NetImport/NetImportRepository.cs | 20 +++++++ .../NetImport/NetImportRequest.cs | 21 +++++++ .../NetImport/NetImportResponse.cs | 24 ++++++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 28 ++++++---- 25 files changed, 457 insertions(+), 154 deletions(-) delete mode 100644 src/NzbDrone.Core/AutoImport.derp/AutoImportBase.cs delete mode 100644 src/NzbDrone.Core/AutoImport.derp/AutoImportDefinition.cs delete mode 100644 src/NzbDrone.Core/AutoImport.derp/AutoImportRepository.cs delete mode 100644 src/NzbDrone.Core/AutoImport.derp/IAutoImport.cs delete mode 100644 src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchList.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/119_create_netimport_table.cs create mode 100644 src/NzbDrone.Core/NetImport/Exceptions/NetImportException.cs create mode 100644 src/NzbDrone.Core/NetImport/HttpNetImportBase.cs create mode 100644 src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchList.cs create mode 100644 src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListAPI.cs create mode 100644 src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListParser.cs rename src/NzbDrone.Core/{AutoImport.derp => NetImport}/IMDbWatchList/IMDbWatchListProxy.cs (93%) create mode 100644 src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListRequestGenerator.cs rename src/NzbDrone.Core/{AutoImport.derp => NetImport}/IMDbWatchList/IMDbWatchListSettings.cs (60%) rename src/NzbDrone.Core/{AutoImporter/IAutoImporter.cs => NetImport/INetImport.cs} (77%) create mode 100644 src/NzbDrone.Core/NetImport/INetImportRequestGenerator.cs create mode 100644 src/NzbDrone.Core/NetImport/IProcessNetImportResponse.cs rename src/NzbDrone.Core/{AutoImporter/AutoImporterBase.cs => NetImport/NetImportBase.cs} (89%) rename src/NzbDrone.Core/{AutoImporter/AutoImporterDefinition.cs => NetImport/NetImportDefinition.cs} (65%) create mode 100644 src/NzbDrone.Core/NetImport/NetImportPageableRequest.cs create mode 100644 src/NzbDrone.Core/NetImport/NetImportPageableRequestChain.cs create mode 100644 src/NzbDrone.Core/NetImport/NetImportRepository.cs create mode 100644 src/NzbDrone.Core/NetImport/NetImportRequest.cs create mode 100644 src/NzbDrone.Core/NetImport/NetImportResponse.cs diff --git a/src/NzbDrone.Core/AutoImport.derp/AutoImportBase.cs b/src/NzbDrone.Core/AutoImport.derp/AutoImportBase.cs deleted file mode 100644 index 9dc51714c..000000000 --- a/src/NzbDrone.Core/AutoImport.derp/AutoImportBase.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using FluentValidation.Results; -using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.AutoImport -{ - public abstract class AutoImportBase : IAutoImport where TSettings : IProviderConfig, new() - { - public abstract string Name { get; } - - public Type ConfigContract => typeof(TSettings); - - public virtual ProviderMessage Message => null; - - public IEnumerable DefaultDefinitions => new List(); - - public ProviderDefinition Definition { get; set; } - public abstract ValidationResult Test(); - - public abstract string Link { get; } - - protected TSettings Settings => (TSettings)Definition.Settings; - - public override string ToString() - { - return GetType().Name; - } - - public abstract bool Enabled { get; } - - public virtual object RequestAction(string action, IDictionary query) { return null; } - } -} diff --git a/src/NzbDrone.Core/AutoImport.derp/AutoImportDefinition.cs b/src/NzbDrone.Core/AutoImport.derp/AutoImportDefinition.cs deleted file mode 100644 index f1d53e72e..000000000 --- a/src/NzbDrone.Core/AutoImport.derp/AutoImportDefinition.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.ThingiProvider; - -namespace NzbDrone.Core.AutoImport -{ - public class AutoImportDefinition : ProviderDefinition - { - public AutoImportDefinition() - { - Tags = new HashSet(); - } - - public bool Enabled { get; set; } - public HashSet Tags { get; set; } - - public override bool Enable => Enabled; - } -} diff --git a/src/NzbDrone.Core/AutoImport.derp/AutoImportRepository.cs b/src/NzbDrone.Core/AutoImport.derp/AutoImportRepository.cs deleted file mode 100644 index 398bcaf99..000000000 --- a/src/NzbDrone.Core/AutoImport.derp/AutoImportRepository.cs +++ /dev/null @@ -1,20 +0,0 @@ -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.ThingiProvider; - - -namespace NzbDrone.Core.AutoImport -{ - public interface IAutoImportRepository : IProviderRepository - { - - } - - public class AutoImportRepository : ProviderRepository, IAutoImportRepository - { - public AutoImportRepository(IMainDatabase database, IEventAggregator eventAggregator) - : base(database, eventAggregator) - { - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/AutoImport.derp/IAutoImport.cs b/src/NzbDrone.Core/AutoImport.derp/IAutoImport.cs deleted file mode 100644 index 5597ddee8..000000000 --- a/src/NzbDrone.Core/AutoImport.derp/IAutoImport.cs +++ /dev/null @@ -1,12 +0,0 @@ -using NzbDrone.Core.ThingiProvider; - -namespace NzbDrone.Core.AutoImport -{ - public interface IAutoImport : IProvider - { - string Link { get; } - bool Enabled { get; } - - // void OnGrab(GrabMessage grabMessage); - } -} diff --git a/src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchList.cs b/src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchList.cs deleted file mode 100644 index d060e1362..000000000 --- a/src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchList.cs +++ /dev/null @@ -1,43 +0,0 @@ - -using System.Collections.Generic; -using FluentValidation.Results; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Tv; -using NzbDrone.Core.AutoImport; -using NzbDrone.Core.AutoImport.IMDbWatchList; -using System; - -namespace NzbDrone.Core.AutoImpoter.IMDbWatchList -{ - public class IMDbWatchList : AutoImportBase - { - public override bool Enabled - { - get - { - throw new NotImplementedException(); - } - } - - //private readonly INotifyMyAndroidProxy _proxy; - - //public NotifyMyAndroid(INotifyMyAndroidProxy proxy) - //{ - // _proxy = proxy; - //} - - public override string Link => "http://rss.imdb.com/"; - - public override string Name => "IMDb Public Watchlist"; - - - public override ValidationResult Test() - { - var failures = new List(); - - // failures.AddIfNotNull(_proxy.Test(Settings)); - - return new ValidationResult(failures); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/119_create_netimport_table.cs b/src/NzbDrone.Core/Datastore/Migration/119_create_netimport_table.cs new file mode 100644 index 000000000..7784ad1c4 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/119_create_netimport_table.cs @@ -0,0 +1,18 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(119)] + public class create_netimport_table : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("NetImport") + .WithColumn("Enabled").AsBoolean() + .WithColumn("Name").AsString().Unique() + .WithColumn("Implementation").AsString() + .WithColumn("Settings").AsString().Nullable(); + } + } +} diff --git a/src/NzbDrone.Core/NetImport/Exceptions/NetImportException.cs b/src/NzbDrone.Core/NetImport/Exceptions/NetImportException.cs new file mode 100644 index 000000000..d3444d991 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Exceptions/NetImportException.cs @@ -0,0 +1,23 @@ +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.NetImport.Exceptions +{ + public class NetImportException : NzbDroneException + { + private readonly NetImportResponse _netImportResponse; + + public NetImportException(NetImportResponse response, string message, params object[] args) + : base(message, args) + { + _netImportResponse = response; + } + + public NetImportException(NetImportResponse response, string message) + : base(message) + { + _netImportResponse = response; + } + + public NetImportResponse Response => _netImportResponse; + } +} diff --git a/src/NzbDrone.Core/NetImport/HttpNetImportBase.cs b/src/NzbDrone.Core/NetImport/HttpNetImportBase.cs new file mode 100644 index 000000000..9d5582a9c --- /dev/null +++ b/src/NzbDrone.Core/NetImport/HttpNetImportBase.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Http.CloudFlare; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.NetImport +{ + public abstract class HttpNetImportBase : NetImportBase + where TSettings : IProviderConfig, new() + { + protected const int MaxNumResultsPerQuery = 1000; + + protected readonly IHttpClient _httpClient; + + public override bool Enabled => true; + + public virtual TimeSpan RateLimit => TimeSpan.FromSeconds(2); + + public abstract INetImportRequestGenerator GetRequestGenerator(); + public abstract IParseNetImportResponse GetParser(); + + public HttpNetImportBase(IHttpClient httpClient, IConfigService configService, IParsingService parsingService, Logger logger) + : base(configService, parsingService, logger) + { + _httpClient = httpClient; + } + + public override IList Fetch() + { + return new List(); + } + + protected override void Test(List failures) + { + throw new NotImplementedException(); + } + + protected virtual ValidationFailure TestConnection() + { + throw new NotImplementedException(); + } + } + +} diff --git a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchList.cs b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchList.cs new file mode 100644 index 000000000..d97fb99c2 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchList.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.PassThePopcorn; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.NetImport.IMDbWatchList +{ + public class IMDbWatchList : HttpNetImportBase + { + public override string Name => "IMDbWatchList"; + public override string Link => "http://rss.imdb.com/list/"; + public override bool Enabled => true; + + + public IMDbWatchList(IHttpClient httpClient, IConfigService configService, IParsingService parsingService, Logger logger) + : base(httpClient, configService, parsingService, logger) + { } + + public override INetImportRequestGenerator GetRequestGenerator() + { + return new IMDbWatchListRequestGenerator() { Settings = Settings }; + } + + public override IParseNetImportResponse GetParser() + { + return new IMDbWatchListParser(Settings); + } + } +} diff --git a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListAPI.cs b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListAPI.cs new file mode 100644 index 000000000..73570c897 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListAPI.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace NzbDrone.Core.NetImport.IMDbWatchList +{ + class IMDbWatchListAPI + { + [XmlRoot(ElementName = "item")] + public class Movie + { + [XmlElement(ElementName = "pubDate")] + public string PublishDate { get; set; } + [XmlElement(ElementName = "title")] + public string Title { get; set; } + [XmlElement(ElementName = "link")] + public string Link { get; set; } + [XmlElement(ElementName = "guid")] + public string Guid { get; set; } + [XmlElement(ElementName = "description")] + public string Description { get; set; } + } + + [XmlRoot(ElementName = "channel")] + public class Channel + { + [XmlElement(ElementName = "title")] + public string Title { get; set; } + [XmlElement(ElementName = "link")] + public string Link { get; set; } + [XmlElement(ElementName = "description")] + public string Description { get; set; } + [XmlElement(ElementName = "pubDate")] + public string PublishDate { get; set; } + [XmlElement(ElementName = "lastBuildDate")] + public string LastBuildDate { get; set; } + [XmlElement(ElementName = "item")] + public List Movie { get; set; } + } + } +} diff --git a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListParser.cs b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListParser.cs new file mode 100644 index 000000000..7fd666141 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListParser.cs @@ -0,0 +1,54 @@ +using Newtonsoft.Json; +using NzbDrone.Core.NetImport.Exceptions; +using NzbDrone.Core.Tv; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; + +namespace NzbDrone.Core.NetImport.IMDbWatchList +{ + public class IMDbWatchListParser : IParseNetImportResponse + { + private readonly IMDbWatchListSettings _settings; + + public IMDbWatchListParser(IMDbWatchListSettings settings) + { + _settings = settings; + } + + public IList ParseResponse(NetImportResponse netImportResponse) + { + var torrentInfos = new List(); + + if (netImportResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + { + throw new NetImportException(netImportResponse, + "Unexpected response status {0} code from API request", + netImportResponse.HttpResponse.StatusCode); + } + + var jsonResponse = JsonConvert.DeserializeObject(netImportResponse.Content); + + var responseData = jsonResponse.Movie; + if (responseData == null) + { + throw new NetImportException(netImportResponse, + "This list has no movies"); + } + + foreach (var result in responseData) + { + torrentInfos.Add(new Movie() + { + Title = Parser.Parser.ParseMovieTitle(result.Title, false).MovieTitle, + Year = Parser.Parser.ParseMovieTitle(result.Title, false).Year, + ImdbId = Parser.Parser.ParseImdbId(result.Link).ToString() + }); + } + + return torrentInfos.ToArray(); + } + } +} diff --git a/src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchListProxy.cs b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListProxy.cs similarity index 93% rename from src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchListProxy.cs rename to src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListProxy.cs index 28561ef66..664919c52 100644 --- a/src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchListProxy.cs +++ b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListProxy.cs @@ -8,7 +8,7 @@ using NzbDrone.Core.Exceptions; using RestSharp; using NzbDrone.Core.Rest; -namespace NzbDrone.Core.AutoImport.IMDbWatchList +namespace NzbDrone.Core.NetImport.IMDbWatchList { public interface IIMDbWatchListProxy { @@ -64,8 +64,8 @@ namespace NzbDrone.Core.AutoImport.IMDbWatchList { try { - Verify(settings.IMDbWatchListId); - ImportMovies(settings.IMDbWatchListId); + Verify(settings.Link); + ImportMovies(settings.Link); } catch (Exception ex) { diff --git a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListRequestGenerator.cs b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListRequestGenerator.cs new file mode 100644 index 000000000..cfd92810b --- /dev/null +++ b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListRequestGenerator.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.IndexerSearch.Definitions; + +namespace NzbDrone.Core.NetImport.IMDbWatchList +{ + public class IMDbWatchListRequestGenerator : INetImportRequestGenerator + { + public IMDbWatchListSettings Settings { get; set; } + + public virtual NetImportPageableRequestChain GetMovies() + { + var pageableRequests = new NetImportPageableRequestChain(); + + pageableRequests.Add(GetMovies(null)); + + return pageableRequests; + } + + public NetImportPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + return new NetImportPageableRequestChain(); + } + + private IEnumerable GetMovies(string searchParameters) + { + var request = new NetImportRequest($"{Settings.Link.Trim()}", HttpAccept.Rss); + yield return request; + } + } +} diff --git a/src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchListSettings.cs b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListSettings.cs similarity index 60% rename from src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchListSettings.cs rename to src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListSettings.cs index 4ecd75c72..f63e7206f 100644 --- a/src/NzbDrone.Core/AutoImport.derp/IMDbWatchList/IMDbWatchListSettings.cs +++ b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListSettings.cs @@ -3,13 +3,13 @@ using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; -namespace NzbDrone.Core.AutoImport.IMDbWatchList +namespace NzbDrone.Core.NetImport.IMDbWatchList { public class IMDbWatchListSettingsValidator : AbstractValidator { public IMDbWatchListSettingsValidator() { - RuleFor(c => c.IMDbWatchListId).NotEmpty(); + RuleFor(c => c.Link).NotEmpty(); } } @@ -17,10 +17,15 @@ namespace NzbDrone.Core.AutoImport.IMDbWatchList { private static readonly IMDbWatchListSettingsValidator Validator = new IMDbWatchListSettingsValidator(); - [FieldDefinition(0, Label = "Watch List Id", HelpLink = "http://rss.imdb.com/list/")] - public string IMDbWatchListId { get; set; } + public IMDbWatchListSettings() + { + Link = "http://rss.imdb.com/list/"; + } - public bool IsValid => !string.IsNullOrWhiteSpace(IMDbWatchListId); + [FieldDefinition(0, Label = "Watch List RSS link", HelpLink = "http://rss.imdb.com/list/")] + public string Link { get; set; } + + public bool IsValid => !string.IsNullOrWhiteSpace(Link); public NzbDroneValidationResult Validate() { diff --git a/src/NzbDrone.Core/AutoImporter/IAutoImporter.cs b/src/NzbDrone.Core/NetImport/INetImport.cs similarity index 77% rename from src/NzbDrone.Core/AutoImporter/IAutoImporter.cs rename to src/NzbDrone.Core/NetImport/INetImport.cs index f78f90141..82ec79629 100644 --- a/src/NzbDrone.Core/AutoImporter/IAutoImporter.cs +++ b/src/NzbDrone.Core/NetImport/INetImport.cs @@ -4,9 +4,9 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Tv; -namespace NzbDrone.Core.AutoImporter +namespace NzbDrone.Core.NetImport { - public interface IAutoImporter : IProvider + public interface INetImport : IProvider { string Link { get; } bool Enabled { get; } diff --git a/src/NzbDrone.Core/NetImport/INetImportRequestGenerator.cs b/src/NzbDrone.Core/NetImport/INetImportRequestGenerator.cs new file mode 100644 index 000000000..d2c8107a4 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/INetImportRequestGenerator.cs @@ -0,0 +1,9 @@ +using NzbDrone.Core.IndexerSearch.Definitions; + +namespace NzbDrone.Core.NetImport +{ + public interface INetImportRequestGenerator + { + NetImportPageableRequestChain GetMovies(); + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/NetImport/IProcessNetImportResponse.cs b/src/NzbDrone.Core/NetImport/IProcessNetImportResponse.cs new file mode 100644 index 000000000..4776f551e --- /dev/null +++ b/src/NzbDrone.Core/NetImport/IProcessNetImportResponse.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.NetImport +{ + public interface IParseNetImportResponse + { + IList ParseResponse(NetImportResponse netMovieImporterResponse); + } +} diff --git a/src/NzbDrone.Core/AutoImporter/AutoImporterBase.cs b/src/NzbDrone.Core/NetImport/NetImportBase.cs similarity index 89% rename from src/NzbDrone.Core/AutoImporter/AutoImporterBase.cs rename to src/NzbDrone.Core/NetImport/NetImportBase.cs index 7d95bdfe8..a3d2723e2 100644 --- a/src/NzbDrone.Core/AutoImporter/AutoImporterBase.cs +++ b/src/NzbDrone.Core/NetImport/NetImportBase.cs @@ -11,9 +11,9 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Tv; -namespace NzbDrone.Core.AutoImporter +namespace NzbDrone.Core.NetImport { - public abstract class AutoImporterBase : IAutoImporter + public abstract class NetImportBase : INetImport where TSettings : IProviderConfig, new() { protected readonly IConfigService _configService; @@ -25,7 +25,7 @@ namespace NzbDrone.Core.AutoImporter public abstract bool Enabled { get; } - public AutoImporterBase(IConfigService configService, IParsingService parsingService, Logger logger) + public NetImportBase(IConfigService configService, IParsingService parsingService, Logger logger) { _configService = configService; _parsingService = parsingService; @@ -42,7 +42,7 @@ namespace NzbDrone.Core.AutoImporter { var config = (IProviderConfig)new TSettings(); - yield return new AutoImporterDefinition + yield return new NetImportDefinition { Name = GetType().Name, Link = Link, diff --git a/src/NzbDrone.Core/AutoImporter/AutoImporterDefinition.cs b/src/NzbDrone.Core/NetImport/NetImportDefinition.cs similarity index 65% rename from src/NzbDrone.Core/AutoImporter/AutoImporterDefinition.cs rename to src/NzbDrone.Core/NetImport/NetImportDefinition.cs index b3fad1178..6d2442eb7 100644 --- a/src/NzbDrone.Core/AutoImporter/AutoImporterDefinition.cs +++ b/src/NzbDrone.Core/NetImport/NetImportDefinition.cs @@ -1,8 +1,8 @@ using NzbDrone.Core.ThingiProvider; -namespace NzbDrone.Core.AutoImporter +namespace NzbDrone.Core.NetImport { - public class AutoImporterDefinition : ProviderDefinition + public class NetImportDefinition : ProviderDefinition { public bool Enabled { get; set; } public string Link { get; set; } diff --git a/src/NzbDrone.Core/NetImport/NetImportPageableRequest.cs b/src/NzbDrone.Core/NetImport/NetImportPageableRequest.cs new file mode 100644 index 000000000..50a43fce9 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/NetImportPageableRequest.cs @@ -0,0 +1,25 @@ +using System.Collections; +using System.Collections.Generic; + +namespace NzbDrone.Core.NetImport +{ + public class NetImportPageableRequest : IEnumerable + { + private readonly IEnumerable _enumerable; + + public NetImportPageableRequest(IEnumerable enumerable) + { + _enumerable = enumerable; + } + + public IEnumerator GetEnumerator() + { + return _enumerable.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _enumerable.GetEnumerator(); + } + } +} diff --git a/src/NzbDrone.Core/NetImport/NetImportPageableRequestChain.cs b/src/NzbDrone.Core/NetImport/NetImportPageableRequestChain.cs new file mode 100644 index 000000000..080b8727a --- /dev/null +++ b/src/NzbDrone.Core/NetImport/NetImportPageableRequestChain.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq; + +namespace NzbDrone.Core.NetImport +{ + public class NetImportPageableRequestChain + { + private List> _chains; + + public NetImportPageableRequestChain() + { + _chains = new List>(); + _chains.Add(new List()); + } + + public int Tiers => _chains.Count; + + public IEnumerable GetAllTiers() + { + return _chains.SelectMany(v => v); + } + + public IEnumerable GetTier(int index) + { + return _chains[index]; + } + + public void Add(IEnumerable request) + { + if (request == null) return; + + _chains.Last().Add(new NetImportPageableRequest(request)); + } + + public void AddTier(IEnumerable request) + { + AddTier(); + Add(request); + } + + public void AddTier() + { + if (_chains.Last().Count == 0) return; + + _chains.Add(new List()); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/NetImport/NetImportRepository.cs b/src/NzbDrone.Core/NetImport/NetImportRepository.cs new file mode 100644 index 000000000..8efa8a4a8 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/NetImportRepository.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.ThingiProvider; + + +namespace NzbDrone.Core.NetImport +{ + public interface INetImportRepository : IProviderRepository + { + + } + + public class NetImportRepository : ProviderRepository, INetImportRepository + { + public NetImportRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/NetImport/NetImportRequest.cs b/src/NzbDrone.Core/NetImport/NetImportRequest.cs new file mode 100644 index 000000000..e00fe316f --- /dev/null +++ b/src/NzbDrone.Core/NetImport/NetImportRequest.cs @@ -0,0 +1,21 @@ +using NzbDrone.Common.Http; + +namespace NzbDrone.Core.NetImport +{ + public class NetImportRequest + { + public HttpRequest HttpRequest { get; private set; } + + public NetImportRequest(string url, HttpAccept httpAccept) + { + HttpRequest = new HttpRequest(url, httpAccept); + } + + public NetImportRequest(HttpRequest httpRequest) + { + HttpRequest = httpRequest; + } + + public HttpUri Url => HttpRequest.Url; + } +} diff --git a/src/NzbDrone.Core/NetImport/NetImportResponse.cs b/src/NzbDrone.Core/NetImport/NetImportResponse.cs new file mode 100644 index 000000000..3174b0775 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/NetImportResponse.cs @@ -0,0 +1,24 @@ +using NzbDrone.Common.Http; + +namespace NzbDrone.Core.NetImport +{ + public class NetImportResponse + { + private readonly NetImportRequest _netImport; + private readonly HttpResponse _httpResponse; + + public NetImportResponse(NetImportRequest netImport, HttpResponse httpResponse) + { + _netImport = netImport; + _httpResponse = httpResponse; + } + + public NetImportRequest Request => _netImport; + + public HttpRequest HttpRequest => _httpResponse.Request; + + public HttpResponse HttpResponse => _httpResponse; + + public string Content => _httpResponse.Content; + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 563e08c8e..ebbf9c761 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -122,16 +122,24 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + + From 6878abe2a20f8f1f32e93601325642ea906fd98f Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Sun, 15 Jan 2017 15:35:38 -0500 Subject: [PATCH 05/83] whoops, only parse title once --- .../NetImport/IMDbWatchList/IMDbWatchListParser.cs | 8 +++++--- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListParser.cs b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListParser.cs index 7fd666141..4042eff95 100644 --- a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListParser.cs +++ b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListParser.cs @@ -40,11 +40,13 @@ namespace NzbDrone.Core.NetImport.IMDbWatchList foreach (var result in responseData) { + var title = Parser.Parser.ParseMovieTitle(result.Title, false); + torrentInfos.Add(new Movie() { - Title = Parser.Parser.ParseMovieTitle(result.Title, false).MovieTitle, - Year = Parser.Parser.ParseMovieTitle(result.Title, false).Year, - ImdbId = Parser.Parser.ParseImdbId(result.Link).ToString() + Title = title.MovieTitle, + Year = title.Year, + ImdbId = Parser.Parser.ParseImdbId(result.Link) }); } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index ebbf9c761..f26bca9f4 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -122,6 +122,7 @@ + From b02944a3b2eec7d5ff6b80ea0e9bbe3b10895a3a Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Tue, 17 Jan 2017 20:30:21 -0500 Subject: [PATCH 06/83] Added Qualties to Settings --- .../Datastore/Migration/119_create_netimport_table.cs | 9 +++++---- .../NetImport/IMDbWatchList/IMDbWatchList.cs | 3 ++- .../NetImport/IMDbWatchList/IMDbWatchListParser.cs | 1 + .../NetImport/IMDbWatchList/IMDbWatchListSettings.cs | 5 +++++ src/NzbDrone.Core/NetImport/NetImportBase.cs | 2 ++ src/NzbDrone.Core/NetImport/NetImportDefinition.cs | 3 ++- src/NzbDrone.Core/NzbDrone.Core.csproj | 4 +++- 7 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/NzbDrone.Core/Datastore/Migration/119_create_netimport_table.cs b/src/NzbDrone.Core/Datastore/Migration/119_create_netimport_table.cs index 7784ad1c4..086464746 100644 --- a/src/NzbDrone.Core/Datastore/Migration/119_create_netimport_table.cs +++ b/src/NzbDrone.Core/Datastore/Migration/119_create_netimport_table.cs @@ -9,10 +9,11 @@ namespace NzbDrone.Core.Datastore.Migration protected override void MainDbUpgrade() { Create.TableForModel("NetImport") - .WithColumn("Enabled").AsBoolean() - .WithColumn("Name").AsString().Unique() - .WithColumn("Implementation").AsString() - .WithColumn("Settings").AsString().Nullable(); + .WithColumn("Enabled").AsBoolean() + .WithColumn("ProfileId").AsInt32() + .WithColumn("Name").AsString().Unique() + .WithColumn("Implementation").AsString() + .WithColumn("Settings").AsString().Nullable(); } } } diff --git a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchList.cs b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchList.cs index d97fb99c2..ed5c06da4 100644 --- a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchList.cs +++ b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchList.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Xml.Serialization; using FluentValidation.Results; using NLog; using NzbDrone.Common.Http; @@ -15,9 +16,9 @@ namespace NzbDrone.Core.NetImport.IMDbWatchList { public override string Name => "IMDbWatchList"; public override string Link => "http://rss.imdb.com/list/"; + public override int ProfileId => 1; public override bool Enabled => true; - public IMDbWatchList(IHttpClient httpClient, IConfigService configService, IParsingService parsingService, Logger logger) : base(httpClient, configService, parsingService, logger) { } diff --git a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListParser.cs b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListParser.cs index 4042eff95..20fb368ec 100644 --- a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListParser.cs +++ b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListParser.cs @@ -46,6 +46,7 @@ namespace NzbDrone.Core.NetImport.IMDbWatchList { Title = title.MovieTitle, Year = title.Year, + ProfileId = _settings.ProfileId, ImdbId = Parser.Parser.ParseImdbId(result.Link) }); } diff --git a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListSettings.cs b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListSettings.cs index f63e7206f..3ca24faf3 100644 --- a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListSettings.cs +++ b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListSettings.cs @@ -1,5 +1,6 @@ using FluentValidation; using NzbDrone.Core.Annotations; +using NzbDrone.Core.Profiles; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -20,10 +21,14 @@ namespace NzbDrone.Core.NetImport.IMDbWatchList public IMDbWatchListSettings() { Link = "http://rss.imdb.com/list/"; + ProfileId = 1; } [FieldDefinition(0, Label = "Watch List RSS link", HelpLink = "http://rss.imdb.com/list/")] public string Link { get; set; } + + [FieldDefinition(1, Label = "Quality", Type = FieldType.Select, SelectOptions = typeof(Profile), HelpText = "Quality of all imported movies")] + public int ProfileId { get; set; } public bool IsValid => !string.IsNullOrWhiteSpace(Link); diff --git a/src/NzbDrone.Core/NetImport/NetImportBase.cs b/src/NzbDrone.Core/NetImport/NetImportBase.cs index a3d2723e2..0f3f6fc0f 100644 --- a/src/NzbDrone.Core/NetImport/NetImportBase.cs +++ b/src/NzbDrone.Core/NetImport/NetImportBase.cs @@ -22,6 +22,7 @@ namespace NzbDrone.Core.NetImport public abstract string Name { get; } public abstract string Link { get; } + public abstract int ProfileId { get; } public abstract bool Enabled { get; } @@ -46,6 +47,7 @@ namespace NzbDrone.Core.NetImport { Name = GetType().Name, Link = Link, + ProfileId = ProfileId, Enabled = config.Validate().IsValid && Enabled, Implementation = GetType().Name, Settings = config diff --git a/src/NzbDrone.Core/NetImport/NetImportDefinition.cs b/src/NzbDrone.Core/NetImport/NetImportDefinition.cs index 6d2442eb7..b4af0c4a2 100644 --- a/src/NzbDrone.Core/NetImport/NetImportDefinition.cs +++ b/src/NzbDrone.Core/NetImport/NetImportDefinition.cs @@ -4,8 +4,9 @@ namespace NzbDrone.Core.NetImport { public class NetImportDefinition : ProviderDefinition { - public bool Enabled { get; set; } public string Link { get; set; } + public int ProfileId { get; set; } + public bool Enabled { get; set; } public override bool Enable => Enabled; } } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index f26bca9f4..d09f0a840 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -1273,7 +1273,9 @@ - + + + From 734a36de065d035a82e81afc1d5791dd06125ca8 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Sat, 21 Jan 2017 18:42:58 +0100 Subject: [PATCH 07/83] Added some abstraction for settings. --- .../IMDbWatchList/IMDbWatchListSettings.cs | 25 ++---------- .../NetImport/NetImportBaseSettings.cs | 40 +++++++++++++++++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + 3 files changed, 45 insertions(+), 21 deletions(-) create mode 100644 src/NzbDrone.Core/NetImport/NetImportBaseSettings.cs diff --git a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListSettings.cs b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListSettings.cs index 3ca24faf3..6d10fa78c 100644 --- a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListSettings.cs +++ b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListSettings.cs @@ -6,17 +6,10 @@ using NzbDrone.Core.Validation; namespace NzbDrone.Core.NetImport.IMDbWatchList { - public class IMDbWatchListSettingsValidator : AbstractValidator - { - public IMDbWatchListSettingsValidator() - { - RuleFor(c => c.Link).NotEmpty(); - } - } - public class IMDbWatchListSettings : IProviderConfig + public class IMDbWatchListSettings : NetImportBaseSettings { - private static readonly IMDbWatchListSettingsValidator Validator = new IMDbWatchListSettingsValidator(); + //private const string helpLink = "https://imdb.com"; public IMDbWatchListSettings() { @@ -24,17 +17,7 @@ namespace NzbDrone.Core.NetImport.IMDbWatchList ProfileId = 1; } - [FieldDefinition(0, Label = "Watch List RSS link", HelpLink = "http://rss.imdb.com/list/")] - public string Link { get; set; } - - [FieldDefinition(1, Label = "Quality", Type = FieldType.Select, SelectOptions = typeof(Profile), HelpText = "Quality of all imported movies")] - public int ProfileId { get; set; } - - public bool IsValid => !string.IsNullOrWhiteSpace(Link); - - public NzbDroneValidationResult Validate() - { - return new NzbDroneValidationResult(Validator.Validate(this)); - } + [FieldDefinition(0, Label = "RSS Link", HelpText = "Link to the rss feed of movies.")] + public new string Link { get; set; } } } diff --git a/src/NzbDrone.Core/NetImport/NetImportBaseSettings.cs b/src/NzbDrone.Core/NetImport/NetImportBaseSettings.cs new file mode 100644 index 000000000..cdbbd0baf --- /dev/null +++ b/src/NzbDrone.Core/NetImport/NetImportBaseSettings.cs @@ -0,0 +1,40 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.NetImport +{ + public class NetImportBaseSettingsValidator : AbstractValidator + { + public NetImportBaseSettingsValidator() + { + RuleFor(c => c.Link).NotEmpty(); + } + } + + public class NetImportBaseSettings : IProviderConfig + { + private static readonly NetImportBaseSettingsValidator Validator = new NetImportBaseSettingsValidator(); + + public NetImportBaseSettings() + { + Link = "http://rss.imdb.com/list/"; + ProfileId = 1; + } + + [FieldDefinition(0, Label = "Link", HelpText = "Link to the list of movies.")] + public string Link { get; set; } + + [FieldDefinition(1, Label = "Quality", Type = FieldType.Select, SelectOptions = typeof(Profile), HelpText = "Quality of all imported movies")] + public int ProfileId { get; set; } + + public bool IsValid => !string.IsNullOrWhiteSpace(Link); + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index d09f0a840..06402f783 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -126,6 +126,7 @@ + From a98b69859c7dbd19ebc3476ca9d7cc4f349d56af Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Sat, 21 Jan 2017 20:29:31 +0100 Subject: [PATCH 08/83] Big Abstraction for IMDBWatchlist -> RSSImport (With a test) --- .../Files/imdb_watchlist.xml | 1760 +++++++++++++++++ .../IndexerTests/BasicRssParserFixture.cs | 1 + .../NetImport/RSSImportFixture.cs | 34 + .../NzbDrone.Core.Test.csproj | 4 + .../Migration/119_create_netimport_table.cs | 1 - .../NetImport/IMDbWatchList/IMDbWatchList.cs | 36 - .../IMDbWatchList/IMDbWatchListParser.cs | 57 - src/NzbDrone.Core/NetImport/INetImport.cs | 1 - src/NzbDrone.Core/NetImport/NetImportBase.cs | 4 - .../NetImport/NetImportDefinition.cs | 2 - .../IMDbWatchListAPI.cs | 2 +- .../IMDbWatchListProxy.cs | 6 +- .../NetImport/RSSImport/RSSImport.cs | 52 + .../NetImport/RSSImport/RSSImportParser.cs | 236 +++ .../RSSImportRequestGenerator.cs} | 6 +- .../RSSImportSettings.cs} | 8 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 12 +- 17 files changed, 2104 insertions(+), 118 deletions(-) create mode 100644 src/NzbDrone.Core.Test/Files/imdb_watchlist.xml create mode 100644 src/NzbDrone.Core.Test/NetImport/RSSImportFixture.cs delete mode 100644 src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchList.cs delete mode 100644 src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListParser.cs rename src/NzbDrone.Core/NetImport/{IMDbWatchList => RSSImport}/IMDbWatchListAPI.cs (96%) rename src/NzbDrone.Core/NetImport/{IMDbWatchList => RSSImport}/IMDbWatchListProxy.cs (92%) create mode 100644 src/NzbDrone.Core/NetImport/RSSImport/RSSImport.cs create mode 100644 src/NzbDrone.Core/NetImport/RSSImport/RSSImportParser.cs rename src/NzbDrone.Core/NetImport/{IMDbWatchList/IMDbWatchListRequestGenerator.cs => RSSImport/RSSImportRequestGenerator.cs} (82%) rename src/NzbDrone.Core/NetImport/{IMDbWatchList/IMDbWatchListSettings.cs => RSSImport/RSSImportSettings.cs} (68%) diff --git a/src/NzbDrone.Core.Test/Files/imdb_watchlist.xml b/src/NzbDrone.Core.Test/Files/imdb_watchlist.xml new file mode 100644 index 000000000..b49485a6c --- /dev/null +++ b/src/NzbDrone.Core.Test/Files/imdb_watchlist.xml @@ -0,0 +1,1760 @@ + + + + Movie Watchlist + http://www.imdb.com/list/ls005547488/ + + Fri, 15 Jul 2011 05:14:06 GMT + Tue, 25 Mar 2014 02:22:29 GMT + + Tue, 25 Mar 2014 02:22:29 GMT + Think Like a Man Too (2014) + http://www.imdb.com/title/tt2239832/ + http://www.imdb.com/title/tt2239832/ + + + + Tue, 25 Mar 2014 00:30:49 GMT + The Machine (2013) + http://www.imdb.com/title/tt2317225/ + http://www.imdb.com/title/tt2317225/ + + + + Sun, 23 Mar 2014 07:51:40 GMT + The Great Beauty (2013) + http://www.imdb.com/title/tt2358891/ + http://www.imdb.com/title/tt2358891/ + + + + Sun, 23 Mar 2014 07:51:03 GMT + A Touch of Sin (2013) + http://www.imdb.com/title/tt2852400/ + http://www.imdb.com/title/tt2852400/ + + + + Sun, 23 Mar 2014 07:49:12 GMT + All Is Lost (2013) + http://www.imdb.com/title/tt2017038/ + http://www.imdb.com/title/tt2017038/ + + + + Sat, 22 Mar 2014 05:07:32 GMT + Nymphomaniac: Vol. II (2013) + http://www.imdb.com/title/tt2382009/ + http://www.imdb.com/title/tt2382009/ + + + + Sat, 22 Mar 2014 05:07:18 GMT + The Maze Runner (2014) + http://www.imdb.com/title/tt1790864/ + http://www.imdb.com/title/tt1790864/ + + + + Thu, 16 Jan 2014 04:57:39 GMT + Winter's Tale (2014) + http://www.imdb.com/title/tt1837709/ + http://www.imdb.com/title/tt1837709/ + + + + Thu, 16 Jan 2014 04:50:58 GMT + Love at First Sight (2010 Short Film) + http://www.imdb.com/title/tt1735878/ + http://www.imdb.com/title/tt1735878/ + + + + Thu, 16 Jan 2014 04:47:51 GMT + Run & Jump (2013) + http://www.imdb.com/title/tt2343158/ + http://www.imdb.com/title/tt2343158/ + + + + Thu, 16 Jan 2014 04:45:23 GMT + The Railway Man (2013) + http://www.imdb.com/title/tt2058107/ + http://www.imdb.com/title/tt2058107/ + + + + Thu, 16 Jan 2014 04:41:47 GMT + Welcome to the Jungle (2013) + http://www.imdb.com/title/tt2193265/ + http://www.imdb.com/title/tt2193265/ + + + + Thu, 16 Jan 2014 04:38:26 GMT + Le Week-End (2013) + http://www.imdb.com/title/tt2392326/ + http://www.imdb.com/title/tt2392326/ + + + + Thu, 16 Jan 2014 04:31:57 GMT + Labor Day (2013) + http://www.imdb.com/title/tt1967545/ + http://www.imdb.com/title/tt1967545/ + + + + Thu, 16 Jan 2014 04:05:40 GMT + Grand Piano (2013) + http://www.imdb.com/title/tt2039345/ + http://www.imdb.com/title/tt2039345/ + + + + Thu, 16 Jan 2014 04:05:05 GMT + Gloria (2013) + http://www.imdb.com/title/tt2425486/ + http://www.imdb.com/title/tt2425486/ + + + + Thu, 16 Jan 2014 04:04:21 GMT + Gimme Shelter (2013) + http://www.imdb.com/title/tt1657510/ + http://www.imdb.com/title/tt1657510/ + + + + Thu, 16 Jan 2014 04:01:29 GMT + The Past (2013) + http://www.imdb.com/title/tt2404461/ + http://www.imdb.com/title/tt2404461/ + + + + Thu, 16 Jan 2014 04:00:49 GMT + Fading Gigolo (2013) + http://www.imdb.com/title/tt2258345/ + http://www.imdb.com/title/tt2258345/ + + + + Thu, 16 Jan 2014 04:00:18 GMT + Edge of Tomorrow (2014) + http://www.imdb.com/title/tt1631867/ + http://www.imdb.com/title/tt1631867/ + + + + Thu, 16 Jan 2014 03:58:29 GMT + Earth to Echo (2014) + http://www.imdb.com/title/tt2183034/ + http://www.imdb.com/title/tt2183034/ + + + + Thu, 16 Jan 2014 03:56:30 GMT + Drew: The Man Behind the Poster (2013 Documentary) + http://www.imdb.com/title/tt1486843/ + http://www.imdb.com/title/tt1486843/ + + + + Thu, 16 Jan 2014 03:55:16 GMT + Doomsdays (2013) + http://www.imdb.com/title/tt2395146/ + http://www.imdb.com/title/tt2395146/ + + + + Thu, 16 Jan 2014 03:52:31 GMT + Design Is One: The Vignellis (2012 Documentary) + http://www.imdb.com/title/tt2610862/ + http://www.imdb.com/title/tt2610862/ + + + + Thu, 16 Jan 2014 03:51:37 GMT + Eastern Promises (2007) + http://www.imdb.com/title/tt0765443/ + http://www.imdb.com/title/tt0765443/ + + + + Thu, 16 Jan 2014 03:50:43 GMT + The Machinist (2004) + http://www.imdb.com/title/tt0361862/ + http://www.imdb.com/title/tt0361862/ + + + + Thu, 16 Jan 2014 03:49:51 GMT + eXistenZ (1999) + http://www.imdb.com/title/tt0120907/ + http://www.imdb.com/title/tt0120907/ + + + + Thu, 16 Jan 2014 03:49:26 GMT + Courage Under Fire (1996) + http://www.imdb.com/title/tt0115956/ + http://www.imdb.com/title/tt0115956/ + + + + Thu, 16 Jan 2014 03:45:04 GMT + Cosmopolis (2012) + http://www.imdb.com/title/tt1480656/ + http://www.imdb.com/title/tt1480656/ + + + + Thu, 16 Jan 2014 03:44:27 GMT + Concussion (2013) + http://www.imdb.com/title/tt2296697/ + http://www.imdb.com/title/tt2296697/ + + + + Thu, 16 Jan 2014 03:43:05 GMT + Closed Curtain (2013) + http://www.imdb.com/title/tt2626926/ + http://www.imdb.com/title/tt2626926/ + + + + Thu, 16 Jan 2014 03:42:25 GMT + Charlie Countryman (2013) + http://www.imdb.com/title/tt1196948/ + http://www.imdb.com/title/tt1196948/ + + + + Thu, 16 Jan 2014 03:41:49 GMT + Captain America: The Winter Soldier (2014) + http://www.imdb.com/title/tt1843866/ + http://www.imdb.com/title/tt1843866/ + + + + Thu, 16 Jan 2014 03:40:59 GMT + Blue Is the Warmest Color (2013) + http://www.imdb.com/title/tt2278871/ + http://www.imdb.com/title/tt2278871/ + + + + Thu, 16 Jan 2014 03:39:37 GMT + Blind Detective (2013) + http://www.imdb.com/title/tt2332707/ + http://www.imdb.com/title/tt2332707/ + + + + Thu, 16 Jan 2014 03:38:05 GMT + Blended (2014) + http://www.imdb.com/title/tt1086772/ + http://www.imdb.com/title/tt1086772/ + + + + Thu, 16 Jan 2014 03:37:38 GMT + Big Bad Wolves (2013) + http://www.imdb.com/title/tt2309224/ + http://www.imdb.com/title/tt2309224/ + + + + Thu, 16 Jan 2014 03:36:35 GMT + Barefoot (2014) + http://www.imdb.com/title/tt2355495/ + http://www.imdb.com/title/tt2355495/ + + + + Thu, 16 Jan 2014 03:35:13 GMT + Bad Words (2013) + http://www.imdb.com/title/tt2170299/ + http://www.imdb.com/title/tt2170299/ + + + + Thu, 16 Jan 2014 03:34:27 GMT + A Fantastic Fear of Everything (2012) + http://www.imdb.com/title/tt2006040/ + http://www.imdb.com/title/tt2006040/ + + + + Thu, 16 Jan 2014 01:21:34 GMT + A Field in England (2013) + http://www.imdb.com/title/tt2375574/ + http://www.imdb.com/title/tt2375574/ + + + + Thu, 16 Jan 2014 01:21:14 GMT + Odd Thomas (2013) + http://www.imdb.com/title/tt1767354/ + http://www.imdb.com/title/tt1767354/ + + + + Thu, 16 Jan 2014 01:14:36 GMT + The Pretty One (2013) + http://www.imdb.com/title/tt2140577/ + http://www.imdb.com/title/tt2140577/ + + + + Thu, 16 Jan 2014 01:08:37 GMT + Awful Nice (2013) + http://www.imdb.com/title/tt1414449/ + http://www.imdb.com/title/tt1414449/ + + + + Wed, 15 Jan 2014 23:10:34 GMT + 50 to 1 (2014) + http://www.imdb.com/title/tt1777595/ + http://www.imdb.com/title/tt1777595/ + + + + Wed, 15 Jan 2014 23:09:57 GMT + $50K and a Call Girl: A Love Story (2014) + http://www.imdb.com/title/tt2106284/ + http://www.imdb.com/title/tt2106284/ + + + + Fri, 10 Jan 2014 04:48:44 GMT + Interstellar (2014) + http://www.imdb.com/title/tt0816692/ + http://www.imdb.com/title/tt0816692/ + + + + Fri, 10 Jan 2014 04:44:18 GMT + 3 Days to Kill (2014) + http://www.imdb.com/title/tt2172934/ + http://www.imdb.com/title/tt2172934/ + + + + Fri, 10 Jan 2014 04:40:50 GMT + Back in the Day (2014) + http://www.imdb.com/title/tt2246887/ + http://www.imdb.com/title/tt2246887/ + + + + Fri, 10 Jan 2014 04:36:30 GMT + 300: Rise of an Empire (2014) + http://www.imdb.com/title/tt1253863/ + http://www.imdb.com/title/tt1253863/ + + + + Fri, 10 Jan 2014 04:28:56 GMT + Small Time (2014) + http://www.imdb.com/title/tt2310109/ + http://www.imdb.com/title/tt2310109/ + + + + Fri, 10 Jan 2014 04:24:20 GMT + The Grand Budapest Hotel (2014) + http://www.imdb.com/title/tt2278388/ + http://www.imdb.com/title/tt2278388/ + + + + Fri, 10 Jan 2014 04:10:34 GMT + Dumbbells (2014) + http://www.imdb.com/title/tt1978428/ + http://www.imdb.com/title/tt1978428/ + + + + Fri, 10 Jan 2014 04:05:22 GMT + Dawn of the Planet of the Apes (2014) + http://www.imdb.com/title/tt2103281/ + http://www.imdb.com/title/tt2103281/ + + + + Fri, 22 Nov 2013 02:30:55 GMT + Beyond Outrage (2012) + http://www.imdb.com/title/tt1724962/ + http://www.imdb.com/title/tt1724962/ + + + + Fri, 22 Nov 2013 02:30:06 GMT + Belle (2013) + http://www.imdb.com/title/tt2404181/ + http://www.imdb.com/title/tt2404181/ + + + + Fri, 22 Nov 2013 02:29:41 GMT + A Simple Plan (1998) + http://www.imdb.com/title/tt0120324/ + http://www.imdb.com/title/tt0120324/ + + + + Fri, 22 Nov 2013 02:29:11 GMT + Approved for Adoption (2012) + http://www.imdb.com/title/tt1621766/ + http://www.imdb.com/title/tt1621766/ + + + + Fri, 22 Nov 2013 02:28:37 GMT + A Fierce Green Fire (2012 Documentary) + http://www.imdb.com/title/tt1539489/ + http://www.imdb.com/title/tt1539489/ + + + + Fri, 22 Nov 2013 02:28:01 GMT + Mother of George (2013) + http://www.imdb.com/title/tt2094890/ + http://www.imdb.com/title/tt2094890/ + + + + Tue, 20 Aug 2013 02:45:42 GMT + What Maisie Knew (2012) + http://www.imdb.com/title/tt1932767/ + http://www.imdb.com/title/tt1932767/ + + + + Tue, 20 Aug 2013 02:45:22 GMT + We're the Millers (2013) + http://www.imdb.com/title/tt1723121/ + http://www.imdb.com/title/tt1723121/ + + + + Tue, 20 Aug 2013 02:44:53 GMT + Visitors (2013 Documentary) + http://www.imdb.com/title/tt2936174/ + http://www.imdb.com/title/tt2936174/ + + + + Tue, 20 Aug 2013 02:43:58 GMT + Twenty Feet from Stardom (2013 Documentary) + http://www.imdb.com/title/tt2396566/ + http://www.imdb.com/title/tt2396566/ + + + + Tue, 20 Aug 2013 02:43:40 GMT + Trance (2013) + http://www.imdb.com/title/tt1924429/ + http://www.imdb.com/title/tt1924429/ + + + + Tue, 20 Aug 2013 02:42:19 GMT + This Is Martin Bonner (2013) + http://www.imdb.com/title/tt1798291/ + http://www.imdb.com/title/tt1798291/ + + + + Tue, 20 Aug 2013 02:41:50 GMT + The Purge (2013) + http://www.imdb.com/title/tt2184339/ + http://www.imdb.com/title/tt2184339/ + + + + Tue, 20 Aug 2013 02:41:27 GMT + The Place Beyond the Pines (2012) + http://www.imdb.com/title/tt1817273/ + http://www.imdb.com/title/tt1817273/ + + + + Tue, 20 Aug 2013 02:41:08 GMT + The Pervert's Guide to Ideology (2012 Documentary) + http://www.imdb.com/title/tt2152198/ + http://www.imdb.com/title/tt2152198/ + + + + Tue, 20 Aug 2013 02:40:36 GMT + The Monuments Men (2014) + http://www.imdb.com/title/tt2177771/ + http://www.imdb.com/title/tt2177771/ + + + + Tue, 20 Aug 2013 02:40:09 GMT + The Kids Are All Right (2010) + http://www.imdb.com/title/tt0842926/ + http://www.imdb.com/title/tt0842926/ + + + + Tue, 20 Aug 2013 02:39:46 GMT + The Internship (2013) + http://www.imdb.com/title/tt2234155/ + http://www.imdb.com/title/tt2234155/ + + + + Tue, 20 Aug 2013 02:39:26 GMT + The Incredible Burt Wonderstone (2013) + http://www.imdb.com/title/tt0790628/ + http://www.imdb.com/title/tt0790628/ + + + + Tue, 20 Aug 2013 02:39:03 GMT + The Company You Keep (2012) + http://www.imdb.com/title/tt1381404/ + http://www.imdb.com/title/tt1381404/ + + + + Tue, 20 Aug 2013 02:38:44 GMT + The Boxtrolls (2014) + http://www.imdb.com/title/tt0787474/ + http://www.imdb.com/title/tt0787474/ + + + + Tue, 20 Aug 2013 02:37:58 GMT + The Artist and the Model (2012) + http://www.imdb.com/title/tt1990217/ + http://www.imdb.com/title/tt1990217/ + + + + Tue, 20 Aug 2013 02:37:34 GMT + Spark: A Burning Man Story (2013 Documentary) + http://www.imdb.com/title/tt2554648/ + http://www.imdb.com/title/tt2554648/ + + + + Tue, 20 Aug 2013 02:36:42 GMT + Smash & Grab: The Story of the Pink Panthers (2013 Documentary) + http://www.imdb.com/title/tt2250032/ + http://www.imdb.com/title/tt2250032/ + + + + Tue, 20 Aug 2013 02:36:16 GMT + A Single Shot (2013) + http://www.imdb.com/title/tt1540741/ + http://www.imdb.com/title/tt1540741/ + + + + Tue, 20 Aug 2013 02:35:49 GMT + Side Effects (2013) + http://www.imdb.com/title/tt2053463/ + http://www.imdb.com/title/tt2053463/ + + + + Tue, 20 Aug 2013 02:34:43 GMT + Paradise (2013) + http://www.imdb.com/title/tt1262990/ + http://www.imdb.com/title/tt1262990/ + + + + Tue, 20 Aug 2013 02:34:00 GMT + Paperman (2012 Short Film) + http://www.imdb.com/title/tt2388725/ + http://www.imdb.com/title/tt2388725/ + + + + Tue, 20 Aug 2013 02:33:23 GMT + Once (2007) + http://www.imdb.com/title/tt0907657/ + http://www.imdb.com/title/tt0907657/ + + + + Tue, 20 Aug 2013 02:32:30 GMT + Mud (2012) + http://www.imdb.com/title/tt1935179/ + http://www.imdb.com/title/tt1935179/ + + + + Tue, 20 Aug 2013 02:31:52 GMT + Much Ado About Nothing (2012) + http://www.imdb.com/title/tt2094064/ + http://www.imdb.com/title/tt2094064/ + + + + Tue, 20 Aug 2013 02:31:32 GMT + Mama (2013) + http://www.imdb.com/title/tt2023587/ + http://www.imdb.com/title/tt2023587/ + + + + Tue, 20 Aug 2013 02:30:22 GMT + Ip Man: The Final Fight (2013) + http://www.imdb.com/title/tt2495118/ + http://www.imdb.com/title/tt2495118/ + + + + Tue, 20 Aug 2013 02:29:58 GMT + Intolerance: Love's Struggle Throughout the Ages (1916) + http://www.imdb.com/title/tt0006864/ + http://www.imdb.com/title/tt0006864/ + + + + Tue, 20 Aug 2013 02:29:26 GMT + Instructions Not Included (2013) + http://www.imdb.com/title/tt2378281/ + http://www.imdb.com/title/tt2378281/ + + + + Tue, 20 Aug 2013 02:29:02 GMT + Insidious: Chapter 2 (2013) + http://www.imdb.com/title/tt2226417/ + http://www.imdb.com/title/tt2226417/ + + + + Tue, 20 Aug 2013 02:27:50 GMT + Inequality for All (2013 Documentary) + http://www.imdb.com/title/tt2215151/ + http://www.imdb.com/title/tt2215151/ + + + + Tue, 20 Aug 2013 02:27:28 GMT + Her (2013) + http://www.imdb.com/title/tt1798709/ + http://www.imdb.com/title/tt1798709/ + + + + Tue, 20 Aug 2013 02:02:50 GMT + The Gatekeepers (2012 Documentary) + http://www.imdb.com/title/tt2309788/ + http://www.imdb.com/title/tt2309788/ + + + + Tue, 20 Aug 2013 02:02:32 GMT + Greetings from Tim Buckley (2012) + http://www.imdb.com/title/tt1823125/ + http://www.imdb.com/title/tt1823125/ + + + + Tue, 20 Aug 2013 02:02:16 GMT + Good Ol' Freda (2013 Documentary) + http://www.imdb.com/title/tt2505938/ + http://www.imdb.com/title/tt2505938/ + + + + Tue, 20 Aug 2013 02:01:56 GMT + Standing Up (2013) + http://www.imdb.com/title/tt1905042/ + http://www.imdb.com/title/tt1905042/ + + + + Tue, 20 Aug 2013 02:01:35 GMT + Gimme the Loot (2012) + http://www.imdb.com/title/tt2139919/ + http://www.imdb.com/title/tt2139919/ + + + + Tue, 20 Aug 2013 01:55:45 GMT + Frozen (2013) + http://www.imdb.com/title/tt2294629/ + http://www.imdb.com/title/tt2294629/ + + + + Tue, 20 Aug 2013 01:54:33 GMT + Enough Said (2013) + http://www.imdb.com/title/tt2390361/ + http://www.imdb.com/title/tt2390361/ + + + + Tue, 20 Aug 2013 01:53:53 GMT + Disconnect (2012) + http://www.imdb.com/title/tt1433811/ + http://www.imdb.com/title/tt1433811/ + + + + Tue, 20 Aug 2013 01:53:18 GMT + The Seventh Dwarf (2014) + http://www.imdb.com/title/tt2914892/ + http://www.imdb.com/title/tt2914892/ + + + + Tue, 20 Aug 2013 01:52:48 GMT + Delicatessen (1991) + http://www.imdb.com/title/tt0101700/ + http://www.imdb.com/title/tt0101700/ + + + + Tue, 20 Aug 2013 01:52:21 GMT + Cold Comes the Night (2013) + http://www.imdb.com/title/tt2511428/ + http://www.imdb.com/title/tt2511428/ + + + + Tue, 20 Aug 2013 01:51:51 GMT + CBGB (2013) + http://www.imdb.com/title/tt1786751/ + http://www.imdb.com/title/tt1786751/ + + + + Tue, 20 Aug 2013 01:51:25 GMT + C.O.G. (2013) + http://www.imdb.com/title/tt1650393/ + http://www.imdb.com/title/tt1650393/ + + + + Tue, 20 Aug 2013 01:50:38 GMT + Beyond the Hills (2012) + http://www.imdb.com/title/tt2258281/ + http://www.imdb.com/title/tt2258281/ + + + + Tue, 20 Aug 2013 01:49:52 GMT + Bears (2014 Documentary) + http://www.imdb.com/title/tt2458776/ + http://www.imdb.com/title/tt2458776/ + + + + Tue, 20 Aug 2013 01:47:45 GMT + A Teacher (2013) + http://www.imdb.com/title/tt2201548/ + http://www.imdb.com/title/tt2201548/ + + + + Tue, 20 Aug 2013 01:37:42 GMT + At Any Price (2012) + http://www.imdb.com/title/tt1937449/ + http://www.imdb.com/title/tt1937449/ + + + + Tue, 20 Aug 2013 01:37:18 GMT + A Strange Brand of Happy (2013) + http://www.imdb.com/title/tt2014168/ + http://www.imdb.com/title/tt2014168/ + + + + Tue, 20 Aug 2013 01:36:35 GMT + American Milkshake (2013) + http://www.imdb.com/title/tt2254364/ + http://www.imdb.com/title/tt2254364/ + + + + Tue, 20 Aug 2013 01:36:14 GMT + American Hustle (2013) + http://www.imdb.com/title/tt1800241/ + http://www.imdb.com/title/tt1800241/ + + + + Tue, 20 Aug 2013 01:33:58 GMT + Airplane! (1980) + http://www.imdb.com/title/tt0080339/ + http://www.imdb.com/title/tt0080339/ + + + + Tue, 20 Aug 2013 01:33:27 GMT + A.C.O.D. (2013) + http://www.imdb.com/title/tt1311060/ + http://www.imdb.com/title/tt1311060/ + + + + Tue, 20 Aug 2013 01:33:07 GMT + 12 O'Clock Boys (2013 Documentary) + http://www.imdb.com/title/tt2420006/ + http://www.imdb.com/title/tt2420006/ + + + + Tue, 20 Aug 2013 01:31:45 GMT + Unfinished Song (2012) + http://www.imdb.com/title/tt1047011/ + http://www.imdb.com/title/tt1047011/ + + + + Tue, 20 Aug 2013 01:31:25 GMT + The Sapphires (2012) + http://www.imdb.com/title/tt1673697/ + http://www.imdb.com/title/tt1673697/ + + + + Tue, 20 Aug 2013 01:30:59 GMT + Stories We Tell (2012 Documentary) + http://www.imdb.com/title/tt2366450/ + http://www.imdb.com/title/tt2366450/ + + + + Tue, 20 Aug 2013 01:30:29 GMT + Morning (2010) + http://www.imdb.com/title/tt1320103/ + http://www.imdb.com/title/tt1320103/ + + + + Tue, 20 Aug 2013 01:28:57 GMT + Kon-Tiki (2012) + http://www.imdb.com/title/tt1613750/ + http://www.imdb.com/title/tt1613750/ + + + + Tue, 20 Aug 2013 01:27:42 GMT + Kelly's Heroes (1970) + http://www.imdb.com/title/tt0065938/ + http://www.imdb.com/title/tt0065938/ + + + + Tue, 20 Aug 2013 01:20:13 GMT + Il Futuro (2013) + http://www.imdb.com/title/tt1992156/ + http://www.imdb.com/title/tt1992156/ + + + + Tue, 20 Aug 2013 01:18:48 GMT + Dear Zachary: A Letter to a Son About His Father (2008 Documentary) + http://www.imdb.com/title/tt1152758/ + http://www.imdb.com/title/tt1152758/ + + + + Tue, 20 Aug 2013 01:17:34 GMT + August: Osage County (2013) + http://www.imdb.com/title/tt1322269/ + http://www.imdb.com/title/tt1322269/ + + + + Tue, 20 Aug 2013 01:17:02 GMT + A Thousand Clowns (1965) + http://www.imdb.com/title/tt0059798/ + http://www.imdb.com/title/tt0059798/ + + + + Fri, 16 Aug 2013 05:39:41 GMT + The Naked Gun 2½: The Smell of Fear (1991) + http://www.imdb.com/title/tt0102510/ + http://www.imdb.com/title/tt0102510/ + + + + Fri, 16 Aug 2013 02:11:27 GMT + Blazing Saddles (1974) + http://www.imdb.com/title/tt0071230/ + http://www.imdb.com/title/tt0071230/ + + + + Wed, 14 Aug 2013 23:11:34 GMT + Super High Me (2007 Documentary) + http://www.imdb.com/title/tt1111833/ + http://www.imdb.com/title/tt1111833/ + + + + Fri, 26 Jul 2013 06:26:43 GMT + I Am Love (2009) + http://www.imdb.com/title/tt1226236/ + http://www.imdb.com/title/tt1226236/ + + + + Fri, 26 Jul 2013 06:26:20 GMT + The Wind Rises (2013) + http://www.imdb.com/title/tt2013293/ + http://www.imdb.com/title/tt2013293/ + + + + Fri, 26 Jul 2013 06:25:56 GMT + Melancholia (2011) + http://www.imdb.com/title/tt1527186/ + http://www.imdb.com/title/tt1527186/ + + + + Fri, 26 Jul 2013 06:14:53 GMT + The Patience Stone (2012) + http://www.imdb.com/title/tt1638353/ + http://www.imdb.com/title/tt1638353/ + + + + Fri, 26 Jul 2013 06:12:55 GMT + The Hunger Games (2012) + http://www.imdb.com/title/tt1392170/ + http://www.imdb.com/title/tt1392170/ + + + + Fri, 26 Jul 2013 06:10:37 GMT + Salinger (2013 Documentary) + http://www.imdb.com/title/tt1596753/ + http://www.imdb.com/title/tt1596753/ + + + + Fri, 26 Jul 2013 06:09:51 GMT + 47 Ronin (2013) + http://www.imdb.com/title/tt1335975/ + http://www.imdb.com/title/tt1335975/ + + + + Fri, 26 Jul 2013 06:06:53 GMT + Kick-Ass 2 (2013) + http://www.imdb.com/title/tt1650554/ + http://www.imdb.com/title/tt1650554/ + + + + Fri, 26 Jul 2013 06:05:54 GMT + Blackfish (2013 Documentary) + http://www.imdb.com/title/tt2545118/ + http://www.imdb.com/title/tt2545118/ + + + + Fri, 26 Jul 2013 06:05:32 GMT + Cockneys vs Zombies (2012) + http://www.imdb.com/title/tt1362058/ + http://www.imdb.com/title/tt1362058/ + + + + Fri, 26 Jul 2013 06:05:11 GMT + Blue Exorcist: The Movie (2012) + http://www.imdb.com/title/tt3028018/ + http://www.imdb.com/title/tt3028018/ + + + + Fri, 26 Jul 2013 06:04:31 GMT + Computer Chess (2013) + http://www.imdb.com/title/tt2007360/ + http://www.imdb.com/title/tt2007360/ + + + + Fri, 26 Jul 2013 06:03:22 GMT + Girl Most Likely (2012) + http://www.imdb.com/title/tt1698648/ + http://www.imdb.com/title/tt1698648/ + + + + Fri, 26 Jul 2013 05:31:00 GMT + Frankenweenie (2012) + http://www.imdb.com/title/tt1142977/ + http://www.imdb.com/title/tt1142977/ + + + + Thu, 18 Jul 2013 07:41:08 GMT + Nowhere Boy (2009) + http://www.imdb.com/title/tt1266029/ + http://www.imdb.com/title/tt1266029/ + + + + Thu, 18 Jul 2013 07:40:41 GMT + Amistad (1997) + http://www.imdb.com/title/tt0118607/ + http://www.imdb.com/title/tt0118607/ + + + + Thu, 18 Jul 2013 07:40:19 GMT + Angus, Thongs and Perfect Snogging (2008) + http://www.imdb.com/title/tt0963743/ + http://www.imdb.com/title/tt0963743/ + + + + Thu, 18 Jul 2013 07:31:50 GMT + Year One (2009) + http://www.imdb.com/title/tt1045778/ + http://www.imdb.com/title/tt1045778/ + + + + Thu, 18 Jul 2013 07:31:23 GMT + RocknRolla (2008) + http://www.imdb.com/title/tt1032755/ + http://www.imdb.com/title/tt1032755/ + + + + Thu, 18 Jul 2013 07:31:07 GMT + World War Z (2013) + http://www.imdb.com/title/tt0816711/ + http://www.imdb.com/title/tt0816711/ + + + + Thu, 18 Jul 2013 07:30:27 GMT + Welcome to the Punch (2013) + http://www.imdb.com/title/tt1684233/ + http://www.imdb.com/title/tt1684233/ + + + + Thu, 18 Jul 2013 07:30:01 GMT + Ways to Live Forever (2010) + http://www.imdb.com/title/tt1446208/ + http://www.imdb.com/title/tt1446208/ + + + + Thu, 18 Jul 2013 07:29:43 GMT + The Rise (2012) + http://www.imdb.com/title/tt1981140/ + http://www.imdb.com/title/tt1981140/ + + + + Thu, 18 Jul 2013 07:29:19 GMT + Warm Bodies (2013) + http://www.imdb.com/title/tt1588173/ + http://www.imdb.com/title/tt1588173/ + + + + Thu, 18 Jul 2013 07:27:30 GMT + Violet & Daisy (2011) + http://www.imdb.com/title/tt1634136/ + http://www.imdb.com/title/tt1634136/ + + + + Thu, 18 Jul 2013 07:24:58 GMT + Tiger Eyes (2012) + http://www.imdb.com/title/tt1748260/ + http://www.imdb.com/title/tt1748260/ + + + + Thu, 18 Jul 2013 07:24:37 GMT + This Is the End (2013) + http://www.imdb.com/title/tt1245492/ + http://www.imdb.com/title/tt1245492/ + + + + Thu, 18 Jul 2013 07:24:19 GMT + The Wolf of Wall Street (2013) + http://www.imdb.com/title/tt0993846/ + http://www.imdb.com/title/tt0993846/ + + + + Thu, 18 Jul 2013 07:24:01 GMT + The Way Way Back (2013) + http://www.imdb.com/title/tt1727388/ + http://www.imdb.com/title/tt1727388/ + + + + Thu, 18 Jul 2013 07:20:15 GMT + The Time Being (2012) + http://www.imdb.com/title/tt1916749/ + http://www.imdb.com/title/tt1916749/ + + + + Thu, 18 Jul 2013 07:19:57 GMT + The Sweeney (2012) + http://www.imdb.com/title/tt0857190/ + http://www.imdb.com/title/tt0857190/ + + + + Thu, 18 Jul 2013 07:19:26 GMT + The Spectacular Now (2013) + http://www.imdb.com/title/tt1714206/ + http://www.imdb.com/title/tt1714206/ + + + + Thu, 18 Jul 2013 07:18:41 GMT + Thérèse (2012) + http://www.imdb.com/title/tt1654829/ + http://www.imdb.com/title/tt1654829/ + + + + Thu, 18 Jul 2013 07:18:17 GMT + The Mortal Instruments: City of Bones (2013) + http://www.imdb.com/title/tt1538403/ + http://www.imdb.com/title/tt1538403/ + + + + Thu, 18 Jul 2013 07:17:15 GMT + The Lifeguard (2013) + http://www.imdb.com/title/tt2265534/ + http://www.imdb.com/title/tt2265534/ + + + + Thu, 18 Jul 2013 07:16:58 GMT + The Lego Movie (2014) + http://www.imdb.com/title/tt1490017/ + http://www.imdb.com/title/tt1490017/ + + + + Thu, 18 Jul 2013 07:05:06 GMT + The Hobbit: The Battle of the Five Armies (2014) + http://www.imdb.com/title/tt2310332/ + http://www.imdb.com/title/tt2310332/ + + + + Thu, 18 Jul 2013 07:04:28 GMT + The Hobbit: The Desolation of Smaug (2013) + http://www.imdb.com/title/tt1170358/ + http://www.imdb.com/title/tt1170358/ + + + + Thu, 18 Jul 2013 07:02:54 GMT + Silver Linings Playbook (2012) + http://www.imdb.com/title/tt1045658/ + http://www.imdb.com/title/tt1045658/ + + + + Thu, 18 Jul 2013 07:02:22 GMT + The Heat (2013) + http://www.imdb.com/title/tt2404463/ + http://www.imdb.com/title/tt2404463/ + + + + Thu, 18 Jul 2013 06:59:40 GMT + The Frozen Ground (2013) + http://www.imdb.com/title/tt2005374/ + http://www.imdb.com/title/tt2005374/ + + + + Thu, 18 Jul 2013 06:59:19 GMT + The Fifth Estate (2013) + http://www.imdb.com/title/tt1837703/ + http://www.imdb.com/title/tt1837703/ + + + + Thu, 18 Jul 2013 06:58:18 GMT + The Counselor (2013) + http://www.imdb.com/title/tt2193215/ + http://www.imdb.com/title/tt2193215/ + + + + Thu, 18 Jul 2013 06:57:39 GMT + The Conjuring (2013) + http://www.imdb.com/title/tt1457767/ + http://www.imdb.com/title/tt1457767/ + + + + Thu, 18 Jul 2013 06:56:31 GMT + The Act of Killing (2012 Documentary) + http://www.imdb.com/title/tt2375605/ + http://www.imdb.com/title/tt2375605/ + + + + Thu, 18 Jul 2013 06:56:11 GMT + Thanks for Sharing (2012) + http://www.imdb.com/title/tt1932718/ + http://www.imdb.com/title/tt1932718/ + + + + Thu, 18 Jul 2013 06:55:46 GMT + Stuck in Love (2012) + http://www.imdb.com/title/tt2205697/ + http://www.imdb.com/title/tt2205697/ + + + + Thu, 18 Jul 2013 06:54:11 GMT + Some Girl(s) (2013) + http://www.imdb.com/title/tt2201221/ + http://www.imdb.com/title/tt2201221/ + + + + Thu, 18 Jul 2013 06:53:27 GMT + Snowpiercer (2013) + http://www.imdb.com/title/tt1706620/ + http://www.imdb.com/title/tt1706620/ + + + + Thu, 18 Jul 2013 06:51:58 GMT + Arbitrage (2012) + http://www.imdb.com/title/tt1764183/ + http://www.imdb.com/title/tt1764183/ + + + + Thu, 18 Jul 2013 06:39:19 GMT + Seventh Son (2014) + http://www.imdb.com/title/tt1121096/ + http://www.imdb.com/title/tt1121096/ + + + + Thu, 18 Jul 2013 06:38:57 GMT + Saving Mr. Banks (2013) + http://www.imdb.com/title/tt2140373/ + http://www.imdb.com/title/tt2140373/ + + + + Thu, 18 Jul 2013 06:38:14 GMT + Runner Runner (2013) + http://www.imdb.com/title/tt2364841/ + http://www.imdb.com/title/tt2364841/ + + + + Thu, 18 Jul 2013 06:37:47 GMT + Rigor Mortis (2013) + http://www.imdb.com/title/tt2771800/ + http://www.imdb.com/title/tt2771800/ + + + + Thu, 18 Jul 2013 06:37:24 GMT + Ride Along (2014) + http://www.imdb.com/title/tt1408253/ + http://www.imdb.com/title/tt1408253/ + + + + Thu, 18 Jul 2013 06:35:37 GMT + Rush (2013) + http://www.imdb.com/title/tt1979320/ + http://www.imdb.com/title/tt1979320/ + + + + Thu, 18 Jul 2013 06:35:07 GMT + Prisoners (2013) + http://www.imdb.com/title/tt1392214/ + http://www.imdb.com/title/tt1392214/ + + + + Thu, 18 Jul 2013 06:34:50 GMT + Prince Avalanche (2013) + http://www.imdb.com/title/tt2195548/ + http://www.imdb.com/title/tt2195548/ + + + + Thu, 18 Jul 2013 06:34:28 GMT + Populaire (2012) + http://www.imdb.com/title/tt2070776/ + http://www.imdb.com/title/tt2070776/ + + + + Thu, 18 Jul 2013 06:34:06 GMT + Pitch Perfect (2012) + http://www.imdb.com/title/tt1981677/ + http://www.imdb.com/title/tt1981677/ + + + + Thu, 18 Jul 2013 06:33:17 GMT + Percy Jackson: Sea of Monsters (2013) + http://www.imdb.com/title/tt1854564/ + http://www.imdb.com/title/tt1854564/ + + + + Thu, 18 Jul 2013 06:33:00 GMT + Percy Jackson & the Olympians: The Lightning Thief (2010) + http://www.imdb.com/title/tt0814255/ + http://www.imdb.com/title/tt0814255/ + + + + Thu, 18 Jul 2013 06:32:39 GMT + Pawn Shop Chronicles (2013) + http://www.imdb.com/title/tt1741243/ + http://www.imdb.com/title/tt1741243/ + + + + Thu, 18 Jul 2013 06:32:04 GMT + Pacific Rim (2013) + http://www.imdb.com/title/tt1663662/ + http://www.imdb.com/title/tt1663662/ + + + + Thu, 18 Jul 2013 06:31:41 GMT + Oz the Great and Powerful (2013) + http://www.imdb.com/title/tt1623205/ + http://www.imdb.com/title/tt1623205/ + + + + Thu, 18 Jul 2013 06:31:21 GMT + Out of the Furnace (2013) + http://www.imdb.com/title/tt1206543/ + http://www.imdb.com/title/tt1206543/ + + + + Thu, 18 Jul 2013 06:30:54 GMT + Anchorman: The Legend of Ron Burgundy (2004) + http://www.imdb.com/title/tt0357413/ + http://www.imdb.com/title/tt0357413/ + + + + Thu, 18 Jul 2013 06:29:59 GMT + Now You See Me (2013) + http://www.imdb.com/title/tt1670345/ + http://www.imdb.com/title/tt1670345/ + + + + Thu, 18 Jul 2013 06:29:33 GMT + No (2012) + http://www.imdb.com/title/tt2059255/ + http://www.imdb.com/title/tt2059255/ + + + + Thu, 18 Jul 2013 06:28:06 GMT + Monsters University (2013) + http://www.imdb.com/title/tt1453405/ + http://www.imdb.com/title/tt1453405/ + + + + Thu, 18 Jul 2013 06:26:52 GMT + Magic Magic (2013) + http://www.imdb.com/title/tt1929308/ + http://www.imdb.com/title/tt1929308/ + + + + Thu, 18 Jul 2013 06:25:38 GMT + Like Someone in Love (2012) + http://www.imdb.com/title/tt1843287/ + http://www.imdb.com/title/tt1843287/ + + + + Thu, 18 Jul 2013 06:24:48 GMT + Jug Face (2013) + http://www.imdb.com/title/tt2620736/ + http://www.imdb.com/title/tt2620736/ + + + + Thu, 18 Jul 2013 06:24:25 GMT + Inside Llewyn Davis (2013) + http://www.imdb.com/title/tt2042568/ + http://www.imdb.com/title/tt2042568/ + + + + Thu, 18 Jul 2013 06:23:39 GMT + I Give It a Year (2013) + http://www.imdb.com/title/tt2244901/ + http://www.imdb.com/title/tt2244901/ + + + + Thu, 18 Jul 2013 06:23:14 GMT + I Declare War (2012) + http://www.imdb.com/title/tt2133239/ + http://www.imdb.com/title/tt2133239/ + + + + Thu, 18 Jul 2013 06:22:51 GMT + How to Train Your Dragon 2 (2014) + http://www.imdb.com/title/tt1646971/ + http://www.imdb.com/title/tt1646971/ + + + + Thu, 18 Jul 2013 06:22:32 GMT + How to Make Money Selling Drugs (2012 Documentary) + http://www.imdb.com/title/tt1276962/ + http://www.imdb.com/title/tt1276962/ + + + + Thu, 18 Jul 2013 06:22:07 GMT + Hell Baby (2013) + http://www.imdb.com/title/tt2318527/ + http://www.imdb.com/title/tt2318527/ + + + + Thu, 18 Jul 2013 06:16:54 GMT + Hannah Arendt (2012) + http://www.imdb.com/title/tt1674773/ + http://www.imdb.com/title/tt1674773/ + + + + Thu, 18 Jul 2013 06:16:01 GMT + Gravity (2013) + http://www.imdb.com/title/tt1454468/ + http://www.imdb.com/title/tt1454468/ + + + + Thu, 18 Jul 2013 06:15:42 GMT + Getaway (2013) + http://www.imdb.com/title/tt2167202/ + http://www.imdb.com/title/tt2167202/ + + + + Thu, 18 Jul 2013 06:15:24 GMT + Generation Um... (2012) + http://www.imdb.com/title/tt1718158/ + http://www.imdb.com/title/tt1718158/ + + + + Thu, 18 Jul 2013 06:14:29 GMT + Fruitvale Station (2013) + http://www.imdb.com/title/tt2334649/ + http://www.imdb.com/title/tt2334649/ + + + + Thu, 18 Jul 2013 06:13:55 GMT + Free Birds (2013) + http://www.imdb.com/title/tt1621039/ + http://www.imdb.com/title/tt1621039/ + + + + Thu, 18 Jul 2013 06:13:32 GMT + Billy Elliot (2000) + http://www.imdb.com/title/tt0249462/ + http://www.imdb.com/title/tt0249462/ + + + + Thu, 18 Jul 2013 06:13:03 GMT + Filth (2013) + http://www.imdb.com/title/tt1450321/ + http://www.imdb.com/title/tt1450321/ + + + + Thu, 18 Jul 2013 06:12:44 GMT + Ferris Bueller's Day Off (1986) + http://www.imdb.com/title/tt0091042/ + http://www.imdb.com/title/tt0091042/ + + + + Thu, 18 Jul 2013 06:12:22 GMT + Fast & Furious 6 (2013) + http://www.imdb.com/title/tt1905041/ + http://www.imdb.com/title/tt1905041/ + + + + Thu, 18 Jul 2013 06:11:49 GMT + Extraction (2013) + http://www.imdb.com/title/tt2823574/ + http://www.imdb.com/title/tt2823574/ + + + + Thu, 18 Jul 2013 06:11:13 GMT + Europa Report (2013) + http://www.imdb.com/title/tt2051879/ + http://www.imdb.com/title/tt2051879/ + + + + Thu, 18 Jul 2013 06:10:52 GMT + Escape Plan (2013) + http://www.imdb.com/title/tt1211956/ + http://www.imdb.com/title/tt1211956/ + + + + Thu, 18 Jul 2013 06:10:30 GMT + Epic (2013) + http://www.imdb.com/title/tt0848537/ + http://www.imdb.com/title/tt0848537/ + + + + Thu, 18 Jul 2013 06:09:42 GMT + Elysium (2013) + http://www.imdb.com/title/tt1535108/ + http://www.imdb.com/title/tt1535108/ + + + + Thu, 18 Jul 2013 06:09:19 GMT + Drift (2013) + http://www.imdb.com/title/tt1714833/ + http://www.imdb.com/title/tt1714833/ + + + + Thu, 18 Jul 2013 06:08:49 GMT + Dragon (2011) + http://www.imdb.com/title/tt1718199/ + http://www.imdb.com/title/tt1718199/ + + + + Thu, 18 Jul 2013 06:08:46 GMT + Dragon (2011) + http://www.imdb.com/title/tt1718199/ + http://www.imdb.com/title/tt1718199/ + + + + Thu, 18 Jul 2013 06:07:34 GMT + Don Jon (2013) + http://www.imdb.com/title/tt2229499/ + http://www.imdb.com/title/tt2229499/ + + + + Thu, 18 Jul 2013 06:07:01 GMT + Despicable Me 2 (2013) + http://www.imdb.com/title/tt1690953/ + http://www.imdb.com/title/tt1690953/ + + + + Thu, 18 Jul 2013 05:55:51 GMT + All the Real Girls (2003) + http://www.imdb.com/title/tt0299458/ + http://www.imdb.com/title/tt0299458/ + + + + Thu, 18 Jul 2013 05:55:35 GMT + The Assassination of Jesse James by the Coward Robert Ford (2007) + http://www.imdb.com/title/tt0443680/ + http://www.imdb.com/title/tt0443680/ + + + + Thu, 18 Jul 2013 05:55:29 GMT + Lars and the Real Girl (2007) + http://www.imdb.com/title/tt0805564/ + http://www.imdb.com/title/tt0805564/ + + + + Thu, 18 Jul 2013 05:48:45 GMT + Cutie and the Boxer (2013 Documentary) + http://www.imdb.com/title/tt2355540/ + http://www.imdb.com/title/tt2355540/ + + + + Thu, 18 Jul 2013 05:48:23 GMT + Superbad (2007) + http://www.imdb.com/title/tt0829482/ + http://www.imdb.com/title/tt0829482/ + + + + Thu, 18 Jul 2013 05:48:03 GMT + Crystal Fairy & the Magical Cactus (2013) + http://www.imdb.com/title/tt2332579/ + http://www.imdb.com/title/tt2332579/ + + + + Thu, 18 Jul 2013 05:47:45 GMT + Cloudy with a Chance of Meatballs 2 (2013) + http://www.imdb.com/title/tt1985966/ + http://www.imdb.com/title/tt1985966/ + + + + Thu, 18 Jul 2013 05:47:26 GMT + Cloudy with a Chance of Meatballs (2009) + http://www.imdb.com/title/tt0844471/ + http://www.imdb.com/title/tt0844471/ + + + + Thu, 18 Jul 2013 05:47:03 GMT + Captain Phillips (2013) + http://www.imdb.com/title/tt1535109/ + http://www.imdb.com/title/tt1535109/ + + + + Thu, 18 Jul 2013 05:46:03 GMT + Byzantium (2012) + http://www.imdb.com/title/tt1531901/ + http://www.imdb.com/title/tt1531901/ + + + + Thu, 18 Jul 2013 05:45:36 GMT + Broken (2012) + http://www.imdb.com/title/tt1441940/ + http://www.imdb.com/title/tt1441940/ + + + + Thu, 18 Jul 2013 05:45:13 GMT + Blue Jasmine (2013) + http://www.imdb.com/title/tt2334873/ + http://www.imdb.com/title/tt2334873/ + + + + Thu, 18 Jul 2013 05:44:53 GMT + Before Midnight (2013) + http://www.imdb.com/title/tt2209418/ + http://www.imdb.com/title/tt2209418/ + + + + Thu, 18 Jul 2013 05:44:21 GMT + Dirty Pretty Things (2002) + http://www.imdb.com/title/tt0301199/ + http://www.imdb.com/title/tt0301199/ + + + + Thu, 18 Jul 2013 05:43:52 GMT + Inside Man (2006) + http://www.imdb.com/title/tt0454848/ + http://www.imdb.com/title/tt0454848/ + + + + Thu, 18 Jul 2013 05:43:40 GMT + About Time (2013) + http://www.imdb.com/title/tt2194499/ + http://www.imdb.com/title/tt2194499/ + + + + Thu, 18 Jul 2013 05:43:26 GMT + Adore (2013) + http://www.imdb.com/title/tt2103267/ + http://www.imdb.com/title/tt2103267/ + + + + Thu, 18 Jul 2013 05:43:07 GMT + After Earth (2013) + http://www.imdb.com/title/tt1815862/ + http://www.imdb.com/title/tt1815862/ + + + + Thu, 18 Jul 2013 05:42:45 GMT + The Kings of Summer (2013) + http://www.imdb.com/title/tt2179116/ + http://www.imdb.com/title/tt2179116/ + + + + Thu, 18 Jul 2013 05:42:37 GMT + Afternoon Delight (2013) + http://www.imdb.com/title/tt2312890/ + http://www.imdb.com/title/tt2312890/ + + + + Thu, 18 Jul 2013 05:42:29 GMT + Ain't Them Bodies Saints (2013) + http://www.imdb.com/title/tt2388637/ + http://www.imdb.com/title/tt2388637/ + + + + Thu, 18 Jul 2013 05:42:21 GMT + Alan Partridge (2013) + http://www.imdb.com/title/tt0469021/ + http://www.imdb.com/title/tt0469021/ + + + + Thu, 18 Jul 2013 05:42:12 GMT + And Now a Word from Our Sponsor (2013) + http://www.imdb.com/title/tt2094762/ + http://www.imdb.com/title/tt2094762/ + + + + diff --git a/src/NzbDrone.Core.Test/IndexerTests/BasicRssParserFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/BasicRssParserFixture.cs index b2819434d..802744c96 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/BasicRssParserFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/BasicRssParserFixture.cs @@ -45,6 +45,7 @@ namespace NzbDrone.Core.Test.IndexerTests return new IndexerResponse(new IndexerRequest(httpRequest), httpResponse); } + [Test] public void should_handle_relative_url() { diff --git a/src/NzbDrone.Core.Test/NetImport/RSSImportFixture.cs b/src/NzbDrone.Core.Test/NetImport/RSSImportFixture.cs new file mode 100644 index 000000000..9eb154b0f --- /dev/null +++ b/src/NzbDrone.Core.Test/NetImport/RSSImportFixture.cs @@ -0,0 +1,34 @@ +using System.Linq; +using System.Text; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.NetImport; +using NzbDrone.Core.NetImport.RSSImport; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.NetImport +{ + public class RSSImportTest : CoreTest + { + private NetImportResponse CreateResponse(string url, string content) + { + var httpRequest = new HttpRequest(url); + var httpResponse = new HttpResponse(httpRequest, new HttpHeader(), Encoding.UTF8.GetBytes(content)); + + return new NetImportResponse(new NetImportRequest(httpRequest), httpResponse); + } + + + [Test] + public void should_handle_relative_url() + { + var xml = ReadAllText("Files/imdb_watchlist.xml"); + + var result = Subject.ParseResponse(CreateResponse("http://my.indexer.com/api?q=My+Favourite+Show", xml)); + + result.First().Title.Should().Be("Think Like a Man Too"); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 70e548681..d65fed883 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -284,6 +284,7 @@ + @@ -409,6 +410,9 @@ sqlite3.dll Always + + Always + Always diff --git a/src/NzbDrone.Core/Datastore/Migration/119_create_netimport_table.cs b/src/NzbDrone.Core/Datastore/Migration/119_create_netimport_table.cs index 086464746..f324ee925 100644 --- a/src/NzbDrone.Core/Datastore/Migration/119_create_netimport_table.cs +++ b/src/NzbDrone.Core/Datastore/Migration/119_create_netimport_table.cs @@ -10,7 +10,6 @@ namespace NzbDrone.Core.Datastore.Migration { Create.TableForModel("NetImport") .WithColumn("Enabled").AsBoolean() - .WithColumn("ProfileId").AsInt32() .WithColumn("Name").AsString().Unique() .WithColumn("Implementation").AsString() .WithColumn("Settings").AsString().Nullable(); diff --git a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchList.cs b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchList.cs deleted file mode 100644 index ed5c06da4..000000000 --- a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchList.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Xml.Serialization; -using FluentValidation.Results; -using NLog; -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Indexers.PassThePopcorn; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.NetImport.IMDbWatchList -{ - public class IMDbWatchList : HttpNetImportBase - { - public override string Name => "IMDbWatchList"; - public override string Link => "http://rss.imdb.com/list/"; - public override int ProfileId => 1; - public override bool Enabled => true; - - public IMDbWatchList(IHttpClient httpClient, IConfigService configService, IParsingService parsingService, Logger logger) - : base(httpClient, configService, parsingService, logger) - { } - - public override INetImportRequestGenerator GetRequestGenerator() - { - return new IMDbWatchListRequestGenerator() { Settings = Settings }; - } - - public override IParseNetImportResponse GetParser() - { - return new IMDbWatchListParser(Settings); - } - } -} diff --git a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListParser.cs b/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListParser.cs deleted file mode 100644 index 20fb368ec..000000000 --- a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListParser.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Newtonsoft.Json; -using NzbDrone.Core.NetImport.Exceptions; -using NzbDrone.Core.Tv; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text; - -namespace NzbDrone.Core.NetImport.IMDbWatchList -{ - public class IMDbWatchListParser : IParseNetImportResponse - { - private readonly IMDbWatchListSettings _settings; - - public IMDbWatchListParser(IMDbWatchListSettings settings) - { - _settings = settings; - } - - public IList ParseResponse(NetImportResponse netImportResponse) - { - var torrentInfos = new List(); - - if (netImportResponse.HttpResponse.StatusCode != HttpStatusCode.OK) - { - throw new NetImportException(netImportResponse, - "Unexpected response status {0} code from API request", - netImportResponse.HttpResponse.StatusCode); - } - - var jsonResponse = JsonConvert.DeserializeObject(netImportResponse.Content); - - var responseData = jsonResponse.Movie; - if (responseData == null) - { - throw new NetImportException(netImportResponse, - "This list has no movies"); - } - - foreach (var result in responseData) - { - var title = Parser.Parser.ParseMovieTitle(result.Title, false); - - torrentInfos.Add(new Movie() - { - Title = title.MovieTitle, - Year = title.Year, - ProfileId = _settings.ProfileId, - ImdbId = Parser.Parser.ParseImdbId(result.Link) - }); - } - - return torrentInfos.ToArray(); - } - } -} diff --git a/src/NzbDrone.Core/NetImport/INetImport.cs b/src/NzbDrone.Core/NetImport/INetImport.cs index 82ec79629..e32eb2ed9 100644 --- a/src/NzbDrone.Core/NetImport/INetImport.cs +++ b/src/NzbDrone.Core/NetImport/INetImport.cs @@ -8,7 +8,6 @@ namespace NzbDrone.Core.NetImport { public interface INetImport : IProvider { - string Link { get; } bool Enabled { get; } IList Fetch(); diff --git a/src/NzbDrone.Core/NetImport/NetImportBase.cs b/src/NzbDrone.Core/NetImport/NetImportBase.cs index 0f3f6fc0f..165cd7375 100644 --- a/src/NzbDrone.Core/NetImport/NetImportBase.cs +++ b/src/NzbDrone.Core/NetImport/NetImportBase.cs @@ -21,8 +21,6 @@ namespace NzbDrone.Core.NetImport protected readonly Logger _logger; public abstract string Name { get; } - public abstract string Link { get; } - public abstract int ProfileId { get; } public abstract bool Enabled { get; } @@ -46,8 +44,6 @@ namespace NzbDrone.Core.NetImport yield return new NetImportDefinition { Name = GetType().Name, - Link = Link, - ProfileId = ProfileId, Enabled = config.Validate().IsValid && Enabled, Implementation = GetType().Name, Settings = config diff --git a/src/NzbDrone.Core/NetImport/NetImportDefinition.cs b/src/NzbDrone.Core/NetImport/NetImportDefinition.cs index b4af0c4a2..fb2313353 100644 --- a/src/NzbDrone.Core/NetImport/NetImportDefinition.cs +++ b/src/NzbDrone.Core/NetImport/NetImportDefinition.cs @@ -4,8 +4,6 @@ namespace NzbDrone.Core.NetImport { public class NetImportDefinition : ProviderDefinition { - public string Link { get; set; } - public int ProfileId { get; set; } public bool Enabled { get; set; } public override bool Enable => Enabled; } diff --git a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListAPI.cs b/src/NzbDrone.Core/NetImport/RSSImport/IMDbWatchListAPI.cs similarity index 96% rename from src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListAPI.cs rename to src/NzbDrone.Core/NetImport/RSSImport/IMDbWatchListAPI.cs index 73570c897..ccc618e9c 100644 --- a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListAPI.cs +++ b/src/NzbDrone.Core/NetImport/RSSImport/IMDbWatchListAPI.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace NzbDrone.Core.NetImport.IMDbWatchList +namespace NzbDrone.Core.NetImport.RSSImport { class IMDbWatchListAPI { diff --git a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListProxy.cs b/src/NzbDrone.Core/NetImport/RSSImport/IMDbWatchListProxy.cs similarity index 92% rename from src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListProxy.cs rename to src/NzbDrone.Core/NetImport/RSSImport/IMDbWatchListProxy.cs index 664919c52..0d46198f4 100644 --- a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListProxy.cs +++ b/src/NzbDrone.Core/NetImport/RSSImport/IMDbWatchListProxy.cs @@ -8,12 +8,12 @@ using NzbDrone.Core.Exceptions; using RestSharp; using NzbDrone.Core.Rest; -namespace NzbDrone.Core.NetImport.IMDbWatchList +namespace NzbDrone.Core.NetImport.RSSImport { public interface IIMDbWatchListProxy { void ImportMovies(string url); - ValidationFailure Test(IMDbWatchListSettings settings); + ValidationFailure Test(RSSImportSettings settings); } public class IMDbWatchListProxy : IIMDbWatchListProxy @@ -60,7 +60,7 @@ namespace NzbDrone.Core.NetImport.IMDbWatchList } } - public ValidationFailure Test(IMDbWatchListSettings settings) + public ValidationFailure Test(RSSImportSettings settings) { try { diff --git a/src/NzbDrone.Core/NetImport/RSSImport/RSSImport.cs b/src/NzbDrone.Core/NetImport/RSSImport/RSSImport.cs new file mode 100644 index 000000000..a945e4b77 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/RSSImport/RSSImport.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Xml.Serialization; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.PassThePopcorn; +using NzbDrone.Core.Parser; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.NetImport.RSSImport +{ + public class RSSImport : HttpNetImportBase + { + public override string Name => "RSSList"; + public override bool Enabled => true; + + public RSSImport(IHttpClient httpClient, IConfigService configService, IParsingService parsingService, Logger logger) + : base(httpClient, configService, parsingService, logger) + { } + + public new virtual IEnumerable DefaultDefinitions + { + get + { + var config = (RSSImportSettings)new RSSImportSettings(); + config.Link = "https://rss.imdb.com/list/YOURLISTID"; + + yield return new NetImportDefinition + { + Name = GetType().Name, + Enabled = config.Validate().IsValid && Enabled, + Implementation = GetType().Name, + Settings = config + }; + } + } + + public override INetImportRequestGenerator GetRequestGenerator() + { + return new RSSImportRequestGenerator() { Settings = Settings }; + } + + public override IParseNetImportResponse GetParser() + { + return new RSSImportParser(Settings); + } + } +} diff --git a/src/NzbDrone.Core/NetImport/RSSImport/RSSImportParser.cs b/src/NzbDrone.Core/NetImport/RSSImport/RSSImportParser.cs new file mode 100644 index 000000000..f3244beec --- /dev/null +++ b/src/NzbDrone.Core/NetImport/RSSImport/RSSImportParser.cs @@ -0,0 +1,236 @@ +using Newtonsoft.Json; +using NzbDrone.Core.NetImport.Exceptions; +using NzbDrone.Core.Tv; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; +using System.Xml.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.NetImport.RSSImport +{ + public class RSSImportParser : IParseNetImportResponse + { + private readonly RSSImportSettings _settings; + private NetImportResponse _importResponse; + private readonly Logger _logger; + + private static readonly Regex ReplaceEntities = new Regex("&[a-z]+;", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public RSSImportParser(RSSImportSettings settings) + { + _settings = settings; + } + + public virtual IList ParseResponse(NetImportResponse importResponse) + { + _importResponse = importResponse; + + var movies = new List(); + + if (!PreProcess(importResponse)) + { + return movies; + } + + var document = LoadXmlDocument(importResponse); + var items = GetItems(document); + + foreach (var item in items) + { + try + { + var reportInfo = ProcessItem(item); + + movies.AddIfNotNull(reportInfo); + } + catch (Exception itemEx) + { + //itemEx.Data.Add("Item", item.Title()); + _logger.Error(itemEx, "An error occurred while processing feed item from " + importResponse.Request.Url); + } + } + + return movies; + } + + protected virtual XDocument LoadXmlDocument(NetImportResponse indexerResponse) + { + try + { + var content = indexerResponse.Content; + content = ReplaceEntities.Replace(content, ReplaceEntity); + + using (var xmlTextReader = XmlReader.Create(new StringReader(content), new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore, IgnoreComments = true })) + { + return XDocument.Load(xmlTextReader); + } + } + catch (XmlException ex) + { + var contentSample = indexerResponse.Content.Substring(0, Math.Min(indexerResponse.Content.Length, 512)); + _logger.Debug("Truncated response content (originally {0} characters): {1}", indexerResponse.Content.Length, contentSample); + + ex.Data.Add("ContentLength", indexerResponse.Content.Length); + ex.Data.Add("ContentSample", contentSample); + + throw; + } + } + + protected virtual string ReplaceEntity(Match match) + { + try + { + var character = WebUtility.HtmlDecode(match.Value); + return string.Concat("&#", (int)character[0], ";"); + } + catch + { + return match.Value; + } + } + + protected virtual Movie CreateNewMovie() + { + return new Movie(); + } + + protected virtual bool PreProcess(NetImportResponse indexerResponse) + { + if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + { + throw new NetImportException(indexerResponse, "Indexer API call resulted in an unexpected StatusCode [{0}]", indexerResponse.HttpResponse.StatusCode); + } + + if (indexerResponse.HttpResponse.Headers.ContentType != null && indexerResponse.HttpResponse.Headers.ContentType.Contains("text/html") && + indexerResponse.HttpRequest.Headers.Accept != null && !indexerResponse.HttpRequest.Headers.Accept.Contains("text/html")) + { + throw new NetImportException(indexerResponse, "Indexer responded with html content. Site is likely blocked or unavailable."); + } + + return true; + } + + protected Movie ProcessItem(XElement item) + { + var releaseInfo = CreateNewMovie(); + + releaseInfo = ProcessItem(item, releaseInfo); + + //_logger.Trace("Parsed: {0}", releaseInfo.Title); + + return PostProcess(item, releaseInfo); + } + + protected virtual Movie ProcessItem(XElement item, Movie releaseInfo) + { + var result = Parser.Parser.ParseMovieTitle(GetTitle(item)); + + releaseInfo.Title = GetTitle(item); + + if (result != null) + { + releaseInfo.Title = result.MovieTitle; + releaseInfo.Year = result.Year; + releaseInfo.ImdbId = result.ImdbId; + } + + try + { + if (releaseInfo.ImdbId.IsNullOrWhiteSpace()) + { + releaseInfo.ImdbId = GetImdbId(item); + } + + } + catch (Exception) + { + _logger.Debug("Unable to extract Imdb Id :(."); + } + + return releaseInfo; + } + + protected virtual Movie PostProcess(XElement item, Movie releaseInfo) + { + return releaseInfo; + } + + protected virtual string GetTitle(XElement item) + { + return item.TryGetValue("title", "Unknown"); + } + + protected virtual DateTime GetPublishDate(XElement item) + { + var dateString = item.TryGetValue("pubDate"); + + if (dateString.IsNullOrWhiteSpace()) + { + throw new UnsupportedFeedException("Rss feed must have a pubDate element with a valid publish date."); + } + + return XElementExtensions.ParseDate(dateString); + } + + protected virtual string GetImdbId(XElement item) + { + var url = item.TryGetValue("link"); + if (url.IsNullOrWhiteSpace()) + { + return ""; + } + return Parser.Parser.ParseImdbId(url); + } + + protected IEnumerable GetItems(XDocument document) + { + var root = document.Root; + + if (root == null) + { + return Enumerable.Empty(); + } + + var channel = root.Element("channel"); + + if (channel == null) + { + return Enumerable.Empty(); + } + + return channel.Elements("item"); + } + + protected virtual string ParseUrl(string value) + { + if (value.IsNullOrWhiteSpace()) + { + return null; + } + + try + { + var url = _importResponse.HttpRequest.Url + new HttpUri(value); + + return url.FullUri; + } + catch (Exception ex) + { + _logger.Debug(ex, string.Format("Failed to parse Url {0}, ignoring.", value)); + return null; + } + } + } +} diff --git a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListRequestGenerator.cs b/src/NzbDrone.Core/NetImport/RSSImport/RSSImportRequestGenerator.cs similarity index 82% rename from src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListRequestGenerator.cs rename to src/NzbDrone.Core/NetImport/RSSImport/RSSImportRequestGenerator.cs index cfd92810b..a28068a07 100644 --- a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListRequestGenerator.cs +++ b/src/NzbDrone.Core/NetImport/RSSImport/RSSImportRequestGenerator.cs @@ -5,11 +5,11 @@ using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; using NzbDrone.Core.IndexerSearch.Definitions; -namespace NzbDrone.Core.NetImport.IMDbWatchList +namespace NzbDrone.Core.NetImport.RSSImport { - public class IMDbWatchListRequestGenerator : INetImportRequestGenerator + public class RSSImportRequestGenerator : INetImportRequestGenerator { - public IMDbWatchListSettings Settings { get; set; } + public RSSImportSettings Settings { get; set; } public virtual NetImportPageableRequestChain GetMovies() { diff --git a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListSettings.cs b/src/NzbDrone.Core/NetImport/RSSImport/RSSImportSettings.cs similarity index 68% rename from src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListSettings.cs rename to src/NzbDrone.Core/NetImport/RSSImport/RSSImportSettings.cs index 6d10fa78c..f4fb783c7 100644 --- a/src/NzbDrone.Core/NetImport/IMDbWatchList/IMDbWatchListSettings.cs +++ b/src/NzbDrone.Core/NetImport/RSSImport/RSSImportSettings.cs @@ -4,16 +4,16 @@ using NzbDrone.Core.Profiles; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; -namespace NzbDrone.Core.NetImport.IMDbWatchList +namespace NzbDrone.Core.NetImport.RSSImport { - public class IMDbWatchListSettings : NetImportBaseSettings + public class RSSImportSettings : NetImportBaseSettings { //private const string helpLink = "https://imdb.com"; - public IMDbWatchListSettings() + public RSSImportSettings() { - Link = "http://rss.imdb.com/list/"; + Link = "http://rss.yoursite.com"; ProfileId = 1; } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 06402f783..a2607ca25 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -130,18 +130,18 @@ - - - + + + - - - + + + From 5aaba98c5785591dcb69666cc7ce075b9a7a5e3f Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Sat, 21 Jan 2017 20:37:08 +0100 Subject: [PATCH 09/83] Imdbid parsing works now from url --- src/NzbDrone.Core.Test/NetImport/RSSImportFixture.cs | 1 + src/NzbDrone.Core/NetImport/RSSImport/RSSImport.cs | 2 +- src/NzbDrone.Core/Parser/Parser.cs | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core.Test/NetImport/RSSImportFixture.cs b/src/NzbDrone.Core.Test/NetImport/RSSImportFixture.cs index 9eb154b0f..f724a1b8d 100644 --- a/src/NzbDrone.Core.Test/NetImport/RSSImportFixture.cs +++ b/src/NzbDrone.Core.Test/NetImport/RSSImportFixture.cs @@ -29,6 +29,7 @@ namespace NzbDrone.Core.Test.NetImport var result = Subject.ParseResponse(CreateResponse("http://my.indexer.com/api?q=My+Favourite+Show", xml)); result.First().Title.Should().Be("Think Like a Man Too"); + result.First().ImdbId.Should().Be("tt2239832"); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/NetImport/RSSImport/RSSImport.cs b/src/NzbDrone.Core/NetImport/RSSImport/RSSImport.cs index a945e4b77..28bb1ddee 100644 --- a/src/NzbDrone.Core/NetImport/RSSImport/RSSImport.cs +++ b/src/NzbDrone.Core/NetImport/RSSImport/RSSImport.cs @@ -31,7 +31,7 @@ namespace NzbDrone.Core.NetImport.RSSImport yield return new NetImportDefinition { - Name = GetType().Name, + Name = "IMDb Watchlist", Enabled = config.Validate().IsValid && Enabled, Implementation = GetType().Name, Settings = config diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index d133f4e86..80d0f6407 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -266,7 +266,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex FileExtensionRegex = new Regex(@"\.[a-z0-9]{2,4}$", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly Regex ReportImdbId = new Regex(@"(?tt\d{9})", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex ReportImdbId = new Regex(@"(?tt\d{7})", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex SimpleTitleRegex = new Regex(@"(?:480[ip]|576[ip]|720[ip]|1080[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*:|]|848x480|1280x720|1920x1080|(8|10)b(it)?)\s*", RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -454,7 +454,7 @@ namespace NzbDrone.Core.Parser { if (match.Groups["imdbid"].Value != null) { - if (match.Groups["imdbid"].Length == 11) + if (match.Groups["imdbid"].Length == 9) { return match.Groups["imdbid"].Value; } From 4f37a36619600ebffaa5aedd8a1458e0a2aaf76e Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Sat, 21 Jan 2017 20:57:29 +0100 Subject: [PATCH 10/83] Updated HttpNetImporterBase. Still needs work to correctly handle failures. --- .../NetImport/RSSImportFixture.cs | 36 ++-- .../NetImport/RSSImportParserFixture.cs | 36 ++++ .../NzbDrone.Core.Test.csproj | 1 + .../NetImport/HttpNetImportBase.cs | 188 +++++++++++++++++- 4 files changed, 247 insertions(+), 14 deletions(-) create mode 100644 src/NzbDrone.Core.Test/NetImport/RSSImportParserFixture.cs diff --git a/src/NzbDrone.Core.Test/NetImport/RSSImportFixture.cs b/src/NzbDrone.Core.Test/NetImport/RSSImportFixture.cs index f724a1b8d..de13c40bf 100644 --- a/src/NzbDrone.Core.Test/NetImport/RSSImportFixture.cs +++ b/src/NzbDrone.Core.Test/NetImport/RSSImportFixture.cs @@ -1,32 +1,42 @@ -using System.Linq; -using System.Text; +using System; +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; using FluentAssertions; +using Moq; using NUnit.Framework; using NzbDrone.Common.Http; -using NzbDrone.Core.Indexers; +using NzbDrone.Core.Datastore; using NzbDrone.Core.NetImport; using NzbDrone.Core.NetImport.RSSImport; using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.NetImport { - public class RSSImportTest : CoreTest + [TestFixture] + public class RSSImportFixture : CoreTest { - private NetImportResponse CreateResponse(string url, string content) - { - var httpRequest = new HttpRequest(url); - var httpResponse = new HttpResponse(httpRequest, new HttpHeader(), Encoding.UTF8.GetBytes(content)); - return new NetImportResponse(new NetImportRequest(httpRequest), httpResponse); + [SetUp] + public void Setup() + { + Subject.Definition = Subject.DefaultDefinitions.First(); + } + private void GivenRecentFeedResponse(string rssXmlFile) + { + var recentFeed = ReadAllText(@"Files/" + rssXmlFile); + + Mocker.GetMock() + .Setup(o => o.Execute(It.IsAny())) + .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); } - [Test] - public void should_handle_relative_url() + public void should_fetch_imdb_list() { - var xml = ReadAllText("Files/imdb_watchlist.xml"); + GivenRecentFeedResponse("imdb_watchlist.xml"); - var result = Subject.ParseResponse(CreateResponse("http://my.indexer.com/api?q=My+Favourite+Show", xml)); + var result = Subject.Fetch(); result.First().Title.Should().Be("Think Like a Man Too"); result.First().ImdbId.Should().Be("tt2239832"); diff --git a/src/NzbDrone.Core.Test/NetImport/RSSImportParserFixture.cs b/src/NzbDrone.Core.Test/NetImport/RSSImportParserFixture.cs new file mode 100644 index 000000000..cde97c653 --- /dev/null +++ b/src/NzbDrone.Core.Test/NetImport/RSSImportParserFixture.cs @@ -0,0 +1,36 @@ +using System.Linq; +using System.Text; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.NetImport; +using NzbDrone.Core.NetImport.RSSImport; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.NetImport +{ + public class RSSImportTest : CoreTest + { + private NetImportResponse CreateResponse(string url, string content) + { + var httpRequest = new HttpRequest(url); + var httpResponse = new HttpResponse(httpRequest, new HttpHeader(), Encoding.UTF8.GetBytes(content)); + + return new NetImportResponse(new NetImportRequest(httpRequest), httpResponse); + } + + + [Test] + public void should_parse_xml_of_imdb() + { + var xml = ReadAllText("Files/imdb_watchlist.xml"); + + var result = Subject.ParseResponse(CreateResponse("http://my.indexer.com/api?q=My+Favourite+Show", xml)); + + result.First().Title.Should().Be("Think Like a Man Too"); + result.First().ImdbId.Should().Be("tt2239832"); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index d65fed883..a563c9f31 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -285,6 +285,7 @@ + diff --git a/src/NzbDrone.Core/NetImport/HttpNetImportBase.cs b/src/NzbDrone.Core/NetImport/HttpNetImportBase.cs index 9d5582a9c..5b82fca5f 100644 --- a/src/NzbDrone.Core/NetImport/HttpNetImportBase.cs +++ b/src/NzbDrone.Core/NetImport/HttpNetImportBase.cs @@ -26,6 +26,10 @@ namespace NzbDrone.Core.NetImport public override bool Enabled => true; + public bool SupportsPaging => PageSize > 0; + + public virtual int PageSize => 0; + public virtual TimeSpan RateLimit => TimeSpan.FromSeconds(2); public abstract INetImportRequestGenerator GetRequestGenerator(); @@ -39,7 +43,189 @@ namespace NzbDrone.Core.NetImport public override IList Fetch() { - return new List(); + var generator = GetRequestGenerator(); + + return FetchMovies(generator.GetMovies()); + } + + protected virtual IList FetchMovies(NetImportPageableRequestChain pageableRequestChain, bool isRecent = false) + { + var movies = new List(); + var url = string.Empty; + + var parser = GetParser(); + + try + { + var fullyUpdated = false; + Movie lastMovie = null; + if (isRecent) + { + //lastReleaseInfo = _indexerStatusService.GetLastRssSyncReleaseInfo(Definition.Id); + } + + for (int i = 0; i < pageableRequestChain.Tiers; i++) + { + var pageableRequests = pageableRequestChain.GetTier(i); + + foreach (var pageableRequest in pageableRequests) + { + var pagedReleases = new List(); + + foreach (var request in pageableRequest) + { + url = request.Url.FullUri; + + var page = FetchPage(request, parser); + + pagedReleases.AddRange(page); + + if (isRecent && page.Any()) + { + if (lastMovie == null) + { + fullyUpdated = true; + break; + }/* + var oldestReleaseDate = page.Select(v => v.PublishDate).Min(); + if (oldestReleaseDate < lastReleaseInfo.PublishDate || page.Any(v => v.DownloadUrl == lastReleaseInfo.DownloadUrl)) + { + fullyUpdated = true; + break; + } + + if (pagedReleases.Count >= MaxNumResultsPerQuery && + oldestReleaseDate < DateTime.UtcNow - TimeSpan.FromHours(24)) + { + fullyUpdated = false; + break; + }*///update later + } + else if (pagedReleases.Count >= MaxNumResultsPerQuery) + { + break; + } + + if (!IsFullPage(page)) + { + break; + } + } + + movies.AddRange(pagedReleases); + } + + if (movies.Any()) + { + break; + } + } + + if (isRecent && !movies.Empty()) + { + var ordered = movies.OrderByDescending(v => v.Title).ToList(); + + lastMovie = ordered.First(); + //_indexerStatusService.UpdateRssSyncStatus(Definition.Id, lastReleaseInfo); + } + + //_indexerStatusService.RecordSuccess(Definition.Id); + } + catch (WebException webException) + { + if (webException.Status == WebExceptionStatus.NameResolutionFailure || + webException.Status == WebExceptionStatus.ConnectFailure) + { + //_indexerStatusService.RecordConnectionFailure(Definition.Id); + } + else + { + //_indexerStatusService.RecordFailure(Definition.Id); + } + + if (webException.Message.Contains("502") || webException.Message.Contains("503") || + webException.Message.Contains("timed out")) + { + _logger.Warn("{0} server is currently unavailable. {1} {2}", this, url, webException.Message); + } + else + { + _logger.Warn("{0} {1} {2}", this, url, webException.Message); + } + } + catch (HttpException httpException) + { + if ((int)httpException.Response.StatusCode == 429) + { + //_indexerStatusService.RecordFailure(Definition.Id, TimeSpan.FromHours(1)); + _logger.Warn("API Request Limit reached for {0}", this); + } + else + { + //_indexerStatusService.RecordFailure(Definition.Id); + _logger.Warn("{0} {1}", this, httpException.Message); + } + } + catch (RequestLimitReachedException) + { + //_indexerStatusService.RecordFailure(Definition.Id, TimeSpan.FromHours(1)); + _logger.Warn("API Request Limit reached for {0}", this); + } + catch (ApiKeyException) + { + //_indexerStatusService.RecordFailure(Definition.Id); + _logger.Warn("Invalid API Key for {0} {1}", this, url); + } + catch (CloudFlareCaptchaException ex) + { + //_indexerStatusService.RecordFailure(Definition.Id); + if (ex.IsExpired) + { + _logger.Error(ex, "Expired CAPTCHA token for {0}, please refresh in indexer settings.", this); + } + else + { + _logger.Error(ex, "CAPTCHA token required for {0}, check indexer settings.", this); + } + } + catch (IndexerException ex) + { + //_indexerStatusService.RecordFailure(Definition.Id); + var message = string.Format("{0} - {1}", ex.Message, url); + _logger.Warn(ex, message); + } + catch (Exception feedEx) + { + //_indexerStatusService.RecordFailure(Definition.Id); + feedEx.Data.Add("FeedUrl", url); + _logger.Error(feedEx, "An error occurred while processing feed. " + url); + } + + return movies; + } + + protected virtual bool IsFullPage(IList page) + { + return PageSize != 0 && page.Count >= PageSize; + } + + protected virtual IList FetchPage(NetImportRequest request, IParseNetImportResponse parser) + { + var response = FetchIndexerResponse(request); + + return parser.ParseResponse(response).ToList(); + } + + protected virtual NetImportResponse FetchIndexerResponse(NetImportRequest request) + { + _logger.Debug("Downloading List " + request.HttpRequest.ToString(false)); + + if (request.HttpRequest.RateLimit < RateLimit) + { + request.HttpRequest.RateLimit = RateLimit; + } + + return new NetImportResponse(request, _httpClient.Execute(request.HttpRequest)); } protected override void Test(List failures) From 2c52795822277d3dbfa261649fd7c25fc48a9836 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Sat, 21 Jan 2017 21:09:02 +0100 Subject: [PATCH 11/83] Add base for netimport api. Still nothing on the UI side. --- src/NzbDrone.Api/NetImport/NetImportModule.cs | 32 +++++++++ .../NetImport/NetImportResource.cs | 10 +++ .../NetImport/NetImportFactory.cs | 70 +++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 src/NzbDrone.Api/NetImport/NetImportModule.cs create mode 100644 src/NzbDrone.Api/NetImport/NetImportResource.cs create mode 100644 src/NzbDrone.Core/NetImport/NetImportFactory.cs diff --git a/src/NzbDrone.Api/NetImport/NetImportModule.cs b/src/NzbDrone.Api/NetImport/NetImportModule.cs new file mode 100644 index 000000000..b128b061d --- /dev/null +++ b/src/NzbDrone.Api/NetImport/NetImportModule.cs @@ -0,0 +1,32 @@ +using NzbDrone.Core.NetImport; + +namespace NzbDrone.Api.NetImport +{ + public class NetImportModule : ProviderModuleBase + { + public NetImportModule(NetImportFactory indexerFactory) + : base(indexerFactory, "indexer") + { + } + + protected override void MapToResource(NetImportResource resource, NetImportDefinition definition) + { + base.MapToResource(resource, definition); + + resource.Enabled = definition.Enabled; + } + + protected override void MapToModel(NetImportDefinition definition, NetImportResource resource) + { + base.MapToModel(definition, resource); + + resource.Enabled = definition.Enabled; + } + + protected override void Validate(NetImportDefinition definition, bool includeWarnings) + { + if (!definition.Enable) return; + base.Validate(definition, includeWarnings); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/NetImport/NetImportResource.cs b/src/NzbDrone.Api/NetImport/NetImportResource.cs new file mode 100644 index 000000000..880b95ef0 --- /dev/null +++ b/src/NzbDrone.Api/NetImport/NetImportResource.cs @@ -0,0 +1,10 @@ +using NzbDrone.Core.NetImport; + +namespace NzbDrone.Api.NetImport +{ + public class NetImportResource : ProviderResource + { + public bool Enabled { get; set; } + public bool EnableSearch { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/NetImport/NetImportFactory.cs b/src/NzbDrone.Core/NetImport/NetImportFactory.cs new file mode 100644 index 000000000..489d8682e --- /dev/null +++ b/src/NzbDrone.Core/NetImport/NetImportFactory.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common.Composition; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.NetImport +{ + public interface INetImportFactory : IProviderFactory + { + List Enabled(); + } + + public class NetImportFactory : ProviderFactory, INetImportFactory + { + //private readonly IIndexerStatusService _indexerStatusService; + private readonly INetImportRepository _providerRepository; + private readonly Logger _logger; + + public NetImportFactory(//IIndexerStatusService indexerStatusService, + INetImportRepository providerRepository, + IEnumerable providers, + IContainer container, + IEventAggregator eventAggregator, + Logger logger) + : base(providerRepository, providers, container, eventAggregator, logger) + { + //_indexerStatusService = indexerStatusService; + _providerRepository = providerRepository; + _logger = logger; + } + + protected override List Active() + { + return base.Active().Where(c => c.Enabled).ToList(); + } + + public override void SetProviderCharacteristics(INetImport provider, NetImportDefinition definition) + { + base.SetProviderCharacteristics(provider, definition); + } + + public List Enabled() + { + var enabledIndexers = GetAvailableProviders().Where(n => ((NetImportDefinition)n.Definition).Enabled); + + var indexers = FilterBlockedIndexers(enabledIndexers); + + return indexers.ToList(); + } + + private IEnumerable FilterBlockedIndexers(IEnumerable indexers) + { + //var blockedIndexers = _indexerStatusService.GetBlockedIndexers().ToDictionary(v => v.IndexerId, v => v); + + foreach (var indexer in indexers) + { + /*IndexerStatus blockedIndexerStatus; + if (blockedIndexers.TryGetValue(indexer.Definition.Id, out blockedIndexerStatus)) + { + _logger.Debug("Temporarily ignoring indexer {0} till {1} due to recent failures.", indexer.Definition.Name, blockedIndexerStatus.DisabledTill.Value.ToLocalTime()); + continue; + }*/ + + yield return indexer; + } + } + } +} \ No newline at end of file From 463d85e4798ff169fdd55344f0331aed40fb7c2b Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Sat, 21 Jan 2017 21:28:14 +0100 Subject: [PATCH 12/83] Basis of UI Update. --- .../Config/NetImportConfigModule.cs | 20 + .../Config/NetImportConfigResource.cs | 19 + src/NzbDrone.Api/NetImport/NetImportModule.cs | 2 +- src/NzbDrone.Api/NzbDrone.Api.csproj | 4 + src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + .../NetImport/Add/IndexerAddCollectionView.js | 9 + .../Add/IndexerAddCollectionViewTemplate.hbs | 18 + .../NetImport/Add/IndexerAddItemView.js | 52 +++ .../Add/IndexerAddItemViewTemplate.hbs | 30 ++ .../NetImport/Add/IndexerSchemaModal.js | 39 ++ .../NetImport/Delete/IndexerDeleteView.js | 19 + .../Delete/IndexerDeleteViewTemplate.hbs | 13 + .../NetImport/Edit/IndexerEditView.js | 122 ++++++ .../Edit/IndexerEditViewTemplate.hbs | 92 ++++ .../NetImport/IndexerCollectionView.js | 25 ++ .../IndexerCollectionViewTemplate.hbs | 16 + src/UI/Settings/NetImport/IndexerItemView.js | 24 + .../NetImport/IndexerItemViewTemplate.hbs | 27 ++ src/UI/Settings/NetImport/IndexerLayout.js | 30 ++ .../NetImport/IndexerLayoutTemplate.hbs | 5 + .../Settings/NetImport/NetImportCollection.js | 13 + src/UI/Settings/NetImport/NetImportModel.js | 3 + .../NetImport/NetImportSettingsModel.js | 7 + .../NetImport/Options/IndexerOptionsView.js | 12 + .../Options/IndexerOptionsViewTemplate.hbs | 40 ++ .../Restriction/RestrictionCollection.js | 7 + .../Restriction/RestrictionCollectionView.js | 26 ++ .../RestrictionCollectionViewTemplate.hbs | 24 + .../Restriction/RestrictionDeleteView.js | 19 + .../RestrictionDeleteViewTemplate.hbs | 13 + .../Restriction/RestrictionEditView.js | 55 +++ .../RestrictionEditViewTemplate.hbs | 60 +++ .../Restriction/RestrictionItemView.js | 28 ++ .../RestrictionItemViewTemplate.hbs | 12 + .../NetImport/Restriction/RestrictionModel.js | 4 + src/UI/Settings/NetImport/indexers.less | 33 ++ src/UI/Settings/SettingsLayout.js | 409 +++++++++--------- src/UI/Settings/SettingsLayoutTemplate.hbs | 82 ++-- 38 files changed, 1177 insertions(+), 237 deletions(-) create mode 100644 src/NzbDrone.Api/Config/NetImportConfigModule.cs create mode 100644 src/NzbDrone.Api/Config/NetImportConfigResource.cs create mode 100644 src/UI/Settings/NetImport/Add/IndexerAddCollectionView.js create mode 100644 src/UI/Settings/NetImport/Add/IndexerAddCollectionViewTemplate.hbs create mode 100644 src/UI/Settings/NetImport/Add/IndexerAddItemView.js create mode 100644 src/UI/Settings/NetImport/Add/IndexerAddItemViewTemplate.hbs create mode 100644 src/UI/Settings/NetImport/Add/IndexerSchemaModal.js create mode 100644 src/UI/Settings/NetImport/Delete/IndexerDeleteView.js create mode 100644 src/UI/Settings/NetImport/Delete/IndexerDeleteViewTemplate.hbs create mode 100644 src/UI/Settings/NetImport/Edit/IndexerEditView.js create mode 100644 src/UI/Settings/NetImport/Edit/IndexerEditViewTemplate.hbs create mode 100644 src/UI/Settings/NetImport/IndexerCollectionView.js create mode 100644 src/UI/Settings/NetImport/IndexerCollectionViewTemplate.hbs create mode 100644 src/UI/Settings/NetImport/IndexerItemView.js create mode 100644 src/UI/Settings/NetImport/IndexerItemViewTemplate.hbs create mode 100644 src/UI/Settings/NetImport/IndexerLayout.js create mode 100644 src/UI/Settings/NetImport/IndexerLayoutTemplate.hbs create mode 100644 src/UI/Settings/NetImport/NetImportCollection.js create mode 100644 src/UI/Settings/NetImport/NetImportModel.js create mode 100644 src/UI/Settings/NetImport/NetImportSettingsModel.js create mode 100644 src/UI/Settings/NetImport/Options/IndexerOptionsView.js create mode 100644 src/UI/Settings/NetImport/Options/IndexerOptionsViewTemplate.hbs create mode 100644 src/UI/Settings/NetImport/Restriction/RestrictionCollection.js create mode 100644 src/UI/Settings/NetImport/Restriction/RestrictionCollectionView.js create mode 100644 src/UI/Settings/NetImport/Restriction/RestrictionCollectionViewTemplate.hbs create mode 100644 src/UI/Settings/NetImport/Restriction/RestrictionDeleteView.js create mode 100644 src/UI/Settings/NetImport/Restriction/RestrictionDeleteViewTemplate.hbs create mode 100644 src/UI/Settings/NetImport/Restriction/RestrictionEditView.js create mode 100644 src/UI/Settings/NetImport/Restriction/RestrictionEditViewTemplate.hbs create mode 100644 src/UI/Settings/NetImport/Restriction/RestrictionItemView.js create mode 100644 src/UI/Settings/NetImport/Restriction/RestrictionItemViewTemplate.hbs create mode 100644 src/UI/Settings/NetImport/Restriction/RestrictionModel.js create mode 100644 src/UI/Settings/NetImport/indexers.less diff --git a/src/NzbDrone.Api/Config/NetImportConfigModule.cs b/src/NzbDrone.Api/Config/NetImportConfigModule.cs new file mode 100644 index 000000000..3cd194116 --- /dev/null +++ b/src/NzbDrone.Api/Config/NetImportConfigModule.cs @@ -0,0 +1,20 @@ +using FluentValidation; +using NzbDrone.Api.Validation; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Api.Config +{ + public class NetImportConfigModule : NzbDroneConfigModule + { + + public NetImportConfigModule(IConfigService configService) + : base(configService) + { + } + + protected override NetImportConfigResource ToResource(IConfigService model) + { + return NetImportConfigResourceMapper.ToResource(model); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Config/NetImportConfigResource.cs b/src/NzbDrone.Api/Config/NetImportConfigResource.cs new file mode 100644 index 000000000..7b32aefe3 --- /dev/null +++ b/src/NzbDrone.Api/Config/NetImportConfigResource.cs @@ -0,0 +1,19 @@ +using NzbDrone.Api.REST; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Api.Config +{ + public class NetImportConfigResource : RestResource + { + } + + public static class NetImportConfigResourceMapper + { + public static NetImportConfigResource ToResource(IConfigService model) + { + return new NetImportConfigResource + { + }; + } + } +} diff --git a/src/NzbDrone.Api/NetImport/NetImportModule.cs b/src/NzbDrone.Api/NetImport/NetImportModule.cs index b128b061d..6130191d7 100644 --- a/src/NzbDrone.Api/NetImport/NetImportModule.cs +++ b/src/NzbDrone.Api/NetImport/NetImportModule.cs @@ -5,7 +5,7 @@ namespace NzbDrone.Api.NetImport public class NetImportModule : ProviderModuleBase { public NetImportModule(NetImportFactory indexerFactory) - : base(indexerFactory, "indexer") + : base(indexerFactory, "netimport") { } diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 6a61d84fc..47a433a54 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -109,6 +109,8 @@ + + @@ -121,6 +123,8 @@ + + diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index a2607ca25..c8dd24853 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -125,6 +125,7 @@ + diff --git a/src/UI/Settings/NetImport/Add/IndexerAddCollectionView.js b/src/UI/Settings/NetImport/Add/IndexerAddCollectionView.js new file mode 100644 index 000000000..5a4102cf2 --- /dev/null +++ b/src/UI/Settings/NetImport/Add/IndexerAddCollectionView.js @@ -0,0 +1,9 @@ +var ThingyAddCollectionView = require('../../ThingyAddCollectionView'); +var ThingyHeaderGroupView = require('../../ThingyHeaderGroupView'); +var AddItemView = require('./IndexerAddItemView'); + +module.exports = ThingyAddCollectionView.extend({ + itemView : ThingyHeaderGroupView.extend({ itemView : AddItemView }), + itemViewContainer : '.add-indexer .items', + template : 'Settings/Indexers/Add/IndexerAddCollectionViewTemplate' +}); \ No newline at end of file diff --git a/src/UI/Settings/NetImport/Add/IndexerAddCollectionViewTemplate.hbs b/src/UI/Settings/NetImport/Add/IndexerAddCollectionViewTemplate.hbs new file mode 100644 index 000000000..3d581b5e4 --- /dev/null +++ b/src/UI/Settings/NetImport/Add/IndexerAddCollectionViewTemplate.hbs @@ -0,0 +1,18 @@ + diff --git a/src/UI/Settings/NetImport/Add/IndexerAddItemView.js b/src/UI/Settings/NetImport/Add/IndexerAddItemView.js new file mode 100644 index 000000000..3a8b0493a --- /dev/null +++ b/src/UI/Settings/NetImport/Add/IndexerAddItemView.js @@ -0,0 +1,52 @@ +var _ = require('underscore'); +var $ = require('jquery'); +var AppLayout = require('../../../AppLayout'); +var Marionette = require('marionette'); +var EditView = require('../Edit/IndexerEditView'); + +module.exports = Marionette.ItemView.extend({ + template : 'Settings/Indexers/Add/IndexerAddItemViewTemplate', + tagName : 'li', + className : 'add-thingy-item', + + events : { + 'click .x-preset' : '_addPreset', + 'click' : '_add' + }, + + initialize : function(options) { + this.targetCollection = options.targetCollection; + }, + + _addPreset : function(e) { + var presetName = $(e.target).closest('.x-preset').attr('data-id'); + var presetData = _.where(this.model.get('presets'), { name : presetName })[0]; + + this.model.set(presetData); + + this._openEdit(); + }, + + _add : function(e) { + if ($(e.target).closest('.btn,.btn-group').length !== 0 && $(e.target).closest('.x-custom').length === 0) { + return; + } + + this._openEdit(); + }, + + _openEdit : function() { + this.model.set({ + id : undefined, + enableRss : this.model.get('supportsRss'), + enableSearch : this.model.get('supportsSearch') + }); + + var editView = new EditView({ + model : this.model, + targetCollection : this.targetCollection + }); + + AppLayout.modalRegion.show(editView); + } +}); \ No newline at end of file diff --git a/src/UI/Settings/NetImport/Add/IndexerAddItemViewTemplate.hbs b/src/UI/Settings/NetImport/Add/IndexerAddItemViewTemplate.hbs new file mode 100644 index 000000000..40bcb4391 --- /dev/null +++ b/src/UI/Settings/NetImport/Add/IndexerAddItemViewTemplate.hbs @@ -0,0 +1,30 @@ +
+
+ {{implementationName}} +
+
+ {{#if_gt presets.length compare=0}} + +
+ + +
+ {{/if_gt}} + {{#if infoLink}} + + + + {{/if}} +
+
\ No newline at end of file diff --git a/src/UI/Settings/NetImport/Add/IndexerSchemaModal.js b/src/UI/Settings/NetImport/Add/IndexerSchemaModal.js new file mode 100644 index 000000000..52b430e89 --- /dev/null +++ b/src/UI/Settings/NetImport/Add/IndexerSchemaModal.js @@ -0,0 +1,39 @@ +var _ = require('underscore'); +var AppLayout = require('../../../AppLayout'); +var Backbone = require('backbone'); +var SchemaCollection = require('../IndexerCollection'); +var AddCollectionView = require('./IndexerAddCollectionView'); + +module.exports = { + open : function(collection) { + var schemaCollection = new SchemaCollection(); + var originalUrl = schemaCollection.url; + schemaCollection.url = schemaCollection.url + '/schema'; + schemaCollection.fetch(); + schemaCollection.url = originalUrl; + + var groupedSchemaCollection = new Backbone.Collection(); + + schemaCollection.on('sync', function() { + + var groups = schemaCollection.groupBy(function(model, iterator) { + return model.get('protocol'); + }); + var modelCollection = _.map(groups, function(values, key, list) { + return { + "header" : key, + collection : values + }; + }); + + groupedSchemaCollection.reset(modelCollection); + }); + + var view = new AddCollectionView({ + collection : groupedSchemaCollection, + targetCollection : collection + }); + + AppLayout.modalRegion.show(view); + } +}; \ No newline at end of file diff --git a/src/UI/Settings/NetImport/Delete/IndexerDeleteView.js b/src/UI/Settings/NetImport/Delete/IndexerDeleteView.js new file mode 100644 index 000000000..58e7e3eb5 --- /dev/null +++ b/src/UI/Settings/NetImport/Delete/IndexerDeleteView.js @@ -0,0 +1,19 @@ +var vent = require('vent'); +var Marionette = require('marionette'); + +module.exports = Marionette.ItemView.extend({ + template : 'Settings/Indexers/Delete/IndexerDeleteViewTemplate', + + events : { + 'click .x-confirm-delete' : '_delete' + }, + + _delete : function() { + this.model.destroy({ + wait : true, + success : function() { + vent.trigger(vent.Commands.CloseModalCommand); + } + }); + } +}); \ No newline at end of file diff --git a/src/UI/Settings/NetImport/Delete/IndexerDeleteViewTemplate.hbs b/src/UI/Settings/NetImport/Delete/IndexerDeleteViewTemplate.hbs new file mode 100644 index 000000000..c5c7ad7db --- /dev/null +++ b/src/UI/Settings/NetImport/Delete/IndexerDeleteViewTemplate.hbs @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/src/UI/Settings/NetImport/Edit/IndexerEditView.js b/src/UI/Settings/NetImport/Edit/IndexerEditView.js new file mode 100644 index 000000000..616c863a7 --- /dev/null +++ b/src/UI/Settings/NetImport/Edit/IndexerEditView.js @@ -0,0 +1,122 @@ +var _ = require('underscore'); +var $ = require('jquery'); +var vent = require('vent'); +var Marionette = require('marionette'); +var DeleteView = require('../Delete/IndexerDeleteView'); +var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); +var AsValidatedView = require('../../../Mixins/AsValidatedView'); +var AsEditModalView = require('../../../Mixins/AsEditModalView'); +require('../../../Form/FormBuilder'); +require('../../../Mixins/AutoComplete'); +require('bootstrap'); + +var view = Marionette.ItemView.extend({ + template : 'Settings/Indexers/Edit/IndexerEditViewTemplate', + + events : { + 'click .x-back' : '_back', + 'click .x-captcha-refresh' : '_onRefreshCaptcha' + }, + + _deleteView : DeleteView, + + initialize : function(options) { + this.targetCollection = options.targetCollection; + }, + + _onAfterSave : function() { + this.targetCollection.add(this.model, { merge : true }); + vent.trigger(vent.Commands.CloseModalCommand); + }, + + _onAfterSaveAndAdd : function() { + this.targetCollection.add(this.model, { merge : true }); + + require('../Add/IndexerSchemaModal').open(this.targetCollection); + }, + + _back : function() { + if (this.model.isNew()) { + this.model.destroy(); + } + + require('../Add/IndexerSchemaModal').open(this.targetCollection); + }, + + _onRefreshCaptcha : function(event) { + var self = this; + + var target = $(event.target).parents('.input-group'); + + this.ui.indicator.show(); + + this.model.requestAction("checkCaptcha") + .then(function(result) { + if (!result.captchaRequest) { + self.model.setFieldValue('CaptchaToken', ''); + + return result; + } + + return self._showCaptcha(target, result.captchaRequest); + }) + .always(function() { + self.ui.indicator.hide(); + }); + }, + + _showCaptcha : function(target, captchaRequest) { + var self = this; + + var widget = $('
').insertAfter(target); + + return this._loadRecaptchaWidget(widget[0], captchaRequest.siteKey, captchaRequest.secretToken) + .then(function(captchaResponse) { + target.parents('.form-group').removeAllErrors(); + widget.remove(); + + var queryParams = { + responseUrl : captchaRequest.responseUrl, + ray : captchaRequest.ray, + captchaResponse: captchaResponse + }; + + return self.model.requestAction("getCaptchaCookie", queryParams); + }) + .then(function(response) { + self.model.setFieldValue('CaptchaToken', response.captchaToken); + }); + }, + + _loadRecaptchaWidget : function(widget, sitekey, stoken) { + var promise = $.Deferred(); + + var renderWidget = function() { + window.grecaptcha.render(widget, { + 'sitekey' : sitekey, + 'stoken' : stoken, + 'callback' : promise.resolve + }); + }; + + if (window.grecaptcha) { + renderWidget(); + } else { + window.grecaptchaLoadCallback = function() { + delete window.grecaptchaLoadCallback; + renderWidget(); + }; + + $.getScript('https://www.google.com/recaptcha/api.js?onload=grecaptchaLoadCallback&render=explicit') + .fail(function() { promise.reject(); }); + } + + return promise; + } +}); + +AsModelBoundView.call(view); +AsValidatedView.call(view); +AsEditModalView.call(view); + +module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/NetImport/Edit/IndexerEditViewTemplate.hbs b/src/UI/Settings/NetImport/Edit/IndexerEditViewTemplate.hbs new file mode 100644 index 000000000..acfb62cbb --- /dev/null +++ b/src/UI/Settings/NetImport/Edit/IndexerEditViewTemplate.hbs @@ -0,0 +1,92 @@ + +
+ + +
+
+ + + + + +
+
+
+
From 8b3b46b724631622b26e771aedc0741b04f2f4cc Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Mon, 23 Jan 2017 16:04:52 +0100 Subject: [PATCH 36/83] Added easy to use List Selection for manual import use later. The place where this resides will change. --- src/UI/AddMovies/AddMoviesLayout.js | 93 +++++++++++-------- src/UI/AddMovies/AddMoviesLayoutTemplate.hbs | 76 ++++++++------- .../NetImport/ListSelectionPartial.hbs | 10 ++ 3 files changed, 105 insertions(+), 74 deletions(-) create mode 100644 src/UI/Settings/NetImport/ListSelectionPartial.hbs diff --git a/src/UI/AddMovies/AddMoviesLayout.js b/src/UI/AddMovies/AddMoviesLayout.js index 30cbc74b3..127e0f0c0 100644 --- a/src/UI/AddMovies/AddMoviesLayout.js +++ b/src/UI/AddMovies/AddMoviesLayout.js @@ -5,57 +5,70 @@ var RootFolderLayout = require('./RootFolders/RootFolderLayout'); var ExistingMoviesCollectionView = require('./Existing/AddExistingMovieCollectionView'); var AddMoviesView = require('./AddMoviesView'); var ProfileCollection = require('../Profile/ProfileCollection'); +var ListCollection = require("../Settings/NetImport/NetImportCollection"); var RootFolderCollection = require('./RootFolders/RootFolderCollection'); require('../Movies/MoviesCollection'); module.exports = Marionette.Layout.extend({ - template : 'AddMovies/AddMoviesLayoutTemplate', + template : 'AddMovies/AddMoviesLayoutTemplate', - regions : { - workspace : '#add-movies-workspace' - }, + regions : { + workspace : '#add-movies-workspace' + }, - events : { - 'click .x-import' : '_importMovies', - 'click .x-add-new' : '_addMovies', - 'click .x-show-existing' : '_toggleExisting' - }, + events : { + 'click .x-import' : '_importMovies', + 'click .x-add-new' : '_addMovies', + 'click .x-show-existing' : '_toggleExisting' + }, - attributes : { - id : 'add-movies-screen' - }, + attributes : { + id : 'add-movies-screen' + }, - initialize : function() { - ProfileCollection.fetch(); - RootFolderCollection.fetch().done(function() { - RootFolderCollection.synced = true; - }); - }, + initialize : function() { + ProfileCollection.fetch(); + RootFolderCollection.fetch().done(function() { + RootFolderCollection.synced = true; + }); + this.templateHelpers = {} + this.listCollection = new ListCollection(); + this.templateHelpers.lists = this.listCollection.toJSON(); - _toggleExisting : function(e) { - var showExisting = e.target.checked; - - vent.trigger(vent.Commands.ShowExistingCommand, { - showExisting: showExisting - }); - }, + this.listenTo(this.listCollection, 'all', this._listsUpdated); + this.listCollection.fetch(); - onShow : function() { - this.workspace.show(new AddMoviesView()); - }, + }, - _folderSelected : function(options) { - vent.trigger(vent.Commands.CloseModalCommand); - this.workspace.show(new ExistingMoviesCollectionView({ model : options.model })); - }, + _toggleExisting : function(e) { + var showExisting = e.target.checked; - _importMovies : function() { - this.rootFolderLayout = new RootFolderLayout(); - this.listenTo(this.rootFolderLayout, 'folderSelected', this._folderSelected); - AppLayout.modalRegion.show(this.rootFolderLayout); - }, + vent.trigger(vent.Commands.ShowExistingCommand, { + showExisting: showExisting + }); + }, - _addMovies : function() { - this.workspace.show(new AddMoviesView()); - } + onShow : function() { + this.workspace.show(new AddMoviesView()); + }, + + _listsUpdated : function() { + this.templateHelpers.lists = this.listCollection.toJSON(); + this.render(); + }, + + _folderSelected : function(options) { + vent.trigger(vent.Commands.CloseModalCommand); + this.workspace.show(new ExistingMoviesCollectionView({ model : options.model })); + }, + + _importMovies : function() { + this.rootFolderLayout = new RootFolderLayout(); + this.listenTo(this.rootFolderLayout, 'folderSelected', this._folderSelected); + AppLayout.modalRegion.show(this.rootFolderLayout); + }, + + _addMovies : function() { + this.workspace.show(new AddMoviesView()); + } }); diff --git a/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs b/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs index c2f9ce419..c4a375918 100644 --- a/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs +++ b/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs @@ -1,43 +1,51 @@
-
-
- - -
-
+
+
+ + +
+
-
-
-
- +
+
+
+ -
-
-
+ + + +
+
+
+ +
+ + +
+ {{> ListSelectionPartial lists}} +
+
+
+
-
-
-
+
+
+
diff --git a/src/UI/Settings/NetImport/ListSelectionPartial.hbs b/src/UI/Settings/NetImport/ListSelectionPartial.hbs new file mode 100644 index 000000000..dd93cc17d --- /dev/null +++ b/src/UI/Settings/NetImport/ListSelectionPartial.hbs @@ -0,0 +1,10 @@ + From 87da542758bbd8ea4d90553c7e05be069f073227 Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Mon, 23 Jan 2017 12:54:31 -0500 Subject: [PATCH 37/83] Add import from http://movies.stevenlu.com/ --- .../NetImport/StevenLu/StevenLuAPI.cs | 18 ++++ .../NetImport/StevenLu/StevenLuImport.cs | 36 ++++++++ .../NetImport/StevenLu/StevenLuParser.cs | 82 +++++++++++++++++++ .../StevenLu/StevenLuRequestGenerator.cs | 28 +++++++ .../NetImport/StevenLu/StevenLuSettings.cs | 22 +++++ 5 files changed, 186 insertions(+) create mode 100644 src/NzbDrone.Core/NetImport/StevenLu/StevenLuAPI.cs create mode 100644 src/NzbDrone.Core/NetImport/StevenLu/StevenLuImport.cs create mode 100644 src/NzbDrone.Core/NetImport/StevenLu/StevenLuParser.cs create mode 100644 src/NzbDrone.Core/NetImport/StevenLu/StevenLuRequestGenerator.cs create mode 100644 src/NzbDrone.Core/NetImport/StevenLu/StevenLuSettings.cs diff --git a/src/NzbDrone.Core/NetImport/StevenLu/StevenLuAPI.cs b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuAPI.cs new file mode 100644 index 000000000..e56655278 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuAPI.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Windows.Forms; +using System.Xml.Serialization; + +namespace NzbDrone.Core.NetImport.StevenLu +{ + public class StevenLuResponse + { + public Movie[] Movie { get; set; } + } + + public class Movie + { + public string title { get; set; } + public string imdb_id { get; set; } + public string poster_url { get; set; } + } +} diff --git a/src/NzbDrone.Core/NetImport/StevenLu/StevenLuImport.cs b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuImport.cs new file mode 100644 index 000000000..1f3b76039 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuImport.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Xml.Serialization; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.PassThePopcorn; +using NzbDrone.Core.Parser; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.NetImport.StevenLu +{ + public class StevenLuImport : HttpNetImportBase + { + public override string Name => "Popular movies from StevenLu"; + public override bool Enabled => true; + public override bool EnableAuto => true; + + public StevenLuImport(IHttpClient httpClient, IConfigService configService, IParsingService parsingService, Logger logger) + : base(httpClient, configService, parsingService, logger) + { } + + public override INetImportRequestGenerator GetRequestGenerator() + { + return new StevenLuRequestGenerator() { Settings = Settings }; + } + + public override IParseNetImportResponse GetParser() + { + return new StevenLuParser(Settings); + } + } +} diff --git a/src/NzbDrone.Core/NetImport/StevenLu/StevenLuParser.cs b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuParser.cs new file mode 100644 index 000000000..a73c190aa --- /dev/null +++ b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuParser.cs @@ -0,0 +1,82 @@ +using Newtonsoft.Json; +using NzbDrone.Core.NetImport.Exceptions; +using NzbDrone.Core.Tv; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using System.Windows.Forms; +using System.Xml; +using System.Xml.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.NetImport.StevenLu +{ + public class StevenLuParser : IParseNetImportResponse + { + private readonly StevenLuSettings _settings; + private NetImportResponse _importResponse; + private readonly Logger _logger; + + public StevenLuParser(StevenLuSettings settings) + { + _settings = settings; + } + + public IList ParseResponse(NetImportResponse importResponse) + { + _importResponse = importResponse; + + var movies = new List(); + + if (!PreProcess(_importResponse)) + { + return movies; + } + + var jsonResponse = JsonConvert.DeserializeObject(_importResponse.Content); + + // no movies were return + if (jsonResponse == null) + { + return movies; + } + + foreach (var item in jsonResponse.Movie) + { + movies.AddIfNotNull(new Tv.Movie() + { + Title = item.title, + ImdbId = item.imdb_id + }); + } + + return movies; + } + + protected virtual bool PreProcess(NetImportResponse indexerResponse) + { + if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + { + throw new NetImportException(indexerResponse, "Indexer API call resulted in an unexpected StatusCode [{0}]", indexerResponse.HttpResponse.StatusCode); + } + + if (indexerResponse.HttpResponse.Headers.ContentType != null && indexerResponse.HttpResponse.Headers.ContentType.Contains("text/json") && + indexerResponse.HttpRequest.Headers.Accept != null && !indexerResponse.HttpRequest.Headers.Accept.Contains("text/json")) + { + throw new NetImportException(indexerResponse, "Indexer responded with html content. Site is likely blocked or unavailable."); + } + + return true; + } + + } +} diff --git a/src/NzbDrone.Core/NetImport/StevenLu/StevenLuRequestGenerator.cs b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuRequestGenerator.cs new file mode 100644 index 000000000..9e573bcf4 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuRequestGenerator.cs @@ -0,0 +1,28 @@ +using NzbDrone.Common.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.NetImport.StevenLu +{ + public class StevenLuRequestGenerator : INetImportRequestGenerator + { + public StevenLuSettings Settings { get; set; } + + public virtual NetImportPageableRequestChain GetMovies() + { + var pageableRequests = new NetImportPageableRequestChain(); + + pageableRequests.Add(GetMovies(null)); + + return pageableRequests; + } + + private IEnumerable GetMovies(string searchParameters) + { + var request = new NetImportRequest($"{Settings.Link.Trim()}", HttpAccept.Json); + yield return request; + } + } +} diff --git a/src/NzbDrone.Core/NetImport/StevenLu/StevenLuSettings.cs b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuSettings.cs new file mode 100644 index 000000000..a0829c868 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuSettings.cs @@ -0,0 +1,22 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.NetImport.StevenLu +{ + + public class StevenLuSettings : NetImportBaseSettings + { + public StevenLuSettings() + { + Link = "https://s3.amazonaws.com/popular-movies/movies.json"; + } + + [FieldDefinition(0, Label = "URL", HelpText = "Don't change this unless you know what you are doing.")] + public new string Link { get; set; } + + } + +} From fbe9ad6582dc4a358a13d0a3a3a80cd764aa6a13 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Mon, 23 Jan 2017 20:00:31 +0100 Subject: [PATCH 38/83] First pass at ui for manually importing from lists. --- .../CouchPotato/CouchPotatoSettings.cs | 2 +- src/UI/AddMovies/AddMoviesLayout.js | 16 +- src/UI/AddMovies/AddMoviesLayoutTemplate.hbs | 11 +- .../AddMovies/List/AddFromListCollection.js | 18 ++ .../List/AddFromListCollectionView.js | 51 +++++ .../AddFromListCollectionViewTemplate.hbs | 5 + src/UI/AddMovies/List/AddFromListView.js | 177 ++++++++++++++++++ .../List/AddFromListViewTemplate.hbs | 15 ++ .../NetImport/ListSelectionPartial.hbs | 3 +- 9 files changed, 277 insertions(+), 21 deletions(-) create mode 100644 src/UI/AddMovies/List/AddFromListCollection.js create mode 100644 src/UI/AddMovies/List/AddFromListCollectionView.js create mode 100644 src/UI/AddMovies/List/AddFromListCollectionViewTemplate.hbs create mode 100644 src/UI/AddMovies/List/AddFromListView.js create mode 100644 src/UI/AddMovies/List/AddFromListViewTemplate.hbs diff --git a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs index fcebef860..073213247 100644 --- a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs +++ b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs @@ -30,7 +30,7 @@ namespace NzbDrone.Core.NetImport.CouchPotato [FieldDefinition(3, Label = "CouchPotato API Key", HelpText = "CoouchPootato API Key.")] public string ApiKey { get; set; } - [FieldDefinition(4, Label = "Only Active", HelpText = "Should only active (not yet downloaded) movies be fetched")] + [FieldDefinition(4, Label = "Only Active", HelpText = "Should only active (not yet downloaded) movies be fetched", Type = FieldType.Checkbox)] public bool OnlyActive { get; set; } } diff --git a/src/UI/AddMovies/AddMoviesLayout.js b/src/UI/AddMovies/AddMoviesLayout.js index 127e0f0c0..00344bfdb 100644 --- a/src/UI/AddMovies/AddMoviesLayout.js +++ b/src/UI/AddMovies/AddMoviesLayout.js @@ -5,7 +5,7 @@ var RootFolderLayout = require('./RootFolders/RootFolderLayout'); var ExistingMoviesCollectionView = require('./Existing/AddExistingMovieCollectionView'); var AddMoviesView = require('./AddMoviesView'); var ProfileCollection = require('../Profile/ProfileCollection'); -var ListCollection = require("../Settings/NetImport/NetImportCollection"); +var AddFromListView = require("./List/AddFromListView"); var RootFolderCollection = require('./RootFolders/RootFolderCollection'); require('../Movies/MoviesCollection'); @@ -19,6 +19,7 @@ module.exports = Marionette.Layout.extend({ events : { 'click .x-import' : '_importMovies', 'click .x-add-new' : '_addMovies', + "click .x-add-lists" : "_addFromList", 'click .x-show-existing' : '_toggleExisting' }, @@ -31,12 +32,7 @@ module.exports = Marionette.Layout.extend({ RootFolderCollection.fetch().done(function() { RootFolderCollection.synced = true; }); - this.templateHelpers = {} - this.listCollection = new ListCollection(); - this.templateHelpers.lists = this.listCollection.toJSON(); - this.listenTo(this.listCollection, 'all', this._listsUpdated); - this.listCollection.fetch(); }, @@ -52,10 +48,6 @@ module.exports = Marionette.Layout.extend({ this.workspace.show(new AddMoviesView()); }, - _listsUpdated : function() { - this.templateHelpers.lists = this.listCollection.toJSON(); - this.render(); - }, _folderSelected : function(options) { vent.trigger(vent.Commands.CloseModalCommand); @@ -70,5 +62,9 @@ module.exports = Marionette.Layout.extend({ _addMovies : function() { this.workspace.show(new AddMoviesView()); + }, + + _addFromList : function() { + this.workspace.show(new AddFromListView()); } }); diff --git a/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs b/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs index c4a375918..0e251dcb7 100644 --- a/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs +++ b/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs @@ -1,11 +1,12 @@
- +
@@ -33,14 +34,6 @@
- -
- - -
- {{> ListSelectionPartial lists}} -
-
diff --git a/src/UI/AddMovies/List/AddFromListCollection.js b/src/UI/AddMovies/List/AddFromListCollection.js new file mode 100644 index 000000000..12f5cb7f0 --- /dev/null +++ b/src/UI/AddMovies/List/AddFromListCollection.js @@ -0,0 +1,18 @@ +var Backbone = require('backbone'); +var MovieModel = require('../../Movies/MovieModel'); +var _ = require('underscore'); + +module.exports = Backbone.Collection.extend({ + url : window.NzbDrone.ApiRoot + '/netimport/movies', + model : MovieModel, + + parse : function(response) { + var self = this; + + _.each(response, function(model) { + model.id = undefined; + }); + + return response; + } +}); diff --git a/src/UI/AddMovies/List/AddFromListCollectionView.js b/src/UI/AddMovies/List/AddFromListCollectionView.js new file mode 100644 index 000000000..4177bd995 --- /dev/null +++ b/src/UI/AddMovies/List/AddFromListCollectionView.js @@ -0,0 +1,51 @@ +var Marionette = require('marionette'); +var AddMoviesView = require('../AddMoviesView'); +var vent = require('vent'); + +module.exports = Marionette.CompositeView.extend({ + itemView : AddMoviesView, + itemViewContainer : '.x-loading-folders', + template : 'AddMovies/List/AddFromListCollectionViewTemplate', + + ui : { + loadingFolders : '.x-loading-list' + }, + + initialize : function() { + this.collection = new UnmappedFolderCollection(); + this.collection.importItems(this.model); + }, + + showCollection : function() { + this._showAndSearch(0); + }, + + appendHtml : function(collectionView, itemView, index) { + collectionView.ui.loadingFolders.before(itemView.el); + }, + + _showAndSearch : function(index) { + var self = this; + var model = this.collection.at(index); + + if (model) { + var currentIndex = index; + var folderName = model.get('folder').name; + this.addItemView(model, this.getItemView(), index); + this.children.findByModel(model).search({ term : folderName }).always(function() { + if (!self.isClosed) { + self._showAndSearch(currentIndex + 1); + } + }); + } + + else { + this.ui.loadingFolders.hide(); + } + }, + + itemViewOptions : { + isExisting : true + } + +}); diff --git a/src/UI/AddMovies/List/AddFromListCollectionViewTemplate.hbs b/src/UI/AddMovies/List/AddFromListCollectionViewTemplate.hbs new file mode 100644 index 000000000..dc812c87f --- /dev/null +++ b/src/UI/AddMovies/List/AddFromListCollectionViewTemplate.hbs @@ -0,0 +1,5 @@ +
+
+ Loading search results from TheTVDB for your movies, this may take a few minutes. +
+
diff --git a/src/UI/AddMovies/List/AddFromListView.js b/src/UI/AddMovies/List/AddFromListView.js new file mode 100644 index 000000000..199240007 --- /dev/null +++ b/src/UI/AddMovies/List/AddFromListView.js @@ -0,0 +1,177 @@ +var _ = require('underscore'); +var vent = require('vent'); +var Marionette = require('marionette'); +var AddFromListCollection = require('./AddFromListCollection'); +//var SearchResultCollectionView = require('./SearchResultCollectionView'); +var AddListView = require("../../Settings/NetImport/Add/NetImportAddItemView"); +var EmptyView = require('../EmptyView'); +var NotFoundView = require('../NotFoundView'); +var ListCollection = require("../../Settings/NetImport/NetImportCollection"); +var ErrorView = require('../ErrorView'); +var LoadingView = require('../../Shared/LoadingView'); +var AppLayout = require('../../AppLayout'); +var SchemaModal = require('../../Settings/NetImport/Add/NetImportSchemaModal'); + +module.exports = Marionette.Layout.extend({ + template : 'AddMovies/List/AddFromListViewTemplate', + + regions : { + fetchResult : '#fetch-result' + }, + + ui : { + moviesSearch : '.x-movies-search', + listSelection : ".x-list-selection", + + }, + + events : { + 'click .x-load-more' : '_onLoadMore', + "change .x-list-selection" : "_listSelected", + "click .x-fetch-list" : "_fetchList" + }, + + initialize : function(options) { + console.log(options); + + this.isExisting = options.isExisting; + //this.collection = new AddFromListCollection(); + + this.templateHelpers = {} + this.listCollection = new ListCollection(); + this.templateHelpers.lists = this.listCollection.toJSON(); + + this.listenTo(this.listCollection, 'all', this._listsUpdated); + this.listCollection.fetch(); + + this.collection = new AddFromListCollection(); + + /*this.listenTo(this.collection, 'sync', this._showResults); + + this.resultCollectionView = new SearchResultCollectionView({ + collection : this.collection, + isExisting : this.isExisting + });*/ + + //this.throttledSearch = _.debounce(this.search, 1000, { trailing : true }).bind(this); + }, + + onRender : function() { + var self = this; + }, + + onShow : function() { + this.ui.moviesSearch.focus(); + }, + + search : function(options) { + var self = this; + + this.collection.reset(); + + if (!options.term || options.term === this.collection.term) { + return Marionette.$.Deferred().resolve(); + } + + this.searchResult.show(new LoadingView()); + this.collection.term = options.term; + this.currentSearchPromise = this.collection.fetch({ + data : { term : options.term } + }); + + this.currentSearchPromise.fail(function() { + self._showError(); + }); + + return this.currentSearchPromise; + }, + + _onMoviesAdded : function(options) { + if (this.isExisting && options.movie.get('path') === this.model.get('folder').path) { + this.close(); + } + + else if (!this.isExisting) { + this.resultCollectionView.setExisting(options.movie.get('tmdbId')); + /*this.collection.term = ''; + this.collection.reset(); + this._clearResults(); + this.ui.moviesSearch.val(''); + this.ui.moviesSearch.focus();*/ //TODO: Maybe add option wheter to clear search result. + } + }, + + _onLoadMore : function() { + var showingAll = this.resultCollectionView.showMore(); + this.ui.searchBar.show(); + + if (showingAll) { + this.ui.loadMore.hide(); + } + }, + + _listSelected : function() { + var rootFolderValue = this.ui.listSelection.val(); + if (rootFolderValue === 'addNew') { + //var rootFolderLayout = new SchemaModal(this.listCollection); + //AppLayout.modalRegion.show(rootFolderLayout); + SchemaModal.open(this.listCollection) + } + }, + + _fetchList : function() { + var self = this; + var listId = this.ui.listSelection.val(); + + this.fetchResult.show(new LoadingView()); + + this.currentFetchPromise = this.collection.fetch( + { data : { profileId : listId} } + ) + this.currentFetchPromise.fail(function() { + self._showError(); + }); + + }, + + _listsUpdated : function() { + this.templateHelpers.lists = this.listCollection.toJSON(); + this.render(); + }, + + _clearResults : function() { + + if (!this.isExisting) { + this.searchResult.show(new EmptyView()); + } else { + this.searchResult.close(); + } + }, + + _showResults : function() { + if (!this.isClosed) { + if (this.collection.length === 0) { + this.ui.searchBar.show(); + this.searchResult.show(new NotFoundView({ term : this.collection.term })); + } else { + this.searchResult.show(this.resultCollectionView); + if (!this.showingAll) { + this.ui.loadMore.show(); + } + } + } + }, + + _abortExistingSearch : function() { + if (this.currentSearchPromise && this.currentSearchPromise.readyState > 0 && this.currentSearchPromise.readyState < 4) { + console.log('aborting previous pending search request.'); + this.currentSearchPromise.abort(); + } else { + this._clearResults(); + } + }, + + _showError : function() { + this.fetchResult.show(new ErrorView({ term : "" })); + } +}); diff --git a/src/UI/AddMovies/List/AddFromListViewTemplate.hbs b/src/UI/AddMovies/List/AddFromListViewTemplate.hbs new file mode 100644 index 000000000..fcf4e2611 --- /dev/null +++ b/src/UI/AddMovies/List/AddFromListViewTemplate.hbs @@ -0,0 +1,15 @@ + +
+
+
diff --git a/src/UI/Settings/NetImport/ListSelectionPartial.hbs b/src/UI/Settings/NetImport/ListSelectionPartial.hbs index dd93cc17d..d2b37459d 100644 --- a/src/UI/Settings/NetImport/ListSelectionPartial.hbs +++ b/src/UI/Settings/NetImport/ListSelectionPartial.hbs @@ -1,4 +1,5 @@ - + {{#if this}} {{#each this}} From e59db74cada839c347b698c086934b6a8655ae5f Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Mon, 23 Jan 2017 14:39:27 -0500 Subject: [PATCH 39/83] Add StevenLu to csproj --- src/NzbDrone.Core/NzbDrone.Core.csproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 704687008..4f2c6c742 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -128,6 +128,11 @@ + + + + + From 032bc2d5c4474710c21d2ab0cbe332bcebb0a539 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Mon, 23 Jan 2017 20:51:33 +0100 Subject: [PATCH 40/83] Add basic ui of manual import. --- src/NzbDrone.Api/NzbDrone.Api.csproj | 3 +- .../Series/FetchMovieListModule.cs | 45 ++++++++++++ .../List/AddFromListCollectionView.js | 72 +++++++++---------- .../AddFromListCollectionViewTemplate.hbs | 5 +- src/UI/AddMovies/List/AddFromListView.js | 53 +++++++++++--- src/UI/AddMovies/List/ListItemView.js | 22 ++++++ .../AddMovies/List/ListItemViewTemplate.hbs | 3 + 7 files changed, 151 insertions(+), 52 deletions(-) create mode 100644 src/NzbDrone.Api/Series/FetchMovieListModule.cs create mode 100644 src/UI/AddMovies/List/ListItemView.js create mode 100644 src/UI/AddMovies/List/ListItemViewTemplate.hbs diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index de7b4faab..097933cdc 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -238,6 +238,7 @@ + @@ -299,4 +300,4 @@ --> - + \ No newline at end of file diff --git a/src/NzbDrone.Api/Series/FetchMovieListModule.cs b/src/NzbDrone.Api/Series/FetchMovieListModule.cs new file mode 100644 index 000000000..a6ec92f76 --- /dev/null +++ b/src/NzbDrone.Api/Series/FetchMovieListModule.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using Nancy; +using NzbDrone.Api.Extensions; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MetadataSource; +using System.Linq; +using NzbDrone.Core.NetImport; + +namespace NzbDrone.Api.Movie +{ + public class FetchMovieListModule : NzbDroneRestModule + { + private readonly IFetchNetImport _fetchNetImport; + + public FetchMovieListModule(IFetchNetImport netImport) + : base("/netimport/movies") + { + _fetchNetImport = netImport; + Get["/"] = x => Search(); + } + + + private Response Search() + { + var results = _fetchNetImport.FetchAndFilter((int) Request.Query.listId, false); + return MapToResource(results).AsResponse(); + } + + + private static IEnumerable MapToResource(IEnumerable movies) + { + foreach (var currentSeries in movies) + { + var resource = currentSeries.ToResource(); + var poster = currentSeries.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster); + if (poster != null) + { + resource.RemotePoster = poster.Url; + } + + yield return resource; + } + } + } +} \ No newline at end of file diff --git a/src/UI/AddMovies/List/AddFromListCollectionView.js b/src/UI/AddMovies/List/AddFromListCollectionView.js index 4177bd995..91a963601 100644 --- a/src/UI/AddMovies/List/AddFromListCollectionView.js +++ b/src/UI/AddMovies/List/AddFromListCollectionView.js @@ -1,51 +1,47 @@ var Marionette = require('marionette'); -var AddMoviesView = require('../AddMoviesView'); +var ListItemView = require('./ListItemView'); var vent = require('vent'); -module.exports = Marionette.CompositeView.extend({ - itemView : AddMoviesView, - itemViewContainer : '.x-loading-folders', - template : 'AddMovies/List/AddFromListCollectionViewTemplate', +module.exports = Marionette.CollectionView.extend({ + itemView : ListItemView, ui : { - loadingFolders : '.x-loading-list' + loadingList : '.x-loading-list' }, initialize : function() { - this.collection = new UnmappedFolderCollection(); - this.collection.importItems(this.model); + }, showCollection : function() { - this._showAndSearch(0); - }, - - appendHtml : function(collectionView, itemView, index) { - collectionView.ui.loadingFolders.before(itemView.el); - }, - - _showAndSearch : function(index) { - var self = this; - var model = this.collection.at(index); - - if (model) { - var currentIndex = index; - var folderName = model.get('folder').name; - this.addItemView(model, this.getItemView(), index); - this.children.findByModel(model).search({ term : folderName }).always(function() { - if (!self.isClosed) { - self._showAndSearch(currentIndex + 1); - } - }); - } - - else { - this.ui.loadingFolders.hide(); - } - }, - - itemViewOptions : { - isExisting : true - } + }, + // + // appendHtml : function(collectionView, itemView, index) { + // collectionView.ui.loadingFolders.before(itemView.el); + // }, + // + // _showAndSearch : function(index) { + // var self = this; + // var model = this.collection.at(index); + // + // if (model) { + // var currentIndex = index; + // var folderName = model.get('folder').name; + // this.addItemView(model, this.getItemView(), index); + // this.children.findByModel(model).search({ term : folderName }).always(function() { + // if (!self.isClosed) { + // self._showAndSearch(currentIndex + 1); + // } + // }); + // } + // + // else { + // this.ui.loadingFolders.hide(); + // } + // }, + // + // itemViewOptions : { + // isExisting : true + // } }); diff --git a/src/UI/AddMovies/List/AddFromListCollectionViewTemplate.hbs b/src/UI/AddMovies/List/AddFromListCollectionViewTemplate.hbs index dc812c87f..34a766b7a 100644 --- a/src/UI/AddMovies/List/AddFromListCollectionViewTemplate.hbs +++ b/src/UI/AddMovies/List/AddFromListCollectionViewTemplate.hbs @@ -1,5 +1,4 @@
-
- Loading search results from TheTVDB for your movies, this may take a few minutes. -
+
+
diff --git a/src/UI/AddMovies/List/AddFromListView.js b/src/UI/AddMovies/List/AddFromListView.js index 199240007..292d02665 100644 --- a/src/UI/AddMovies/List/AddFromListView.js +++ b/src/UI/AddMovies/List/AddFromListView.js @@ -1,8 +1,9 @@ var _ = require('underscore'); var vent = require('vent'); var Marionette = require('marionette'); +var Backgrid = require('backgrid'); var AddFromListCollection = require('./AddFromListCollection'); -//var SearchResultCollectionView = require('./SearchResultCollectionView'); +var AddFromListCollectionView = require('./AddFromListCollectionView'); var AddListView = require("../../Settings/NetImport/Add/NetImportAddItemView"); var EmptyView = require('../EmptyView'); var NotFoundView = require('../NotFoundView'); @@ -10,6 +11,16 @@ var ListCollection = require("../../Settings/NetImport/NetImportCollection"); var ErrorView = require('../ErrorView'); var LoadingView = require('../../Shared/LoadingView'); var AppLayout = require('../../AppLayout'); +var InCinemasCell = require('../../Cells/InCinemasCell'); +var MovieTitleCell = require('../../Cells/MovieTitleCell'); +var TemplatedCell = require('../../Cells/TemplatedCell'); +var ProfileCell = require('../../Cells/ProfileCell'); +var MovieLinksCell = require('../../Cells/MovieLinksCell'); +var MovieActionCell = require('../../Cells/MovieActionCell'); +var MovieStatusCell = require('../../Cells/MovieStatusCell'); +var MovieDownloadStatusCell = require('../../Cells/MovieDownloadStatusCell'); +var DownloadedQualityCell = require('../../Cells/DownloadedQualityCell'); + var SchemaModal = require('../../Settings/NetImport/Add/NetImportSchemaModal'); module.exports = Marionette.Layout.extend({ @@ -25,6 +36,27 @@ module.exports = Marionette.Layout.extend({ }, + columns : [ + { + name : 'title', + label : 'Title', + cell : MovieTitleCell, + cellValue : 'this', + }, + { + name : 'profileId', + label : 'Profile', + cell : ProfileCell + }, + { + name : 'this', + label : 'Links', + cell : MovieLinksCell, + className : "movie-links-cell", + sortable : false, + } + ], + events : { 'click .x-load-more' : '_onLoadMore', "change .x-list-selection" : "_listSelected", @@ -46,6 +78,8 @@ module.exports = Marionette.Layout.extend({ this.collection = new AddFromListCollection(); + this.listenTo(this.collection, 'sync', this._showResults); + /*this.listenTo(this.collection, 'sync', this._showResults); this.resultCollectionView = new SearchResultCollectionView({ @@ -126,7 +160,7 @@ module.exports = Marionette.Layout.extend({ this.fetchResult.show(new LoadingView()); this.currentFetchPromise = this.collection.fetch( - { data : { profileId : listId} } + { data : { listId : listId} } ) this.currentFetchPromise.fail(function() { self._showError(); @@ -149,17 +183,16 @@ module.exports = Marionette.Layout.extend({ }, _showResults : function() { - if (!this.isClosed) { if (this.collection.length === 0) { - this.ui.searchBar.show(); - this.searchResult.show(new NotFoundView({ term : this.collection.term })); + this.fetchResult.show(new NotFoundView({ term : "" })); } else { - this.searchResult.show(this.resultCollectionView); - if (!this.showingAll) { - this.ui.loadMore.show(); - } + this.fetchResult.show(new Backgrid.Grid({ + collection : this.collection, + columns : this.columns, + className : 'table table-hover' + })); } - } + }, _abortExistingSearch : function() { diff --git a/src/UI/AddMovies/List/ListItemView.js b/src/UI/AddMovies/List/ListItemView.js new file mode 100644 index 000000000..f93b2778e --- /dev/null +++ b/src/UI/AddMovies/List/ListItemView.js @@ -0,0 +1,22 @@ +var _ = require('underscore'); +var vent = require('vent'); +var AppLayout = require('../../AppLayout'); +var Backbone = require('backbone'); +var Marionette = require('marionette'); +var Config = require('../../Config'); +var Messenger = require('../../Shared/Messenger'); +var AsValidatedView = require('../../Mixins/AsValidatedView'); + +require('jquery.dotdotdot'); + +var view = Marionette.ItemView.extend({ + + template : 'AddMovies/SearchResultViewTemplate', + + +}) + + +AsValidatedView.apply(view); + +module.exports = view; diff --git a/src/UI/AddMovies/List/ListItemViewTemplate.hbs b/src/UI/AddMovies/List/ListItemViewTemplate.hbs new file mode 100644 index 000000000..70d974ae7 --- /dev/null +++ b/src/UI/AddMovies/List/ListItemViewTemplate.hbs @@ -0,0 +1,3 @@ +
+ ASDF +
From e182d8b964fb7bca97357b7691141853293a41f3 Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Mon, 23 Jan 2017 15:18:04 -0500 Subject: [PATCH 41/83] fix importing for StevenLu --- src/NzbDrone.Core/NetImport/StevenLu/StevenLuAPI.cs | 5 ----- src/NzbDrone.Core/NetImport/StevenLu/StevenLuParser.cs | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/NzbDrone.Core/NetImport/StevenLu/StevenLuAPI.cs b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuAPI.cs index e56655278..1dedc8719 100644 --- a/src/NzbDrone.Core/NetImport/StevenLu/StevenLuAPI.cs +++ b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuAPI.cs @@ -5,11 +5,6 @@ using System.Xml.Serialization; namespace NzbDrone.Core.NetImport.StevenLu { public class StevenLuResponse - { - public Movie[] Movie { get; set; } - } - - public class Movie { public string title { get; set; } public string imdb_id { get; set; } diff --git a/src/NzbDrone.Core/NetImport/StevenLu/StevenLuParser.cs b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuParser.cs index a73c190aa..9c032c162 100644 --- a/src/NzbDrone.Core/NetImport/StevenLu/StevenLuParser.cs +++ b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuParser.cs @@ -42,7 +42,7 @@ namespace NzbDrone.Core.NetImport.StevenLu return movies; } - var jsonResponse = JsonConvert.DeserializeObject(_importResponse.Content); + var jsonResponse = JsonConvert.DeserializeObject>(_importResponse.Content); // no movies were return if (jsonResponse == null) @@ -50,7 +50,7 @@ namespace NzbDrone.Core.NetImport.StevenLu return movies; } - foreach (var item in jsonResponse.Movie) + foreach (var item in jsonResponse) { movies.AddIfNotNull(new Tv.Movie() { From 43d904d20b1ec99aa55ed973acd8c9fc94a46d24 Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Mon, 23 Jan 2017 18:06:42 -0500 Subject: [PATCH 42/83] added trakt user list importing --- src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs | 29 +++++++ .../NetImport/Trakt/TraktImport.cs | 36 ++++++++ .../NetImport/Trakt/TraktParser.cs | 84 +++++++++++++++++++ .../NetImport/Trakt/TraktRequestGenerator.cs | 35 ++++++++ .../NetImport/Trakt/TraktSettings.cs | 30 +++++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 7 +- 6 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs create mode 100644 src/NzbDrone.Core/NetImport/Trakt/TraktImport.cs create mode 100644 src/NzbDrone.Core/NetImport/Trakt/TraktParser.cs create mode 100644 src/NzbDrone.Core/NetImport/Trakt/TraktRequestGenerator.cs create mode 100644 src/NzbDrone.Core/NetImport/Trakt/TraktSettings.cs diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs new file mode 100644 index 000000000..175ff0555 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Windows.Forms; +using System.Xml.Serialization; + +namespace NzbDrone.Core.NetImport.Trakt +{ + public class Ids + { + public int trakt { get; set; } + public string slug { get; set; } + public string imdb { get; set; } + public int tmdb { get; set; } + } + + public class Movie + { + public string title { get; set; } + public int year { get; set; } + public Ids ids { get; set; } + } + + public class TraktResponse + { + public int rank { get; set; } + public string listed_at { get; set; } + public string type { get; set; } + public Movie movie { get; set; } + } +} diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktImport.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktImport.cs new file mode 100644 index 000000000..1e0e083ae --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktImport.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Xml.Serialization; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.PassThePopcorn; +using NzbDrone.Core.Parser; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.NetImport.Trakt +{ + public class TraktImport : HttpNetImportBase + { + public override string Name => "Trakt User List"; + public override bool Enabled => true; + public override bool EnableAuto => false; + + public TraktImport(IHttpClient httpClient, IConfigService configService, IParsingService parsingService, Logger logger) + : base(httpClient, configService, parsingService, logger) + { } + + public override INetImportRequestGenerator GetRequestGenerator() + { + return new TraktRequestGenerator() { Settings = Settings }; + } + + public override IParseNetImportResponse GetParser() + { + return new TraktParser(Settings); + } + } +} diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktParser.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktParser.cs new file mode 100644 index 000000000..611dfc5b8 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktParser.cs @@ -0,0 +1,84 @@ +using Newtonsoft.Json; +using NzbDrone.Core.NetImport.Exceptions; +using NzbDrone.Core.Tv; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using System.Windows.Forms; +using System.Xml; +using System.Xml.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.NetImport.Trakt +{ + public class TraktParser : IParseNetImportResponse + { + private readonly TraktSettings _settings; + private NetImportResponse _importResponse; + private readonly Logger _logger; + + public TraktParser(TraktSettings settings) + { + _settings = settings; + } + + public IList ParseResponse(NetImportResponse importResponse) + { + _importResponse = importResponse; + + var movies = new List(); + + if (!PreProcess(_importResponse)) + { + return movies; + } + + var jsonResponse = JsonConvert.DeserializeObject>(_importResponse.Content); + + // no movies were return + if (jsonResponse == null) + { + return movies; + } + + foreach (var movie in jsonResponse) + { + movies.AddIfNotNull(new Tv.Movie() + { + Title = movie.movie.title, + ImdbId = movie.movie.ids.imdb, + TmdbId = movie.movie.ids.tmdb, + Year = movie.movie.year + }); + } + + return movies; + } + + protected virtual bool PreProcess(NetImportResponse indexerResponse) + { + if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + { + throw new NetImportException(indexerResponse, "Indexer API call resulted in an unexpected StatusCode [{0}]", indexerResponse.HttpResponse.StatusCode); + } + + if (indexerResponse.HttpResponse.Headers.ContentType != null && indexerResponse.HttpResponse.Headers.ContentType.Contains("text/json") && + indexerResponse.HttpRequest.Headers.Accept != null && !indexerResponse.HttpRequest.Headers.Accept.Contains("text/json")) + { + throw new NetImportException(indexerResponse, "Indexer responded with html content. Site is likely blocked or unavailable."); + } + + return true; + } + + } +} diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktRequestGenerator.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktRequestGenerator.cs new file mode 100644 index 000000000..152da8986 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktRequestGenerator.cs @@ -0,0 +1,35 @@ +using NzbDrone.Common.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.NetImport.Trakt +{ + public class TraktRequestGenerator : INetImportRequestGenerator + { + public TraktSettings Settings { get; set; } + + public virtual NetImportPageableRequestChain GetMovies() + { + var pageableRequests = new NetImportPageableRequestChain(); + + pageableRequests.Add(GetMovies(null)); + + return pageableRequests; + } + + private IEnumerable GetMovies(string searchParameters) + { + // https://api.trakt.tv/users/timdturner/lists/custom1/items/movies + // trakt-api-version = 2 + // trakt-api-key = 657bb899dcb81ec8ee838ff09f6e013ff7c740bf0ccfa54dd41e791b9a70b2f0 + + var request = new NetImportRequest($"{Settings.Link.Trim()}{Settings.Username.Trim()}/lists/{Settings.Listname.Trim()}/items/movies", HttpAccept.Json); + request.HttpRequest.Headers.Add("trakt-api-version", "2"); + request.HttpRequest.Headers.Add("trakt-api-key", "657bb899dcb81ec8ee838ff09f6e013ff7c740bf0ccfa54dd41e791b9a70b2f0"); + + yield return request; + } + } +} diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktSettings.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktSettings.cs new file mode 100644 index 000000000..2279cf373 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktSettings.cs @@ -0,0 +1,30 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.NetImport.Trakt +{ + + public class TraktSettings : NetImportBaseSettings + { + public TraktSettings() + { + Link = "https://api.trakt.tv/users/"; + Username = ""; + Listname = ""; + } + + [FieldDefinition(0, Label = "Trakt API URL", HelpText = "Link to to Trakt API URL, do not change unless you know what you are doing.")] + public new string Link { get; set; } + + [FieldDefinition(1, Label = "Trakt Username", HelpText = "Trakt Username the list belongs to.")] + public string Username { get; set; } + + [FieldDefinition(2, Label = "Trakt List Name", HelpText = "Trakt List Name")] + public string Listname { get; set; } + + } + +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 4f2c6c742..1b6ef7f6f 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -123,6 +123,11 @@ + + + + + @@ -132,7 +137,7 @@ - + From d09d30544f37f12c2f13ceb06ce1f3c16a8439a3 Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Tue, 24 Jan 2017 13:22:02 -0500 Subject: [PATCH 43/83] allow null value for seed time. --- src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoAPI.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoAPI.cs b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoAPI.cs index 1da2039ed..6a11b87b4 100644 --- a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoAPI.cs +++ b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoAPI.cs @@ -59,7 +59,7 @@ namespace NzbDrone.Core.NetImport.CouchPotato public int leechers { get; set; } public int score { get; set; } public string provider { get; set; } - public int seed_time { get; set; } + public int? seed_time { get; set; } public string provider_extra { get; set; } public string detail_url { get; set; } public string type { get; set; } From a75f3e1f8ea80d87a2a45276c82b3e5459a37bd3 Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Tue, 24 Jan 2017 13:42:27 -0500 Subject: [PATCH 44/83] monitored to false for movies already downloaded on CP --- src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoParser.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoParser.cs b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoParser.cs index fbaf7055e..424e5cc71 100644 --- a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoParser.cs +++ b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoParser.cs @@ -77,7 +77,8 @@ namespace NzbDrone.Core.NetImport.CouchPotato { Title = item.title, ImdbId = item.info.imdb, - TmdbId = item.info.tmdb_id + TmdbId = item.info.tmdb_id, + Monitored = false }); } } From 95d97c59d798f0a838cdc6c2d10a26977d8a17e7 Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Tue, 24 Jan 2017 13:47:44 -0500 Subject: [PATCH 45/83] rephrase wording --- src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs index 073213247..d299e1b92 100644 --- a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs +++ b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs @@ -30,7 +30,7 @@ namespace NzbDrone.Core.NetImport.CouchPotato [FieldDefinition(3, Label = "CouchPotato API Key", HelpText = "CoouchPootato API Key.")] public string ApiKey { get; set; } - [FieldDefinition(4, Label = "Only Active", HelpText = "Should only active (not yet downloaded) movies be fetched", Type = FieldType.Checkbox)] + [FieldDefinition(4, Label = "Only Wanted", HelpText = "Only add wanted movies.", Type = FieldType.Checkbox)] public bool OnlyActive { get; set; } } From e2c2bdb65b19e0731e8e2a9ce81d9c9367aa2a68 Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Wed, 25 Jan 2017 11:35:10 -0500 Subject: [PATCH 46/83] nullable all the fields.. --- .../NetImport/CouchPotato/CouchPotatoAPI.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoAPI.cs b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoAPI.cs index 6a11b87b4..4982e02da 100644 --- a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoAPI.cs +++ b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoAPI.cs @@ -29,47 +29,47 @@ namespace NzbDrone.Core.NetImport.CouchPotato public class Info { public string[] genres { get; set; } - public int tmdb_id { get; set; } + public int? tmdb_id { get; set; } public string plot { get; set; } public string tagline { get; set; } - public int year { get; set; } + public int? year { get; set; } public string original_title { get; set; } - public bool via_imdb { get; set; } + public bool? via_imdb { get; set; } public string[] directors { get; set; } public string[] titles { get; set; } public string imdb { get; set; } public string mpaa { get; set; } - public bool via_tmdb { get; set; } + public bool? via_tmdb { get; set; } public string[] actors { get; set; } public string[] writers { get; set; } - public int runtime { get; set; } + public int? runtime { get; set; } public string type { get; set; } public string released { get; set; } } public class ReleaseInfo { - public double size { get; set; } - public int seeders { get; set; } + public double? size { get; set; } + public int? seeders { get; set; } public string protocol { get; set; } public string description { get; set; } public string url { get; set; } - public int age { get; set; } + public int? age { get; set; } public string id { get; set; } - public int leechers { get; set; } - public int score { get; set; } + public int? leechers { get; set; } + public int? score { get; set; } public string provider { get; set; } public int? seed_time { get; set; } public string provider_extra { get; set; } public string detail_url { get; set; } public string type { get; set; } - public double seed_ratio { get; set; } + public double? seed_ratio { get; set; } public string name { get; set; } } public class DownloadInfo { - public bool status_support { get; set; } + public bool? status_support { get; set; } public string id { get; set; } public string downloader { get; set; } } @@ -83,8 +83,8 @@ namespace NzbDrone.Core.NetImport.CouchPotato public string media_id { get; set; } public string _rev { get; set; } public string _t { get; set; } - public bool is_3d { get; set; } - public int last_edit { get; set; } + public bool? is_3d { get; set; } + public int? last_edit { get; set; } public string identifier { get; set; } public string quality { get; set; } } From 72aac6e551a8f2d6526fde07e235369a9377a2fc Mon Sep 17 00:00:00 2001 From: Tim Turner Date: Wed, 25 Jan 2017 19:03:40 -0500 Subject: [PATCH 47/83] Update Rename Preview to support folder renaming --- .../MediaFiles/RenameMovieFileService.cs | 5 +-- .../Organizer/FileNameBuilder.cs | 31 +++++++++++++++++-- src/UI/Rename/RenamePreviewFormatView.js | 1 + .../RenamePreviewFormatViewTemplate.hbs | 1 + src/UI/Rename/RenamePreviewLayoutTemplate.hbs | 2 +- 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/RenameMovieFileService.cs b/src/NzbDrone.Core/MediaFiles/RenameMovieFileService.cs index 9a4019f56..38036cf5a 100644 --- a/src/NzbDrone.Core/MediaFiles/RenameMovieFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/RenameMovieFileService.cs @@ -71,8 +71,9 @@ namespace NzbDrone.Core.MediaFiles { MovieId = movie.Id, MovieFileId = file.Id, - ExistingPath = file.RelativePath, - NewPath = movie.Path.GetRelativePath(newPath) + ExistingPath = movieFilePath, + //NewPath = movie.Path.GetRelativePath(newPath) + NewPath = newPath }; } diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 535a89071..7cd3e0c0e 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -155,12 +155,11 @@ namespace NzbDrone.Core.Organizer return GetOriginalTitle(movieFile); } - //TODO: Update namingConfig for Movies! var pattern = namingConfig.StandardMovieFormat; var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); AddMovieTokens(tokenHandlers, movie); - AddReleaseDateTokens(tokenHandlers, movie.Year); //In case we want to separate the year + AddReleaseDateTokens(tokenHandlers, movie.Year); AddImdbIdTokens(tokenHandlers, movie.ImdbId); AddQualityTokens(tokenHandlers, movie, movieFile); AddMediaInfoTokens(tokenHandlers, movieFile); @@ -186,11 +185,37 @@ namespace NzbDrone.Core.Organizer { Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); - var path = movie.Path; + var path = BuildMoviePath(movie); return Path.Combine(path, fileName + extension); } + public string BuildMoviePath(Movie movie) + { + var path = movie.Path; + var directory = new DirectoryInfo(path).Name; + var parentDirectoryPath = new DirectoryInfo(path).Parent.FullName; + var namingConfig = _namingConfigService.GetConfig(); + + var movieFile = movie.MovieFile; + + var pattern = namingConfig.MovieFolderFormat; + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + AddMovieTokens(tokenHandlers, movie); + AddReleaseDateTokens(tokenHandlers, movie.Year); + AddImdbIdTokens(tokenHandlers, movie.ImdbId); + AddQualityTokens(tokenHandlers, movie, movieFile); + AddMediaInfoTokens(tokenHandlers, movieFile); + AddMovieFileTokens(tokenHandlers, movieFile); + + var directoryName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); + directoryName = FileNameCleanupRegex.Replace(directoryName, match => match.Captures[0].Value[0].ToString()); + directoryName = TrimSeparatorsRegex.Replace(directoryName, string.Empty); + + return Path.Combine(parentDirectoryPath, directoryName); + } + public string BuildSeasonPath(Series series, int seasonNumber) { var path = series.Path; diff --git a/src/UI/Rename/RenamePreviewFormatView.js b/src/UI/Rename/RenamePreviewFormatView.js index 5796e6748..141b42f8b 100644 --- a/src/UI/Rename/RenamePreviewFormatView.js +++ b/src/UI/Rename/RenamePreviewFormatView.js @@ -9,6 +9,7 @@ module.exports = Marionette.ItemView.extend({ //var type = this.model.get('seriesType'); return { rename : this.naming.get('renameEpisodes'), + folderFormat: this.naming.get('movieFolderFormat'), format : this.naming.get('standardMovieFormat') }; }, diff --git a/src/UI/Rename/RenamePreviewFormatViewTemplate.hbs b/src/UI/Rename/RenamePreviewFormatViewTemplate.hbs index 77297f56b..99a1f6462 100644 --- a/src/UI/Rename/RenamePreviewFormatViewTemplate.hbs +++ b/src/UI/Rename/RenamePreviewFormatViewTemplate.hbs @@ -1,3 +1,4 @@ {{#if rename}} +Folder Naming pattern: {{folderFormat}}
Naming pattern: {{format}} {{/if}} diff --git a/src/UI/Rename/RenamePreviewLayoutTemplate.hbs b/src/UI/Rename/RenamePreviewLayoutTemplate.hbs index a3aa41d51..ee1f2571c 100644 --- a/src/UI/Rename/RenamePreviewLayoutTemplate.hbs +++ b/src/UI/Rename/RenamePreviewLayoutTemplate.hbs @@ -9,7 +9,7 @@
diff --git a/src/UI/Cells/MovieListTitleCell.js b/src/UI/Cells/MovieListTitleCell.js new file mode 100644 index 000000000..6d9142131 --- /dev/null +++ b/src/UI/Cells/MovieListTitleCell.js @@ -0,0 +1,7 @@ +var TemplatedCell = require('./TemplatedCell'); + +module.exports = TemplatedCell.extend({ + className : 'series-title-cell', + template : 'Cells/MovieListTitleTemplate', + +}); diff --git a/src/UI/Cells/MovieListTitleTemplate.hbs b/src/UI/Cells/MovieListTitleTemplate.hbs new file mode 100644 index 000000000..6c4bb964b --- /dev/null +++ b/src/UI/Cells/MovieListTitleTemplate.hbs @@ -0,0 +1 @@ +{{title}} From 6efd63a292bb9697978271f447f5abe1b560df7a Mon Sep 17 00:00:00 2001 From: vertigo235 Date: Thu, 26 Jan 2017 14:13:27 -0600 Subject: [PATCH 50/83] Remove mofilefile id for now (#464) --- src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs index d89f3aa42..a5ecbeb74 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs @@ -57,7 +57,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Radarr_Movie_Id", movie.Id.ToString()); environmentVariables.Add("Radarr_Movie_Title", movie.Title); environmentVariables.Add("Radarr_Movie_ImdbId", movie.ImdbId.ToString()); - environmentVariables.Add("Radarr_MovieFile_Id", movieFile.Id.ToString()); + //environmentVariables.Add("Radarr_MovieFile_Id", movieFile.Id.ToString()); TODO: Debug issue with moviefile ID and add back later environmentVariables.Add("Radarr_MovieFile_RelativePath", movieFile.RelativePath); environmentVariables.Add("Radarr_MovieFile_Path", Path.Combine(movie.Path, movieFile.RelativePath)); environmentVariables.Add("Radarr_MovieFile_Quality", movieFile.Quality.Quality.Name); From 487c5e22ce334b0f1bea38e4e3006fc5e077e6c0 Mon Sep 17 00:00:00 2001 From: vertigo235 Date: Thu, 26 Jan 2017 14:46:55 -0600 Subject: [PATCH 51/83] Moviefile, what movie file? (#466) Send the moviefile object. --- src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs | 2 +- src/NzbDrone.Core/Notifications/NotificationService.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs index a5ecbeb74..d89f3aa42 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs @@ -57,7 +57,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Radarr_Movie_Id", movie.Id.ToString()); environmentVariables.Add("Radarr_Movie_Title", movie.Title); environmentVariables.Add("Radarr_Movie_ImdbId", movie.ImdbId.ToString()); - //environmentVariables.Add("Radarr_MovieFile_Id", movieFile.Id.ToString()); TODO: Debug issue with moviefile ID and add back later + environmentVariables.Add("Radarr_MovieFile_Id", movieFile.Id.ToString()); environmentVariables.Add("Radarr_MovieFile_RelativePath", movieFile.RelativePath); environmentVariables.Add("Radarr_MovieFile_Path", Path.Combine(movie.Path, movieFile.RelativePath)); environmentVariables.Add("Radarr_MovieFile_Quality", movieFile.Quality.Quality.Name); diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs index eefef7653..53c237159 100644 --- a/src/NzbDrone.Core/Notifications/NotificationService.cs +++ b/src/NzbDrone.Core/Notifications/NotificationService.cs @@ -213,7 +213,9 @@ namespace NzbDrone.Core.Notifications downloadMessage.Message = GetMessage(message.Movie.Movie, message.Movie.Quality); downloadMessage.Series = null; downloadMessage.EpisodeFile = null; + downloadMessage.MovieFile = message.MovieFile; downloadMessage.Movie = message.Movie.Movie; + downloadMessage.OldFiles = null; downloadMessage.OldMovieFiles = message.OldFiles; downloadMessage.SourcePath = message.Movie.Path; From dbe5946d10cee158bfa7c07d05d1be4882c35d99 Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Thu, 26 Jan 2017 17:31:27 -0500 Subject: [PATCH 52/83] Only wanted is default for CP --- src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs index d299e1b92..98ab00bff 100644 --- a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs +++ b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs @@ -14,7 +14,7 @@ namespace NzbDrone.Core.NetImport.CouchPotato Link = "http://localhost"; Port = 5050; UrlBase = ""; - OnlyActive = false; + OnlyActive = true; } [FieldDefinition(0, Label = "CouchPotato URL", HelpText = "Link to your CoouchPootato.")] From 50a5a2de9ba191b49eaed55652ccbf8c20cd02f6 Mon Sep 17 00:00:00 2001 From: Tim Turner Date: Thu, 26 Jan 2017 17:58:19 -0500 Subject: [PATCH 53/83] Movie reference properly updates UI now Still need to fix the 'Files' tab to be updated --- src/NzbDrone.Core/Extras/ExtraService.cs | 11 +++++++++++ .../MediaFiles/MovieFileMovingService.cs | 9 +++++++-- .../MediaFiles/RenameMovieFileService.cs | 3 ++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Extras/ExtraService.cs b/src/NzbDrone.Core/Extras/ExtraService.cs index 012e8f921..85889ce44 100644 --- a/src/NzbDrone.Core/Extras/ExtraService.cs +++ b/src/NzbDrone.Core/Extras/ExtraService.cs @@ -123,6 +123,17 @@ namespace NzbDrone.Core.Extras // } //} + // TODO + public void Handle(MovieFolderCreatedEvent message) + { + var movie = message.Movie; + + foreach(var extraFileManager in _extraFileManagers) + { + //extraFileManager.CreateAfterMovieImport(movie, message.MovieFolder); + } + } + public void Handle(EpisodeFolderCreatedEvent message) { var series = message.Series; diff --git a/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs index 3f057a4d0..ad18e9c60 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs @@ -102,6 +102,8 @@ namespace NzbDrone.Core.MediaFiles Ensure.That(movie,() => movie).IsNotNull(); Ensure.That(destinationFilePath, () => destinationFilePath).IsValidPath(); + + var movieFilePath = movieFile.Path ?? Path.Combine(movie.Path, movieFile.RelativePath); if (!_diskProvider.FileExists(movieFilePath)) @@ -116,7 +118,10 @@ namespace NzbDrone.Core.MediaFiles _diskTransferService.TransferFile(movieFilePath, destinationFilePath, mode); - movieFile.RelativePath = movie.Path.GetRelativePathWithoutChildCheck(destinationFilePath); + var newMoviePath = new OsPath(destinationFilePath).Directory.FullPath.TrimEnd(Path.DirectorySeparatorChar); + movie.Path = newMoviePath; + + movieFile.RelativePath = movie.Path.GetRelativePath(destinationFilePath); _updateMovieFileService.ChangeFileDateForFile(movieFile, movie); @@ -157,7 +162,7 @@ namespace NzbDrone.Core.MediaFiles if (!_diskProvider.FolderExists(movieFolder)) { CreateFolder(movieFolder); - newEvent.SeriesFolder = movieFolder; + newEvent.MovieFolder = movieFolder; changed = true; } diff --git a/src/NzbDrone.Core/MediaFiles/RenameMovieFileService.cs b/src/NzbDrone.Core/MediaFiles/RenameMovieFileService.cs index 38036cf5a..4742922c4 100644 --- a/src/NzbDrone.Core/MediaFiles/RenameMovieFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/RenameMovieFileService.cs @@ -95,6 +95,7 @@ namespace NzbDrone.Core.MediaFiles _movieFileMover.MoveMovieFile(movieFile, movie); _mediaFileService.Update(movieFile); + _movieService.UpdateMovie(movie); renamed.Add(movieFile); _logger.Debug("Renamed movie file: {0}", movieFile); @@ -123,7 +124,7 @@ namespace NzbDrone.Core.MediaFiles public void Execute(RenameMovieCommand message) { - _logger.Debug("Renaming all files for selected movie"); + _logger.Debug("Renaming all files for selected movies"); var moviesToRename = _movieService.GetMovies(message.MovieIds); foreach(var movie in moviesToRename) From 7bb319b6d6fbf590117fc81ce4857f89c3e6858e Mon Sep 17 00:00:00 2001 From: Tim Turner Date: Thu, 26 Jan 2017 19:16:19 -0500 Subject: [PATCH 54/83] Update Files tab when movie renamed --- src/UI/Movies/Files/FilesLayout.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/UI/Movies/Files/FilesLayout.js b/src/UI/Movies/Files/FilesLayout.js index 3e6dd2bdd..15b1e32af 100644 --- a/src/UI/Movies/Files/FilesLayout.js +++ b/src/UI/Movies/Files/FilesLayout.js @@ -74,6 +74,19 @@ module.exports = Marionette.Layout.extend({ var file = movie.model.get("movieFile"); this.filesCollection.add(file); //this.listenTo(this.releaseCollection, 'sync', this._showSearchResults); + + this.listenTo(this.model, 'change', function(model, options) { + if (options && options.changeSource === 'signalr') { + this._refresh(movie); + } + }); + }, + + _refresh : function(movie) { + this.filesCollection = new FilesCollection(); + var file = movie.model.get("movieFile"); + this.filesCollection.add(file); + this.onShow(); }, onShow : function() { From a8eec60c9d8687c118fc825e92d3ec39c9312403 Mon Sep 17 00:00:00 2001 From: Tim Turner Date: Thu, 26 Jan 2017 19:57:31 -0500 Subject: [PATCH 55/83] Remove old folder and all contents --- src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs index ad18e9c60..c5a751420 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs @@ -102,8 +102,6 @@ namespace NzbDrone.Core.MediaFiles Ensure.That(movie,() => movie).IsNotNull(); Ensure.That(destinationFilePath, () => destinationFilePath).IsValidPath(); - - var movieFilePath = movieFile.Path ?? Path.Combine(movie.Path, movieFile.RelativePath); if (!_diskProvider.FileExists(movieFilePath)) @@ -118,6 +116,9 @@ namespace NzbDrone.Core.MediaFiles _diskTransferService.TransferFile(movieFilePath, destinationFilePath, mode); + var oldMoviePath = new OsPath(movieFilePath).Directory.FullPath.TrimEnd(Path.DirectorySeparatorChar); + + var newMoviePath = new OsPath(destinationFilePath).Directory.FullPath.TrimEnd(Path.DirectorySeparatorChar); movie.Path = newMoviePath; @@ -137,6 +138,8 @@ namespace NzbDrone.Core.MediaFiles _mediaFileAttributeService.SetFilePermissions(destinationFilePath); + _diskProvider.DeleteFolder(oldMoviePath, true); + return movieFile; } From 00541e6cc1881a57c47d12fd33e071c8a1efde88 Mon Sep 17 00:00:00 2001 From: vertigo235 Date: Fri, 27 Jan 2017 12:01:18 -0600 Subject: [PATCH 56/83] More Notification Updates (#482) * Custom Script: Add Movie_Path * Add Emby Update Support * Notifications: Maybe add Kodi / XBMC Update Support --- .../CustomScript/CustomScript.cs | 1 + .../MediaBrowser/MediaBrowser.cs | 8 ++- .../MediaBrowser/MediaBrowserProxy.cs | 10 ++++ .../MediaBrowser/MediaBrowserService.cs | 8 +++ .../Notifications/Xbmc/HttpApiProvider.cs | 49 +++++++++++++++++++ .../Notifications/Xbmc/IApiProvider.cs | 1 + .../Notifications/Xbmc/JsonApiProvider.cs | 38 +++++++++++++- src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs | 26 +++++++++- .../Notifications/Xbmc/XbmcService.cs | 7 +++ 9 files changed, 143 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs index d89f3aa42..8ff95a672 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs @@ -56,6 +56,7 @@ namespace NzbDrone.Core.Notifications.CustomScript environmentVariables.Add("Radarr_EventType", "Download"); environmentVariables.Add("Radarr_Movie_Id", movie.Id.ToString()); environmentVariables.Add("Radarr_Movie_Title", movie.Title); + environmentVariables.Add("Radarr_Movie_Path", movie.Path); environmentVariables.Add("Radarr_Movie_ImdbId", movie.ImdbId.ToString()); environmentVariables.Add("Radarr_MovieFile_Id", movieFile.Id.ToString()); environmentVariables.Add("Radarr_MovieFile_RelativePath", movieFile.RelativePath); diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs index b47385736..f293a6ecd 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs @@ -37,14 +37,18 @@ namespace NzbDrone.Core.Notifications.MediaBrowser if (Settings.UpdateLibrary) { - _mediaBrowserService.Update(Settings, message.Series); + _mediaBrowserService.UpdateMovies(Settings, message.Movie); } } public override void OnMovieRename(Movie movie) { + if (Settings.UpdateLibrary) + { + _mediaBrowserService.UpdateMovies(Settings, movie); + } } - + public override void OnRename(Series series) { if (Settings.UpdateLibrary) diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs index 251488d87..dafccb99a 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs @@ -40,6 +40,16 @@ namespace NzbDrone.Core.Notifications.MediaBrowser ProcessRequest(request, settings); } + + public void UpdateMovies(MediaBrowserSettings settings, string imdbid) + { + var path = string.Format("/Library/Movies/Updated?ImdbId={0}", imdbid); + var request = BuildRequest(path, settings); + request.Headers.Add("Content-Length", "0"); + + ProcessRequest(request, settings); + } + private string ProcessRequest(HttpRequest request, MediaBrowserSettings settings) { request.Headers.Add("X-MediaBrowser-Token", settings.ApiKey); diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs index 748d2a67f..9c76145cd 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserService.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Core.Notifications.MediaBrowser { void Notify(MediaBrowserSettings settings, string title, string message); void Update(MediaBrowserSettings settings, Series series); + void UpdateMovies(MediaBrowserSettings settings, Movie movie); ValidationFailure Test(MediaBrowserSettings settings); } @@ -35,6 +36,13 @@ namespace NzbDrone.Core.Notifications.MediaBrowser _proxy.Update(settings, series.TvdbId); } + + public void UpdateMovies(MediaBrowserSettings settings, Movie movie) + { + _proxy.UpdateMovies(settings, movie.ImdbId); + } + + public ValidationFailure Test(MediaBrowserSettings settings) { try diff --git a/src/NzbDrone.Core/Notifications/Xbmc/HttpApiProvider.cs b/src/NzbDrone.Core/Notifications/Xbmc/HttpApiProvider.cs index 76f2bc91f..528728cdf 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/HttpApiProvider.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/HttpApiProvider.cs @@ -51,6 +51,24 @@ namespace NzbDrone.Core.Notifications.Xbmc UpdateLibrary(settings, series); } + public void UpdateMovie(XbmcSettings settings, Movie movie) + { + if (!settings.AlwaysUpdate) + { + _logger.Debug("Determining if there are any active players on XBMC host: {0}", settings.Address); + var activePlayers = GetActivePlayers(settings); + + if (activePlayers.Any(a => a.Type.Equals("video"))) + { + _logger.Debug("Video is currently playing, skipping library update"); + return; + } + } + + UpdateMovieLibrary(settings, movie); + } + + public void Clean(XbmcSettings settings) { const string cleanVideoLibrary = "CleanLibrary(video)"; @@ -167,6 +185,37 @@ namespace NzbDrone.Core.Notifications.Xbmc } } + private void UpdateMovieLibrary(XbmcSettings settings, Movie movie) + { + try + { + //_logger.Debug("Sending Update DB Request to XBMC Host: {0}", settings.Address); + //var xbmcSeriesPath = GetSeriesPath(settings, series); + + ////If the path is found update it, else update the whole library + //if (!string.IsNullOrEmpty(xbmcSeriesPath)) + //{ + // _logger.Debug("Updating series [{0}] on XBMC host: {1}", series, settings.Address); + // var command = BuildExecBuiltInCommand(string.Format("UpdateLibrary(video,{0})", xbmcSeriesPath)); + // SendCommand(settings, command); + //} + + //else + //{ + //Update the entire library + _logger.Debug("Series [{0}] doesn't exist on XBMC host: {1}, Updating Entire Library", movie, settings.Address); + var command = BuildExecBuiltInCommand("UpdateLibrary(video)"); + SendCommand(settings, command); + //} + } + + catch (Exception ex) + { + _logger.Debug(ex, ex.Message); + } + } + + private string SendCommand(XbmcSettings settings, string command) { var url = string.Format("http://{0}/xbmcCmds/xbmcHttp?command={1}", settings.Address, command); diff --git a/src/NzbDrone.Core/Notifications/Xbmc/IApiProvider.cs b/src/NzbDrone.Core/Notifications/Xbmc/IApiProvider.cs index bf250edc3..94bf80862 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/IApiProvider.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/IApiProvider.cs @@ -7,6 +7,7 @@ namespace NzbDrone.Core.Notifications.Xbmc { void Notify(XbmcSettings settings, string title, string message); void Update(XbmcSettings settings, Series series); + void UpdateMovie(XbmcSettings settings, Movie movie); void Clean(XbmcSettings settings); bool CanHandle(XbmcVersion version); } diff --git a/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs b/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs index 1a0674908..378bb0774 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs @@ -44,7 +44,25 @@ namespace NzbDrone.Core.Notifications.Xbmc UpdateLibrary(settings, series); } - + + public void UpdateMovie(XbmcSettings settings, Movie movie) + { + if (!settings.AlwaysUpdate) + { + _logger.Debug("Determining if there are any active players on XBMC host: {0}", settings.Address); + var activePlayers = _proxy.GetActivePlayers(settings); + + if (activePlayers.Any(a => a.Type.Equals("video"))) + { + _logger.Debug("Video is currently playing, skipping library update"); + return; + } + } + + UpdateMovieLibrary(settings, movie); + } + + public void Clean(XbmcSettings settings) { _proxy.CleanLibrary(settings); @@ -108,5 +126,23 @@ namespace NzbDrone.Core.Notifications.Xbmc _logger.Debug(ex, ex.Message); } } + + private void UpdateMovieLibrary(XbmcSettings settings, Movie movie) + { + try + { + var response = _proxy.UpdateLibrary(settings, null); + + if (!response.Equals("OK", StringComparison.InvariantCultureIgnoreCase)) + { + _logger.Debug("Failed to update library for: {0}", settings.Address); + } + } + + catch (Exception ex) + { + _logger.Debug(ex, ex.Message); + } + } } } diff --git a/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs b/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs index 4939fbe3d..60469a9a9 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs @@ -33,13 +33,14 @@ namespace NzbDrone.Core.Notifications.Xbmc const string header = "Radarr - Downloaded"; Notify(Settings, header, message.Message); - UpdateAndClean(message.Series, message.OldFiles.Any()); + UpdateAndCleanMovie(message.Movie, message.OldFiles.Any()); } public override void OnMovieRename(Movie movie) { + UpdateAndCleanMovie(movie); } - + public override void OnRename(Series series) { UpdateAndClean(series); @@ -92,5 +93,26 @@ namespace NzbDrone.Core.Notifications.Xbmc _logger.Debug(ex, logMessage); } } + + private void UpdateAndCleanMovie(Movie movie, bool clean = true) + { + try + { + if (Settings.UpdateLibrary) + { + _xbmcService.UpdateMovie(Settings, movie); + } + + if (clean && Settings.CleanLibrary) + { + _xbmcService.Clean(Settings); + } + } + catch (SocketException ex) + { + var logMessage = string.Format("Unable to connect to XBMC Host: {0}:{1}", Settings.Host, Settings.Port); + _logger.Debug(ex, logMessage); + } + } } } diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs index 84127f69f..85dbc99c5 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.Notifications.Xbmc { void Notify(XbmcSettings settings, string title, string message); void Update(XbmcSettings settings, Series series); + void UpdateMovie(XbmcSettings settings, Movie movie); void Clean(XbmcSettings settings); ValidationFailure Test(XbmcSettings settings, string message); } @@ -51,6 +52,12 @@ namespace NzbDrone.Core.Notifications.Xbmc provider.Update(settings, series); } + public void UpdateMovie(XbmcSettings settings, Movie movie) + { + var provider = GetApiProvider(settings); + provider.UpdateMovie(settings, movie); + } + public void Clean(XbmcSettings settings) { var provider = GetApiProvider(settings); From dd8af0ad8c0d35d661f8dc7d66b847b60f22dc5f Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Fri, 27 Jan 2017 19:36:25 +0100 Subject: [PATCH 57/83] Manual Import works now! Also fixed a few bugs. --- .../NetImport/ListImportModule.cs | 34 +++ src/NzbDrone.Api/NzbDrone.Api.csproj | 1 + .../Series/FetchMovieListModule.cs | 6 +- .../SkyHook/Resource/TMDBResources.cs | 2 + .../MetadataSource/SkyHook/SkyHookProxy.cs | 19 +- .../NetImport/NetImportSearchService.cs | 6 - src/NzbDrone.Core/Tv/MovieService.cs | 30 ++ src/UI/AddMovies/List/AddFromListView.js | 26 +- .../List/AddFromListViewTemplate.hbs | 2 +- src/UI/Movies/MoviesCollection.js | 259 ++++++++++-------- 10 files changed, 247 insertions(+), 138 deletions(-) create mode 100644 src/NzbDrone.Api/NetImport/ListImportModule.cs diff --git a/src/NzbDrone.Api/NetImport/ListImportModule.cs b/src/NzbDrone.Api/NetImport/ListImportModule.cs new file mode 100644 index 000000000..f1d81aefd --- /dev/null +++ b/src/NzbDrone.Api/NetImport/ListImportModule.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using Nancy; +using Nancy.Extensions; +using NzbDrone.Api.Extensions; +using NzbDrone.Api.Movie; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Api.NetImport +{ + public class ListImportModule : NzbDroneApiModule + { + private readonly IMovieService _movieService; + private readonly ISearchForNewMovie _movieSearch; + + public ListImportModule(IMovieService movieService, ISearchForNewMovie movieSearch) + : base("/movie/import") + { + _movieService = movieService; + _movieSearch = movieSearch; + Put["/"] = Movie => SaveAll(); + } + + private Response SaveAll() + { + var resources = Request.Body.FromJson>(); + + var Movies = resources.Select(MovieResource => _movieSearch.MapMovieToTmdbMovie(MovieResource.ToModel())).Where(m => m != null).DistinctBy(m => m.TmdbId).ToList(); + + return _movieService.AddMovies(Movies).ToResource().AsResponse(HttpStatusCode.Accepted); + } + } +} diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 097933cdc..1f0542788 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -123,6 +123,7 @@ + diff --git a/src/NzbDrone.Api/Series/FetchMovieListModule.cs b/src/NzbDrone.Api/Series/FetchMovieListModule.cs index 52475d0cb..871ebd7bc 100644 --- a/src/NzbDrone.Api/Series/FetchMovieListModule.cs +++ b/src/NzbDrone.Api/Series/FetchMovieListModule.cs @@ -28,7 +28,7 @@ namespace NzbDrone.Api.Movie List realResults = new List(); - foreach (var movie in results) + /*foreach (var movie in results) { var mapped = _movieSearch.MapMovieToTmdbMovie(movie); @@ -36,9 +36,9 @@ namespace NzbDrone.Api.Movie { realResults.Add(mapped); } - } + }*/ - return MapToResource(realResults).AsResponse(); + return MapToResource(results).AsResponse(); } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TMDBResources.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TMDBResources.cs index 469e72776..182992c38 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TMDBResources.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TMDBResources.cs @@ -42,6 +42,8 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource public bool adult { get; set; } public string backdrop_path { get; set; } public Belongs_To_Collection belongs_to_collection { get; set; } + public int? status_code { get; set; } + public string status_message { get; set; } public int budget { get; set; } public Genre[] genres { get; set; } public string homepage { get; set; } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index b6151d46b..70d3dda89 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -84,6 +84,18 @@ namespace NzbDrone.Core.MetadataSource.SkyHook var resource = response.Resource; + if (resource.status_message != null) + { + if (resource.status_code == 34) + { + _logger.Warn("Movie with TmdbId {0} could not be found. This is probably the case when the movie was deleted from TMDB.", TmdbId); + return null; + } + + _logger.Warn(resource.status_message); + return null; + } + var movie = new Movie(); movie.TmdbId = TmdbId; @@ -567,10 +579,9 @@ namespace NzbDrone.Core.MetadataSource.SkyHook Movie newMovie = movie; if (movie.TmdbId > 0) { - return newMovie; + newMovie = GetMovieInfo(movie.TmdbId); } - - if (movie.ImdbId.IsNotNullOrWhiteSpace()) + else if (movie.ImdbId.IsNotNullOrWhiteSpace()) { newMovie = GetMovieInfo(movie.ImdbId); } @@ -586,7 +597,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook if (newMovie == null) { - _logger.Warn("Couldn't map movie {0} to a movie on The Movie DB."); + _logger.Warn("Couldn't map movie {0} to a movie on The Movie DB. It will not be added :(", movie.Title); return null; } diff --git a/src/NzbDrone.Core/NetImport/NetImportSearchService.cs b/src/NzbDrone.Core/NetImport/NetImportSearchService.cs index 49500cd4c..e119d8b36 100644 --- a/src/NzbDrone.Core/NetImport/NetImportSearchService.cs +++ b/src/NzbDrone.Core/NetImport/NetImportSearchService.cs @@ -25,7 +25,6 @@ namespace NzbDrone.Core.NetImport private readonly IMovieService _movieService; private readonly ISearchForNewMovie _movieSearch; private readonly IRootFolderService _rootFolder; - private string defaultRootFolder; public NetImportSearchService(INetImportFactory netImportFactory, IMovieService movieService, ISearchForNewMovie movieSearch, IRootFolderService rootFolder, Logger logger) @@ -34,11 +33,6 @@ namespace NzbDrone.Core.NetImport _movieService = movieService; _movieSearch = movieSearch; _rootFolder = rootFolder; - var folder = _rootFolder.All().FirstOrDefault(); - if (folder != null) - { - defaultRootFolder = folder.Path; - } _logger = logger; } diff --git a/src/NzbDrone.Core/Tv/MovieService.cs b/src/NzbDrone.Core/Tv/MovieService.cs index 03ba48e4f..9c33e1ecd 100644 --- a/src/NzbDrone.Core/Tv/MovieService.cs +++ b/src/NzbDrone.Core/Tv/MovieService.cs @@ -21,6 +21,7 @@ namespace NzbDrone.Core.Tv Movie GetMovie(int movieId); List GetMovies(IEnumerable movieIds); Movie AddMovie(Movie newMovie); + List AddMovies(List newMovies); Movie FindByImdbId(string imdbid); Movie FindByTitle(string title); Movie FindByTitle(string title, int year); @@ -92,6 +93,35 @@ namespace NzbDrone.Core.Tv return newMovie; } + public List AddMovies(List newMovies) + { + _logger.Debug("Adding {0} movies", newMovies.Count); + + newMovies.ForEach(m => Ensure.That(m, () => m).IsNotNull()); + + newMovies.ForEach(m => + { + if (string.IsNullOrWhiteSpace(m.Path)) + { + var folderName = _fileNameBuilder.GetMovieFolder(m); + m.Path = Path.Combine(m.RootFolderPath, folderName); + } + + m.CleanTitle = m.Title.CleanSeriesTitle(); + m.SortTitle = MovieTitleNormalizer.Normalize(m.Title, m.TmdbId); + m.Added = DateTime.UtcNow; + }); + + _movieRepository.InsertMany(newMovies); + + newMovies.ForEach(m => + { + _eventAggregator.PublishEvent(new MovieAddedEvent(m)); + }); + + return newMovies; + } + public Movie FindByTitle(string title) { return _movieRepository.FindByTitle(title.CleanSeriesTitle()); diff --git a/src/UI/AddMovies/List/AddFromListView.js b/src/UI/AddMovies/List/AddFromListView.js index 05d60dcad..d2605c74f 100644 --- a/src/UI/AddMovies/List/AddFromListView.js +++ b/src/UI/AddMovies/List/AddFromListView.js @@ -22,7 +22,8 @@ var MovieStatusCell = require('../../Cells/MovieStatusCell'); var MovieDownloadStatusCell = require('../../Cells/MovieDownloadStatusCell'); var DownloadedQualityCell = require('../../Cells/DownloadedQualityCell'); var MoviesCollection = require('../../Movies/MoviesCollection'); - +var Messenger = require('../../Shared/Messenger'); +require('jquery.dotdotdot'); var SchemaModal = require('../../Settings/NetImport/Add/NetImportSchemaModal'); module.exports = Marionette.Layout.extend({ @@ -35,7 +36,7 @@ module.exports = Marionette.Layout.extend({ ui : { moviesSearch : '.x-movies-search', listSelection : ".x-list-selection", - + importSelected : ".x-import-selected" }, columns : [ @@ -185,9 +186,24 @@ module.exports = Marionette.Layout.extend({ _importSelected : function() { var selected = this.importGrid.getSelectedModels(); console.log(selected); - _.each(selected, function(elem){ - elem.save(); - }) + var promise = MoviesCollection.importFromList(selected); + this.ui.importSelected.spinForPromise(promise); + this.ui.importSelected.addClass('disabled'); + + Messenger.show({ + message : "Importing {0} movies. This can take multiple minutes depending on how many movies should be imported. Don't close this browser window until it is finished!".format(selected.length), + hideOnNavigate : false, + hideAfter : 30, + type : "error" + }); + + promise.done(function() { + Messenger.show({ + message : "Imported movies from list.", + hideAfter : 8, + hideOnNavigate : true + }); + }); /*for (m in selected) { debugger; m.save() diff --git a/src/UI/AddMovies/List/AddFromListViewTemplate.hbs b/src/UI/AddMovies/List/AddFromListViewTemplate.hbs index cad2b19bc..f63c95573 100644 --- a/src/UI/AddMovies/List/AddFromListViewTemplate.hbs +++ b/src/UI/AddMovies/List/AddFromListViewTemplate.hbs @@ -9,7 +9,7 @@
- +
diff --git a/src/UI/Movies/MoviesCollection.js b/src/UI/Movies/MoviesCollection.js index 193f47ef6..779fb4a0d 100644 --- a/src/UI/Movies/MoviesCollection.js +++ b/src/UI/Movies/MoviesCollection.js @@ -10,142 +10,163 @@ var moment = require('moment'); require('../Mixins/backbone.signalr.mixin'); var Collection = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/movie', - model : MovieModel, - tableName : 'movie', + url : window.NzbDrone.ApiRoot + '/movie', + model : MovieModel, + tableName : 'movie', - state : { - sortKey : 'sortTitle', - order : 1, - pageSize : 100000, - secondarySortKey : 'sortTitle', - secondarySortOrder : -1 - }, + state : { + sortKey : 'sortTitle', + order : 1, + pageSize : 100000, + secondarySortKey : 'sortTitle', + secondarySortOrder : -1 + }, - mode : 'client', + mode : 'client', - save : function() { - var self = this; + save : function() { + var self = this; - var proxy = _.extend(new Backbone.Model(), { - id : '', + var proxy = _.extend(new Backbone.Model(), { + id : '', - url : self.url + '/editor', + url : self.url + '/editor', - toJSON : function() { - return self.filter(function(model) { - return model.edited; - }); - } - }); + toJSON : function() { + return self.filter(function(model) { + return model.edited; + }); + } + }); - this.listenTo(proxy, 'sync', function(proxyModel, models) { - this.add(models, { merge : true }); - this.trigger('save', this); - }); + this.listenTo(proxy, 'sync', function(proxyModel, models) { + this.add(models, { merge : true }); + this.trigger('save', this); + }); - return proxy.save(); - }, + return proxy.save(); + }, - filterModes : { - 'all' : [ - null, - null - ], - 'continuing' : [ - 'status', - 'continuing' - ], - 'ended' : [ - 'status', - 'ended' - ], - 'monitored' : [ - 'monitored', - true - ], - 'missing' : [ - 'downloaded', - false - ] - }, + importFromList : function(models) { + var self = this; - sortMappings : { - title : { - sortKey : 'sortTitle' - }, - statusWeight : { - sortValue : function(model, attr) { - if (model.getStatus() == "released") { - return 1; - } - if (model.getStatus() == "inCinemas") { - return 0; - } - return -1; - } - }, - downloadedQuality : { - sortValue : function(model, attr) { - if (model.get("movieFile")) { - return 1000-model.get("movieFile").quality.quality.id; - } + var proxy = _.extend(new Backbone.Model(), { + id : "", - return -1; - } - }, - nextAiring : { - sortValue : function(model, attr, order) { - var nextAiring = model.get(attr); + url : self.url + "/import", - if (nextAiring) { - return moment(nextAiring).unix(); - } + toJSON : function() { + return models; + } + }); - if (order === 1) { - return 0; - } + this.listenTo(proxy, "sync", function(proxyModel, models) { + this.add(models, { merge : true}); + this.trigger("save", this); + }); - return Number.MAX_VALUE; - } - }, - status: { - sortValue : function(model, attr) { - debugger; - if (model.get("downloaded")) { - return -1; - } - return 0; - } - }, - percentOfEpisodes : { - sortValue : function(model, attr) { - var percentOfEpisodes = model.get(attr); - var episodeCount = model.get('episodeCount'); + return proxy.save(); + }, - return percentOfEpisodes + episodeCount / 1000000; - } - }, - inCinemas : { + filterModes : { + 'all' : [ + null, + null + ], + 'continuing' : [ + 'status', + 'continuing' + ], + 'ended' : [ + 'status', + 'ended' + ], + 'monitored' : [ + 'monitored', + true + ], + 'missing' : [ + 'downloaded', + false + ] + }, - sortValue : function(model, attr) { - var monthNames = ["January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December" - ]; - if (model.get("inCinemas")) { - return model.get("inCinemas"); - } - return "2100-01-01"; - } - }, - path : { - sortValue : function(model) { - var path = model.get('path'); + sortMappings : { + title : { + sortKey : 'sortTitle' + }, + statusWeight : { + sortValue : function(model, attr) { + if (model.getStatus() == "released") { + return 1; + } + if (model.getStatus() == "inCinemas") { + return 0; + } + return -1; + } + }, + downloadedQuality : { + sortValue : function(model, attr) { + if (model.get("movieFile")) { + return 1000-model.get("movieFile").quality.quality.id; + } - return path.toLowerCase(); - } - } - } + return -1; + } + }, + nextAiring : { + sortValue : function(model, attr, order) { + var nextAiring = model.get(attr); + + if (nextAiring) { + return moment(nextAiring).unix(); + } + + if (order === 1) { + return 0; + } + + return Number.MAX_VALUE; + } + }, + status: { + sortValue : function(model, attr) { + debugger; + if (model.get("downloaded")) { + return -1; + } + return 0; + } + }, + percentOfEpisodes : { + sortValue : function(model, attr) { + var percentOfEpisodes = model.get(attr); + var episodeCount = model.get('episodeCount'); + + return percentOfEpisodes + episodeCount / 1000000; + } + }, + inCinemas : { + + sortValue : function(model, attr) { + var monthNames = ["January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + ]; + if (model.get("inCinemas")) { + return model.get("inCinemas"); + } + return "2100-01-01"; + } + }, + path : { + sortValue : function(model) { + var path = model.get('path'); + + return path.toLowerCase(); + } + } + } }); Collection = AsFilteredCollection.call(Collection); From cbc70a8ff3fa6673504a4798556d604b3f108209 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Fri, 27 Jan 2017 20:01:28 +0100 Subject: [PATCH 58/83] Added option to specify preferred words in quality profile. (#462) * UI Done * Should theoretically work. * Preferred tags works now correctly :) --- src/NzbDrone.Api/Profiles/ProfileResource.cs | 3 + .../124_add_preferred_tags_to_profile.cs | 20 +++++ .../DownloadDecisionComparer.cs | 21 ++++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + src/NzbDrone.Core/Profiles/Profile.cs | 1 + .../Settings/Profile/Edit/EditProfileView.js | 36 ++++++--- .../Profile/Edit/EditProfileViewTemplate.hbs | 74 +++++++++++-------- src/UI/Settings/Profile/ProfileView.js | 42 ++++++----- .../Settings/Profile/ProfileViewTemplate.hbs | 20 ++--- 9 files changed, 146 insertions(+), 72 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/124_add_preferred_tags_to_profile.cs diff --git a/src/NzbDrone.Api/Profiles/ProfileResource.cs b/src/NzbDrone.Api/Profiles/ProfileResource.cs index ee02bcb32..65e560b59 100644 --- a/src/NzbDrone.Api/Profiles/ProfileResource.cs +++ b/src/NzbDrone.Api/Profiles/ProfileResource.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Api.Profiles { public string Name { get; set; } public Quality Cutoff { get; set; } + public string PreferredTags { get; set; } public List Items { get; set; } public Language Language { get; set; } } @@ -33,6 +34,7 @@ namespace NzbDrone.Api.Profiles Name = model.Name, Cutoff = model.Cutoff, + PreferredTags = model.PreferredTags != null ? string.Join(",", model.PreferredTags) : "", Items = model.Items.ConvertAll(ToResource), Language = model.Language }; @@ -59,6 +61,7 @@ namespace NzbDrone.Api.Profiles Name = resource.Name, Cutoff = (Quality)resource.Cutoff.Id, + PreferredTags = resource.PreferredTags.Split(',').ToList(), Items = resource.Items.ConvertAll(ToModel), Language = resource.Language }; diff --git a/src/NzbDrone.Core/Datastore/Migration/124_add_preferred_tags_to_profile.cs b/src/NzbDrone.Core/Datastore/Migration/124_add_preferred_tags_to_profile.cs new file mode 100644 index 000000000..531af0eb6 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/124_add_preferred_tags_to_profile.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; +using System.Data; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(124)] + public class add_preferred_tags_to_profile : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Profiles").AddColumn("PreferredTags").AsString().Nullable(); + } + + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs index aba427cbf..fdb45f6d4 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs @@ -23,6 +23,7 @@ namespace NzbDrone.Core.DecisionEngine var comparers = new List { CompareQuality, + ComparePreferredWords, CompareProtocol, ComparePeersIfTorrent, CompareAgeIfUsenet, @@ -65,6 +66,26 @@ namespace NzbDrone.Core.DecisionEngine CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.ParsedEpisodeInfo.Quality.Revision.Version)); } + private int ComparePreferredWords(DownloadDecision x, DownloadDecision y) + { + return CompareBy(x.RemoteMovie, y.RemoteMovie, remoteMovie => + { + var title = remoteMovie.Release.Title; + remoteMovie.Movie.Profile.LazyLoad(); + var preferredWords = remoteMovie.Movie.Profile.Value.PreferredTags; + + if (preferredWords == null) + { + return 0; + } + + var num = preferredWords.AsEnumerable().Count(w => title.ToLower().Contains(w.ToLower())); + + return num; + + }); +; } + private int CompareProtocol(DownloadDecision x, DownloadDecision y) { diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 6f7eee5c0..d84d0401b 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -183,6 +183,7 @@ + diff --git a/src/NzbDrone.Core/Profiles/Profile.cs b/src/NzbDrone.Core/Profiles/Profile.cs index 6215e9474..d25104fb6 100644 --- a/src/NzbDrone.Core/Profiles/Profile.cs +++ b/src/NzbDrone.Core/Profiles/Profile.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Core.Profiles public string Name { get; set; } public Quality Cutoff { get; set; } public List Items { get; set; } + public List PreferredTags { get; set; } public Language Language { get; set; } public Quality LastAllowedQuality() diff --git a/src/UI/Settings/Profile/Edit/EditProfileView.js b/src/UI/Settings/Profile/Edit/EditProfileView.js index 23535d9e6..52459c6c1 100644 --- a/src/UI/Settings/Profile/Edit/EditProfileView.js +++ b/src/UI/Settings/Profile/Edit/EditProfileView.js @@ -4,25 +4,37 @@ var LanguageCollection = require('../Language/LanguageCollection'); var Config = require('../../../Config'); var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); var AsValidatedView = require('../../../Mixins/AsValidatedView'); +require('../../../Mixins/TagInput'); +require('bootstrap'); +require('bootstrap.tagsinput'); var view = Marionette.ItemView.extend({ - template : 'Settings/Profile/Edit/EditProfileViewTemplate', + template : 'Settings/Profile/Edit/EditProfileViewTemplate', - ui : { cutoff : '.x-cutoff' }, + ui : { cutoff : '.x-cutoff', + preferred : '.x-preferred', + }, - templateHelpers : function() { - return { - languages : LanguageCollection.toJSON() - }; - }, + onRender : function() { + this.ui.preferred.tagsinput({ + trimValue : true, + tagClass : 'label label-success' + }); + }, - getCutoff : function() { - var self = this; + templateHelpers : function() { + return { + languages : LanguageCollection.toJSON() + }; + }, - return _.findWhere(_.pluck(this.model.get('items'), 'quality'), { id : parseInt(self.ui.cutoff.val(), 10) }); - } + getCutoff : function() { + var self = this; + + return _.findWhere(_.pluck(this.model.get('items'), 'quality'), { id : parseInt(self.ui.cutoff.val(), 10) }); + } }); AsValidatedView.call(view); -module.exports = AsModelBoundView.call(view); \ No newline at end of file +module.exports = AsModelBoundView.call(view); diff --git a/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs b/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs index c19d10e5c..072a70ed0 100644 --- a/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs +++ b/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs @@ -1,45 +1,59 @@
- + -
- -
+
+ +

- + -
- -
+
+ +
-
- -
+
+ +
+ +
+ + +
+ +
+ +
+ +
+
+ +
- + -
- -
+
+ +
-
- -
+
+ +
diff --git a/src/UI/Settings/Profile/ProfileView.js b/src/UI/Settings/Profile/ProfileView.js index 4241c3f12..10a4a9be3 100644 --- a/src/UI/Settings/Profile/ProfileView.js +++ b/src/UI/Settings/Profile/ProfileView.js @@ -6,30 +6,32 @@ require('./AllowedLabeler'); require('./LanguageLabel'); require('bootstrap'); + var view = Marionette.ItemView.extend({ - template : 'Settings/Profile/ProfileViewTemplate', - tagName : 'li', + template : 'Settings/Profile/ProfileViewTemplate', + tagName : 'li', - ui : { - "progressbar" : '.progress .bar', - "deleteButton" : '.x-delete' - }, + ui : { + "progressbar" : '.progress .bar', + "deleteButton" : '.x-delete', - events : { - 'click' : '_editProfile' - }, + }, - initialize : function() { - this.listenTo(this.model, 'sync', this.render); - }, + events : { + 'click' : '_editProfile' + }, - _editProfile : function() { - var view = new EditProfileView({ - model : this.model, - profileCollection : this.model.collection - }); - AppLayout.modalRegion.show(view); - } + initialize : function() { + this.listenTo(this.model, 'sync', this.render); + }, + + _editProfile : function() { + var view = new EditProfileView({ + model : this.model, + profileCollection : this.model.collection + }); + AppLayout.modalRegion.show(view); + } }); -module.exports = AsModelBoundView.call(view); \ No newline at end of file +module.exports = AsModelBoundView.call(view); diff --git a/src/UI/Settings/Profile/ProfileViewTemplate.hbs b/src/UI/Settings/Profile/ProfileViewTemplate.hbs index 4f5b3eef0..2f827a351 100644 --- a/src/UI/Settings/Profile/ProfileViewTemplate.hbs +++ b/src/UI/Settings/Profile/ProfileViewTemplate.hbs @@ -1,13 +1,13 @@
-
-

-
+
+

+
-
- {{languageLabel}} -
+
+ {{languageLabel}} +
-
    - {{allowedLabeler}} -
-
\ No newline at end of file +
    + {{allowedLabeler}} +
+ From 4be7772d53073b49abfd2d3a784ec50b7c5e1064 Mon Sep 17 00:00:00 2001 From: vertigo235 Date: Fri, 27 Jan 2017 13:34:52 -0600 Subject: [PATCH 59/83] Kodi Update Fix: OldFiles -> OldMovieFiles (#483) --- src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs b/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs index 60469a9a9..890e0516d 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Notifications.Xbmc const string header = "Radarr - Downloaded"; Notify(Settings, header, message.Message); - UpdateAndCleanMovie(message.Movie, message.OldFiles.Any()); + UpdateAndCleanMovie(message.Movie, message.OldMovieFiles.Any()); } public override void OnMovieRename(Movie movie) From b76c54ceac20e4998cc5bfb6b58bc49404987c56 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 26 Jan 2017 22:35:16 -0800 Subject: [PATCH 60/83] Fixed: Proper port validation for download clients and connections --- src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs | 8 ++++---- .../Download/Clients/Deluge/DelugeSettings.cs | 2 +- .../Download/Clients/Hadouken/HadoukenSettings.cs | 2 +- .../Download/Clients/NzbVortex/NzbVortexSettings.cs | 2 +- .../Download/Clients/Nzbget/NzbgetSettings.cs | 2 +- .../Download/Clients/QBittorrent/QBittorrentSettings.cs | 2 +- .../Download/Clients/Sabnzbd/SabnzbdSettings.cs | 2 +- .../Download/Clients/Transmission/TransmissionSettings.cs | 2 +- .../Download/Clients/rTorrent/RTorrentSettings.cs | 4 ++-- .../Download/Clients/uTorrent/UTorrentSettings.cs | 2 +- src/NzbDrone.Core/Notifications/Email/EmailSettings.cs | 2 +- src/NzbDrone.Core/Notifications/Growl/GrowlSettings.cs | 2 +- .../Notifications/Plex/PlexClientSettings.cs | 2 +- .../Notifications/Plex/PlexServerSettings.cs | 2 +- 14 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs b/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs index 0687a1413..0a7acb9e1 100644 --- a/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs +++ b/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs @@ -73,14 +73,14 @@ namespace NzbDrone.Api.ClientSchema if (propertyInfo.PropertyType == typeof(int)) { - var value = Convert.ToInt32(field.Value); - propertyInfo.SetValue(target, value, null); + var value = field.Value.ToString().ParseInt32(); + propertyInfo.SetValue(target, value ?? 0, null); } else if (propertyInfo.PropertyType == typeof(long)) { - var value = Convert.ToInt64(field.Value); - propertyInfo.SetValue(target, value, null); + var value = field.Value.ToString().ParseInt64(); + propertyInfo.SetValue(target, value ?? 0, null); } else if (propertyInfo.PropertyType == typeof(int?)) diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs index a7175dff6..679f422b4 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge public DelugeSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.MovieCategory).Matches("^[-a-z]*$").WithMessage("Allowed characters a-z and -"); } diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs index 5291c9515..f66dbb365 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken public HadoukenSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.Username).NotEmpty() .WithMessage("Username must not be empty."); diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs index 749ef9d04..211ba229d 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex public NzbVortexSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.ApiKey).NotEmpty() .WithMessage("API Key is required"); diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs index aff3f27ff..8b7a0c31d 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget public NzbgetSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.Username).NotEmpty().When(c => !string.IsNullOrWhiteSpace(c.Password)); RuleFor(c => c.Password).NotEmpty().When(c => !string.IsNullOrWhiteSpace(c.Username)); diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs index c02619f2f..b5127293d 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent public QBittorrentSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).InclusiveBetween(0, 65535); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); } } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs index 0c1dc8221..00a8ef6b8 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd public SabnzbdSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.ApiKey).NotEmpty() .WithMessage("API Key is required when username/password are not configured") diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs index 9d0a860ec..aeb8887c4 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs @@ -12,7 +12,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission public TransmissionSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.UrlBase).ValidUrlBase(); diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs index 4965c9a78..b978ec721 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs @@ -10,10 +10,10 @@ namespace NzbDrone.Core.Download.Clients.RTorrent public RTorrentSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).InclusiveBetween(0, 65535); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.MovieCategory).NotEmpty() .WithMessage("A category is recommended") - .AsWarning(); + .AsWarning(); } } diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs index 394fc53b4..8bf4c4ccb 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent public UTorrentSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).InclusiveBetween(0, 65535); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.TvCategory).NotEmpty(); } } diff --git a/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs b/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs index a8c1a9851..2af0ed9b8 100644 --- a/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs +++ b/src/NzbDrone.Core/Notifications/Email/EmailSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Notifications.Email public EmailSettingsValidator() { RuleFor(c => c.Server).NotEmpty(); - RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.From).NotEmpty(); RuleFor(c => c.To).NotEmpty(); } diff --git a/src/NzbDrone.Core/Notifications/Growl/GrowlSettings.cs b/src/NzbDrone.Core/Notifications/Growl/GrowlSettings.cs index 3c484dec7..55682003d 100644 --- a/src/NzbDrone.Core/Notifications/Growl/GrowlSettings.cs +++ b/src/NzbDrone.Core/Notifications/Growl/GrowlSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Notifications.Growl public GrowlSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); } } diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexClientSettings.cs b/src/NzbDrone.Core/Notifications/Plex/PlexClientSettings.cs index 34e9e4b75..d10993d79 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexClientSettings.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexClientSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Notifications.Plex public PlexClientSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); } } diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServerSettings.cs b/src/NzbDrone.Core/Notifications/Plex/PlexServerSettings.cs index e792392ab..9a5d0587c 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServerSettings.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexServerSettings.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Notifications.Plex public PlexServerSettingsValidator() { RuleFor(c => c.Host).ValidHost(); - RuleFor(c => c.Port).GreaterThan(0); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); } } From 25a51df89489aa76ec65acd1d95af20b5253508f Mon Sep 17 00:00:00 2001 From: Tim Turner Date: Fri, 27 Jan 2017 19:31:37 -0500 Subject: [PATCH 61/83] Merge FileNameBuilder --- .../Organizer/FileNameBuilder.cs | 1484 +++++++++-------- 1 file changed, 748 insertions(+), 736 deletions(-) diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 7cd3e0c0e..85108ff49 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -41,6 +41,9 @@ namespace NzbDrone.Core.Organizer private static readonly Regex EpisodeRegex = new Regex(@"(?\{episode(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex TagsRegex = new Regex(@"(?\{tags(?:\:0+)?})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SeasonRegex = new Regex(@"(?\{season(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -164,6 +167,7 @@ namespace NzbDrone.Core.Organizer AddQualityTokens(tokenHandlers, movie, movieFile); AddMediaInfoTokens(tokenHandlers, movieFile); AddMovieFileTokens(tokenHandlers, movieFile); + AddTagsTokens(tokenHandlers, movieFile); var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); @@ -340,7 +344,7 @@ namespace NzbDrone.Core.Organizer return title; } - + public static string TitleThe(string title) { string[] prefixes = { "The ", "An ", "A " }; @@ -355,738 +359,746 @@ namespace NzbDrone.Core.Organizer } return title.Trim(); - } - - public static string CleanFileName(string name, bool replace = true) - { - string result = name; - string[] badCharacters = { "\\", "/", "<", ">", "?", "*", ":", "|", "\"" }; - string[] goodCharacters = { "+", "+", "", "", "!", "-", "", "", "" }; - - for (int i = 0; i < badCharacters.Length; i++) - { - result = result.Replace(badCharacters[i], replace ? goodCharacters[i] : string.Empty); - } - - return result.Trim(); - } - - public static string CleanFolderName(string name) - { - name = FileNameCleanupRegex.Replace(name, match => match.Captures[0].Value[0].ToString()); - name = name.Trim(' ', '.'); - - return CleanFileName(name); - } - - private void AddSeriesTokens(Dictionary> tokenHandlers, Series series) - { - tokenHandlers["{Series Title}"] = m => series.Title; - tokenHandlers["{Series CleanTitle}"] = m => CleanTitle(series.Title); - } - - private string AddSeasonEpisodeNumberingTokens(string pattern, Dictionary> tokenHandlers, List episodes, NamingConfig namingConfig) - { - var episodeFormats = GetEpisodeFormat(pattern).DistinctBy(v => v.SeasonEpisodePattern).ToList(); - - int index = 1; - foreach (var episodeFormat in episodeFormats) - { - var seasonEpisodePattern = episodeFormat.SeasonEpisodePattern; - string formatPattern; - - switch ((MultiEpisodeStyle)namingConfig.MultiEpisodeStyle) - { - case MultiEpisodeStyle.Duplicate: - formatPattern = episodeFormat.Separator + episodeFormat.SeasonEpisodePattern; - seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.Repeat: - formatPattern = episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; - seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.Scene: - formatPattern = "-" + episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; - seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.Range: - formatPattern = "-" + episodeFormat.EpisodePattern; - seasonEpisodePattern = FormatRangeNumberTokens(seasonEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.PrefixedRange: - formatPattern = "-" + episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; - seasonEpisodePattern = FormatRangeNumberTokens(seasonEpisodePattern, formatPattern, episodes); - break; - - //MultiEpisodeStyle.Extend - default: - formatPattern = "-" + episodeFormat.EpisodePattern; - seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); - break; - } - - var token = string.Format("{{Season Episode{0}}}", index++); - pattern = pattern.Replace(episodeFormat.SeasonEpisodePattern, token); - tokenHandlers[token] = m => seasonEpisodePattern; - } - - AddSeasonTokens(tokenHandlers, episodes.First().SeasonNumber); - - if (episodes.Count > 1) - { - tokenHandlers["{Episode}"] = m => episodes.First().EpisodeNumber.ToString(m.CustomFormat) + "-" + episodes.Last().EpisodeNumber.ToString(m.CustomFormat); - } - else - { - tokenHandlers["{Episode}"] = m => episodes.First().EpisodeNumber.ToString(m.CustomFormat); - } - - return pattern; - } - - private string AddAbsoluteNumberingTokens(string pattern, Dictionary> tokenHandlers, Series series, List episodes, NamingConfig namingConfig) - { - var absoluteEpisodeFormats = GetAbsoluteFormat(pattern).DistinctBy(v => v.AbsoluteEpisodePattern).ToList(); - - int index = 1; - foreach (var absoluteEpisodeFormat in absoluteEpisodeFormats) - { - if (series.SeriesType != SeriesTypes.Anime) - { - pattern = pattern.Replace(absoluteEpisodeFormat.AbsoluteEpisodePattern, ""); - continue; - } - - var absoluteEpisodePattern = absoluteEpisodeFormat.AbsoluteEpisodePattern; - string formatPattern; - - switch ((MultiEpisodeStyle) namingConfig.MultiEpisodeStyle) - { - - case MultiEpisodeStyle.Duplicate: - formatPattern = absoluteEpisodeFormat.Separator + absoluteEpisodeFormat.AbsoluteEpisodePattern; - absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.Repeat: - var repeatSeparator = absoluteEpisodeFormat.Separator.Trim().IsNullOrWhiteSpace() ? " " : absoluteEpisodeFormat.Separator.Trim(); - - formatPattern = repeatSeparator + absoluteEpisodeFormat.AbsoluteEpisodePattern; - absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.Scene: - formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; - absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.Range: - case MultiEpisodeStyle.PrefixedRange: - formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; - var eps = new List {episodes.First()}; - - if (episodes.Count > 1) eps.Add(episodes.Last()); - - absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, eps); - break; - - //MultiEpisodeStyle.Extend - default: - formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; - absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); - break; - } - - var token = string.Format("{{Absolute Pattern{0}}}", index++); - pattern = pattern.Replace(absoluteEpisodeFormat.AbsoluteEpisodePattern, token); - tokenHandlers[token] = m => absoluteEpisodePattern; - } - - return pattern; - } - - private void AddMovieTokens(Dictionary> tokenHandlers, Movie movie) - { - tokenHandlers["{Movie Title}"] = m => movie.Title; - tokenHandlers["{Movie CleanTitle}"] = m => CleanTitle(movie.Title); - tokenHandlers["{Movie Title The}"] = m => TitleThe(movie.Title); - } - - private void AddReleaseDateTokens(Dictionary> tokenHandlers, int releaseYear) - { - tokenHandlers["{Release Year}"] = m => string.Format("{0}", releaseYear.ToString()); //Do I need m.CustomFormat? - } - - private void AddImdbIdTokens(Dictionary> tokenHandlers, string imdbId) - { - tokenHandlers["{IMDb Id}"] = m => $"{imdbId}"; - } - - private void AddSeasonTokens(Dictionary> tokenHandlers, int seasonNumber) - { - tokenHandlers["{Season}"] = m => seasonNumber.ToString(m.CustomFormat); - } - - private void AddEpisodeTokens(Dictionary> tokenHandlers, List episodes) - { - if (!episodes.First().AirDate.IsNullOrWhiteSpace()) - { - tokenHandlers["{Air Date}"] = m => episodes.First().AirDate.Replace('-', ' '); - } - else - { - tokenHandlers["{Air Date}"] = m => "Unknown"; - } - - tokenHandlers["{Episode Title}"] = m => GetEpisodeTitle(episodes, "+"); - tokenHandlers["{Episode CleanTitle}"] = m => CleanTitle(GetEpisodeTitle(episodes, "and")); - } - - private void AddEpisodeFileTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) - { - tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile); - tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile); - tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? m.DefaultValue("Sonarr"); - } - - private void AddMovieFileTokens(Dictionary> tokenHandlers, MovieFile episodeFile) - { - tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile); - tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile); - //tokenHandlers["{IMDb Id}"] = m => - tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? m.DefaultValue("Sonarr"); - } - - private void AddQualityTokens(Dictionary> tokenHandlers, Series series, EpisodeFile episodeFile) - { - var qualityTitle = _qualityDefinitionService.Get(episodeFile.Quality.Quality).Title; - var qualityProper = GetQualityProper(series, episodeFile.Quality); - var qualityReal = GetQualityReal(series, episodeFile.Quality); - - tokenHandlers["{Quality Full}"] = m => String.Format("{0} {1} {2}", qualityTitle, qualityProper, qualityReal); - tokenHandlers["{Quality Title}"] = m => qualityTitle; - tokenHandlers["{Quality Proper}"] = m => qualityProper; - tokenHandlers["{Quality Real}"] = m => qualityReal; - } - - private void AddQualityTokens(Dictionary> tokenHandlers, Movie movie, MovieFile movieFile) - { - var qualityTitle = _qualityDefinitionService.Get(movieFile.Quality.Quality).Title; - var qualityProper = GetQualityProper(movie, movieFile.Quality); - var qualityReal = GetQualityReal(movie, movieFile.Quality); - - tokenHandlers["{Quality Full}"] = m => String.Format("{0} {1} {2}", qualityTitle, qualityProper, qualityReal); - tokenHandlers["{Quality Title}"] = m => qualityTitle; - tokenHandlers["{Quality Proper}"] = m => qualityProper; - tokenHandlers["{Quality Real}"] = m => qualityReal; - } - - private void AddMediaInfoTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) - { - if (episodeFile.MediaInfo == null) return; - - string videoCodec; - switch (episodeFile.MediaInfo.VideoCodec) - { - case "AVC": - if (episodeFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(episodeFile.SceneName).Contains("h264")) - { - videoCodec = "h264"; - } - else - { - videoCodec = "x264"; - } - break; - - case "V_MPEGH/ISO/HEVC": - if (episodeFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(episodeFile.SceneName).Contains("h265")) - { - videoCodec = "h265"; - } - else - { - videoCodec = "x265"; - } - break; - - case "MPEG-2 Video": - videoCodec = "MPEG2"; - break; - - default: - videoCodec = episodeFile.MediaInfo.VideoCodec; - break; - } - - string audioCodec; - switch (episodeFile.MediaInfo.AudioFormat) - { - case "AC-3": - audioCodec = "AC3"; - break; - - case "E-AC-3": - audioCodec = "EAC3"; - break; - - case "MPEG Audio": - if (episodeFile.MediaInfo.AudioProfile == "Layer 3") - { - audioCodec = "MP3"; - } - else - { - audioCodec = episodeFile.MediaInfo.AudioFormat; - } - break; - - case "DTS": - audioCodec = episodeFile.MediaInfo.AudioFormat; - break; - - default: - audioCodec = episodeFile.MediaInfo.AudioFormat; - break; - } - - var mediaInfoAudioLanguages = GetLanguagesToken(episodeFile.MediaInfo.AudioLanguages); - if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace()) - { - mediaInfoAudioLanguages = string.Format("[{0}]", mediaInfoAudioLanguages); - } - - if (mediaInfoAudioLanguages == "[EN]") - { - mediaInfoAudioLanguages = string.Empty; - } - - var mediaInfoSubtitleLanguages = GetLanguagesToken(episodeFile.MediaInfo.Subtitles); - if (!mediaInfoSubtitleLanguages.IsNullOrWhiteSpace()) - { - mediaInfoSubtitleLanguages = string.Format("[{0}]", mediaInfoSubtitleLanguages); - } - - var videoBitDepth = episodeFile.MediaInfo.VideoBitDepth > 0 ? episodeFile.MediaInfo.VideoBitDepth.ToString() : string.Empty; - var audioChannels = episodeFile.MediaInfo.FormattedAudioChannels > 0 ? - episodeFile.MediaInfo.FormattedAudioChannels.ToString("F1", CultureInfo.InvariantCulture) : - string.Empty; - - tokenHandlers["{MediaInfo Video}"] = m => videoCodec; - tokenHandlers["{MediaInfo VideoCodec}"] = m => videoCodec; - tokenHandlers["{MediaInfo VideoBitDepth}"] = m => videoBitDepth; - - tokenHandlers["{MediaInfo Audio}"] = m => audioCodec; - tokenHandlers["{MediaInfo AudioCodec}"] = m => audioCodec; - tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannels; - - tokenHandlers["{MediaInfo Simple}"] = m => string.Format("{0} {1}", videoCodec, audioCodec); - - tokenHandlers["{MediaInfo Full}"] = m => string.Format("{0} {1}{2} {3}", videoCodec, audioCodec, mediaInfoAudioLanguages, mediaInfoSubtitleLanguages); - } - - private void AddMediaInfoTokens(Dictionary> tokenHandlers, MovieFile movieFile) - { - if (movieFile.MediaInfo == null) return; - - string videoCodec; - switch (movieFile.MediaInfo.VideoCodec) - { - case "AVC": - if (movieFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(movieFile.SceneName).Contains("h264")) - { - videoCodec = "h264"; - } - else - { - videoCodec = "x264"; - } - break; - - case "V_MPEGH/ISO/HEVC": - if (movieFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(movieFile.SceneName).Contains("h265")) - { - videoCodec = "h265"; - } - else - { - videoCodec = "x265"; - } - break; - - case "MPEG-2 Video": - videoCodec = "MPEG2"; - break; - - default: - videoCodec = movieFile.MediaInfo.VideoCodec; - break; - } - - string audioCodec; - switch (movieFile.MediaInfo.AudioFormat) - { - case "AC-3": - audioCodec = "AC3"; - break; - - case "E-AC-3": - audioCodec = "EAC3"; - break; - - case "MPEG Audio": - if (movieFile.MediaInfo.AudioProfile == "Layer 3") - { - audioCodec = "MP3"; - } - else - { - audioCodec = movieFile.MediaInfo.AudioFormat; - } - break; - - case "DTS": - audioCodec = movieFile.MediaInfo.AudioFormat; - break; - - default: - audioCodec = movieFile.MediaInfo.AudioFormat; - break; - } - - var mediaInfoAudioLanguages = GetLanguagesToken(movieFile.MediaInfo.AudioLanguages); - if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace()) - { - mediaInfoAudioLanguages = string.Format("[{0}]", mediaInfoAudioLanguages); - } - - if (mediaInfoAudioLanguages == "[EN]") - { - mediaInfoAudioLanguages = string.Empty; - } - - var mediaInfoSubtitleLanguages = GetLanguagesToken(movieFile.MediaInfo.Subtitles); - if (!mediaInfoSubtitleLanguages.IsNullOrWhiteSpace()) - { - mediaInfoSubtitleLanguages = string.Format("[{0}]", mediaInfoSubtitleLanguages); - } - - var videoBitDepth = movieFile.MediaInfo.VideoBitDepth > 0 ? movieFile.MediaInfo.VideoBitDepth.ToString() : string.Empty; - var audioChannels = movieFile.MediaInfo.FormattedAudioChannels > 0 ? - movieFile.MediaInfo.FormattedAudioChannels.ToString("F1", CultureInfo.InvariantCulture) : - string.Empty; - - tokenHandlers["{MediaInfo Video}"] = m => videoCodec; - tokenHandlers["{MediaInfo VideoCodec}"] = m => videoCodec; - tokenHandlers["{MediaInfo VideoBitDepth}"] = m => videoBitDepth; - - tokenHandlers["{MediaInfo Audio}"] = m => audioCodec; - tokenHandlers["{MediaInfo AudioCodec}"] = m => audioCodec; - tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannels; - - tokenHandlers["{MediaInfo Simple}"] = m => string.Format("{0} {1}", videoCodec, audioCodec); - - tokenHandlers["{MediaInfo Full}"] = m => string.Format("{0} {1}{2} {3}", videoCodec, audioCodec, mediaInfoAudioLanguages, mediaInfoSubtitleLanguages); - } - - private string GetLanguagesToken(string mediaInfoLanguages) - { - List tokens = new List(); - foreach (var item in mediaInfoLanguages.Split('/')) - { - if (!string.IsNullOrWhiteSpace(item)) - tokens.Add(item.Trim()); - } - - var cultures = System.Globalization.CultureInfo.GetCultures(System.Globalization.CultureTypes.NeutralCultures); - for (int i = 0; i < tokens.Count; i++) - { - try - { - var cultureInfo = cultures.FirstOrDefault(p => p.EnglishName == tokens[i]); - - if (cultureInfo != null) - tokens[i] = cultureInfo.TwoLetterISOLanguageName.ToUpper(); - } - catch - { - } - } - - return string.Join("+", tokens.Distinct()); - } - - private string ReplaceTokens(string pattern, Dictionary> tokenHandlers, NamingConfig namingConfig) - { - return TitleRegex.Replace(pattern, match => ReplaceToken(match, tokenHandlers, namingConfig)); - } - - private string ReplaceToken(Match match, Dictionary> tokenHandlers, NamingConfig namingConfig) - { - var tokenMatch = new TokenMatch - { - RegexMatch = match, - Prefix = match.Groups["prefix"].Value, - Separator = match.Groups["separator"].Value, - Suffix = match.Groups["suffix"].Value, - Token = match.Groups["token"].Value, - CustomFormat = match.Groups["customFormat"].Value - }; - - if (tokenMatch.CustomFormat.IsNullOrWhiteSpace()) - { - tokenMatch.CustomFormat = null; - } - - var tokenHandler = tokenHandlers.GetValueOrDefault(tokenMatch.Token, m => string.Empty); - - var replacementText = tokenHandler(tokenMatch).Trim(); - - if (tokenMatch.Token.All(t => !char.IsLetter(t) || char.IsLower(t))) - { - replacementText = replacementText.ToLower(); - } - else if (tokenMatch.Token.All(t => !char.IsLetter(t) || char.IsUpper(t))) - { - replacementText = replacementText.ToUpper(); - } - - if (!tokenMatch.Separator.IsNullOrWhiteSpace()) - { - replacementText = replacementText.Replace(" ", tokenMatch.Separator); - } - - replacementText = CleanFileName(replacementText, namingConfig.ReplaceIllegalCharacters); - - if (!replacementText.IsNullOrWhiteSpace()) - { - replacementText = tokenMatch.Prefix + replacementText + tokenMatch.Suffix; - } - - return replacementText; - } - - private string FormatNumberTokens(string basePattern, string formatPattern, List episodes) - { - var pattern = string.Empty; - - for (int i = 0; i < episodes.Count; i++) - { - var patternToReplace = i == 0 ? basePattern : formatPattern; - - pattern += EpisodeRegex.Replace(patternToReplace, match => ReplaceNumberToken(match.Groups["episode"].Value, episodes[i].EpisodeNumber)); - } - - return ReplaceSeasonTokens(pattern, episodes.First().SeasonNumber); - } - - private string FormatAbsoluteNumberTokens(string basePattern, string formatPattern, List episodes) - { - var pattern = string.Empty; - - for (int i = 0; i < episodes.Count; i++) - { - var patternToReplace = i == 0 ? basePattern : formatPattern; - - pattern += AbsoluteEpisodeRegex.Replace(patternToReplace, match => ReplaceNumberToken(match.Groups["absolute"].Value, episodes[i].AbsoluteEpisodeNumber.Value)); - } - - return ReplaceSeasonTokens(pattern, episodes.First().SeasonNumber); - } - - private string FormatRangeNumberTokens(string seasonEpisodePattern, string formatPattern, List episodes) - { - var eps = new List { episodes.First() }; - - if (episodes.Count > 1) eps.Add(episodes.Last()); - - return FormatNumberTokens(seasonEpisodePattern, formatPattern, eps); - } - - private string ReplaceSeasonTokens(string pattern, int seasonNumber) - { - return SeasonRegex.Replace(pattern, match => ReplaceNumberToken(match.Groups["season"].Value, seasonNumber)); - } - - private string ReplaceNumberToken(string token, int value) - { - var split = token.Trim('{', '}').Split(':'); - if (split.Length == 1) return value.ToString("0"); - - return value.ToString(split[1]); - } - - private EpisodeFormat[] GetEpisodeFormat(string pattern) - { - return _episodeFormatCache.Get(pattern, () => SeasonEpisodePatternRegex.Matches(pattern).OfType() - .Select(match => new EpisodeFormat - { - EpisodeSeparator = match.Groups["episodeSeparator"].Value, - Separator = match.Groups["separator"].Value, - EpisodePattern = match.Groups["episode"].Value, - SeasonEpisodePattern = match.Groups["seasonEpisode"].Value, - }).ToArray()); - } - - private AbsoluteEpisodeFormat[] GetAbsoluteFormat(string pattern) - { - return _absoluteEpisodeFormatCache.Get(pattern, () => AbsoluteEpisodePatternRegex.Matches(pattern).OfType() - .Select(match => new AbsoluteEpisodeFormat - { - Separator = match.Groups["separator"].Value.IsNotNullOrWhiteSpace() ? match.Groups["separator"].Value : "-", - AbsoluteEpisodePattern = match.Groups["absolute"].Value - }).ToArray()); - } - - private string GetEpisodeTitle(List episodes, string separator) - { - separator = string.Format(" {0} ", separator.Trim()); - - if (episodes.Count == 1) - { - return episodes.First().Title.TrimEnd(EpisodeTitleTrimCharacters); - } - - var titles = episodes.Select(c => c.Title.TrimEnd(EpisodeTitleTrimCharacters)) - .Select(CleanupEpisodeTitle) - .Distinct() - .ToList(); - - if (titles.All(t => t.IsNullOrWhiteSpace())) - { - titles = episodes.Select(c => c.Title.TrimEnd(EpisodeTitleTrimCharacters)) - .Distinct() - .ToList(); - } - - return string.Join(separator, titles); - } - - private string CleanupEpisodeTitle(string title) - { - //this will remove (1),(2) from the end of multi part episodes. - return MultiPartCleanupRegex.Replace(title, string.Empty).Trim(); - } - - private string GetQualityProper(Movie movie, QualityModel quality) - { - if (quality.Revision.Version > 1) - { - return "Proper"; - } - - return String.Empty; - } - - private string GetQualityProper(Series series, QualityModel quality) - { - if (quality.Revision.Version > 1) - { - if (series.SeriesType == SeriesTypes.Anime) - { - return "v" + quality.Revision.Version; - } - - return "Proper"; - } - - return String.Empty; - } - - private string GetQualityReal(Series series, QualityModel quality) - { - if (quality.Revision.Real > 0) - { - return "REAL"; - } - - return string.Empty; - } - - private string GetQualityReal(Movie movie, QualityModel quality) - { - if (quality.Revision.Real > 0) - { - return "REAL"; - } - - return string.Empty; - } - - private string GetOriginalTitle(EpisodeFile episodeFile) - { - if (episodeFile.SceneName.IsNullOrWhiteSpace()) - { - return GetOriginalFileName(episodeFile); - } - - return episodeFile.SceneName; - } - - private string GetOriginalFileName(EpisodeFile episodeFile) - { - if (episodeFile.RelativePath.IsNullOrWhiteSpace()) - { - return Path.GetFileNameWithoutExtension(episodeFile.Path); - } - - return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); - } - - private string GetOriginalTitle(MovieFile episodeFile) - { - if (episodeFile.SceneName.IsNullOrWhiteSpace()) - { - return GetOriginalFileName(episodeFile); - } - - return episodeFile.SceneName; - } - - private string GetOriginalFileName(MovieFile episodeFile) - { - if (episodeFile.RelativePath.IsNullOrWhiteSpace()) - { - return Path.GetFileNameWithoutExtension(episodeFile.Path); - } - - return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); - } - } - - internal sealed class TokenMatch - { - public Match RegexMatch { get; set; } - public string Prefix { get; set; } - public string Separator { get; set; } - public string Suffix { get; set; } - public string Token { get; set; } - public string CustomFormat { get; set; } - - public string DefaultValue(string defaultValue) - { - if (string.IsNullOrEmpty(Prefix) && string.IsNullOrEmpty(Suffix)) - { - return defaultValue; - } - else - { - return string.Empty; - } - } - } - - public enum MultiEpisodeStyle - { - Extend = 0, - Duplicate = 1, - Repeat = 2, - Scene = 3, - Range = 4, - PrefixedRange = 5 - } -} + } + + public static string CleanFileName(string name, bool replace = true) + { + string result = name; + string[] badCharacters = { "\\", "/", "<", ">", "?", "*", ":", "|", "\"" }; + string[] goodCharacters = { "+", "+", "", "", "!", "-", "", "", "" }; + + for (int i = 0; i < badCharacters.Length; i++) + { + result = result.Replace(badCharacters[i], replace ? goodCharacters[i] : string.Empty); + } + + return result.Trim(); + } + + public static string CleanFolderName(string name) + { + name = FileNameCleanupRegex.Replace(name, match => match.Captures[0].Value[0].ToString()); + name = name.Trim(' ', '.'); + + return CleanFileName(name); + } + + private void AddSeriesTokens(Dictionary> tokenHandlers, Series series) + { + tokenHandlers["{Series Title}"] = m => series.Title; + tokenHandlers["{Series CleanTitle}"] = m => CleanTitle(series.Title); + } + + private string AddSeasonEpisodeNumberingTokens(string pattern, Dictionary> tokenHandlers, List episodes, NamingConfig namingConfig) + { + var episodeFormats = GetEpisodeFormat(pattern).DistinctBy(v => v.SeasonEpisodePattern).ToList(); + + int index = 1; + foreach (var episodeFormat in episodeFormats) + { + var seasonEpisodePattern = episodeFormat.SeasonEpisodePattern; + string formatPattern; + + switch ((MultiEpisodeStyle)namingConfig.MultiEpisodeStyle) + { + case MultiEpisodeStyle.Duplicate: + formatPattern = episodeFormat.Separator + episodeFormat.SeasonEpisodePattern; + seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.Repeat: + formatPattern = episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; + seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.Scene: + formatPattern = "-" + episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; + seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.Range: + formatPattern = "-" + episodeFormat.EpisodePattern; + seasonEpisodePattern = FormatRangeNumberTokens(seasonEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.PrefixedRange: + formatPattern = "-" + episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; + seasonEpisodePattern = FormatRangeNumberTokens(seasonEpisodePattern, formatPattern, episodes); + break; + + //MultiEpisodeStyle.Extend + default: + formatPattern = "-" + episodeFormat.EpisodePattern; + seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); + break; + } + + var token = string.Format("{{Season Episode{0}}}", index++); + pattern = pattern.Replace(episodeFormat.SeasonEpisodePattern, token); + tokenHandlers[token] = m => seasonEpisodePattern; + } + + AddSeasonTokens(tokenHandlers, episodes.First().SeasonNumber); + + if (episodes.Count > 1) + { + tokenHandlers["{Episode}"] = m => episodes.First().EpisodeNumber.ToString(m.CustomFormat) + "-" + episodes.Last().EpisodeNumber.ToString(m.CustomFormat); + } + else + { + tokenHandlers["{Episode}"] = m => episodes.First().EpisodeNumber.ToString(m.CustomFormat); + } + + return pattern; + } + + private string AddAbsoluteNumberingTokens(string pattern, Dictionary> tokenHandlers, Series series, List episodes, NamingConfig namingConfig) + { + var absoluteEpisodeFormats = GetAbsoluteFormat(pattern).DistinctBy(v => v.AbsoluteEpisodePattern).ToList(); + + int index = 1; + foreach (var absoluteEpisodeFormat in absoluteEpisodeFormats) + { + if (series.SeriesType != SeriesTypes.Anime) + { + pattern = pattern.Replace(absoluteEpisodeFormat.AbsoluteEpisodePattern, ""); + continue; + } + + var absoluteEpisodePattern = absoluteEpisodeFormat.AbsoluteEpisodePattern; + string formatPattern; + + switch ((MultiEpisodeStyle) namingConfig.MultiEpisodeStyle) + { + + case MultiEpisodeStyle.Duplicate: + formatPattern = absoluteEpisodeFormat.Separator + absoluteEpisodeFormat.AbsoluteEpisodePattern; + absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.Repeat: + var repeatSeparator = absoluteEpisodeFormat.Separator.Trim().IsNullOrWhiteSpace() ? " " : absoluteEpisodeFormat.Separator.Trim(); + + formatPattern = repeatSeparator + absoluteEpisodeFormat.AbsoluteEpisodePattern; + absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.Scene: + formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; + absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.Range: + case MultiEpisodeStyle.PrefixedRange: + formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; + var eps = new List {episodes.First()}; + + if (episodes.Count > 1) eps.Add(episodes.Last()); + + absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, eps); + break; + + //MultiEpisodeStyle.Extend + default: + formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; + absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); + break; + } + + var token = string.Format("{{Absolute Pattern{0}}}", index++); + pattern = pattern.Replace(absoluteEpisodeFormat.AbsoluteEpisodePattern, token); + tokenHandlers[token] = m => absoluteEpisodePattern; + } + + return pattern; + } + + private void AddMovieTokens(Dictionary> tokenHandlers, Movie movie) + { + tokenHandlers["{Movie Title}"] = m => movie.Title; + tokenHandlers["{Movie CleanTitle}"] = m => CleanTitle(movie.Title); + tokenHandlers["{Movie Title The}"] = m => TitleThe(movie.Title); + } + + private void AddTagsTokens(Dictionary> tokenHandlers, MovieFile movieFile) + { + if (movieFile.Edition.IsNotNullOrWhiteSpace()) + { + tokenHandlers["{Edition Tags}"] = m => CultureInfo.CurrentCulture.TextInfo.ToTitleCase(movieFile.Edition.ToLower()); + } + } + + private void AddReleaseDateTokens(Dictionary> tokenHandlers, int releaseYear) + { + tokenHandlers["{Release Year}"] = m => string.Format("{0}", releaseYear.ToString()); //Do I need m.CustomFormat? + } + + private void AddImdbIdTokens(Dictionary> tokenHandlers, string imdbId) + { + tokenHandlers["{IMDb Id}"] = m => $"{imdbId}"; + } + + private void AddSeasonTokens(Dictionary> tokenHandlers, int seasonNumber) + { + tokenHandlers["{Season}"] = m => seasonNumber.ToString(m.CustomFormat); + } + + private void AddEpisodeTokens(Dictionary> tokenHandlers, List episodes) + { + if (!episodes.First().AirDate.IsNullOrWhiteSpace()) + { + tokenHandlers["{Air Date}"] = m => episodes.First().AirDate.Replace('-', ' '); + } + else + { + tokenHandlers["{Air Date}"] = m => "Unknown"; + } + + tokenHandlers["{Episode Title}"] = m => GetEpisodeTitle(episodes, "+"); + tokenHandlers["{Episode CleanTitle}"] = m => CleanTitle(GetEpisodeTitle(episodes, "and")); + } + + private void AddEpisodeFileTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) + { + tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile); + tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile); + tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? m.DefaultValue("Sonarr"); + } + + private void AddMovieFileTokens(Dictionary> tokenHandlers, MovieFile episodeFile) + { + tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile); + tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile); + //tokenHandlers["{IMDb Id}"] = m => + tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? m.DefaultValue("Sonarr"); + } + + private void AddQualityTokens(Dictionary> tokenHandlers, Series series, EpisodeFile episodeFile) + { + var qualityTitle = _qualityDefinitionService.Get(episodeFile.Quality.Quality).Title; + var qualityProper = GetQualityProper(series, episodeFile.Quality); + var qualityReal = GetQualityReal(series, episodeFile.Quality); + + tokenHandlers["{Quality Full}"] = m => String.Format("{0} {1} {2}", qualityTitle, qualityProper, qualityReal); + tokenHandlers["{Quality Title}"] = m => qualityTitle; + tokenHandlers["{Quality Proper}"] = m => qualityProper; + tokenHandlers["{Quality Real}"] = m => qualityReal; + } + + private void AddQualityTokens(Dictionary> tokenHandlers, Movie movie, MovieFile movieFile) + { + var qualityTitle = _qualityDefinitionService.Get(movieFile.Quality.Quality).Title; + var qualityProper = GetQualityProper(movie, movieFile.Quality); + var qualityReal = GetQualityReal(movie, movieFile.Quality); + + tokenHandlers["{Quality Full}"] = m => String.Format("{0} {1} {2}", qualityTitle, qualityProper, qualityReal); + tokenHandlers["{Quality Title}"] = m => qualityTitle; + tokenHandlers["{Quality Proper}"] = m => qualityProper; + tokenHandlers["{Quality Real}"] = m => qualityReal; + } + + private void AddMediaInfoTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) + { + if (episodeFile.MediaInfo == null) return; + + string videoCodec; + switch (episodeFile.MediaInfo.VideoCodec) + { + case "AVC": + if (episodeFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(episodeFile.SceneName).Contains("h264")) + { + videoCodec = "h264"; + } + else + { + videoCodec = "x264"; + } + break; + + case "V_MPEGH/ISO/HEVC": + if (episodeFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(episodeFile.SceneName).Contains("h265")) + { + videoCodec = "h265"; + } + else + { + videoCodec = "x265"; + } + break; + + case "MPEG-2 Video": + videoCodec = "MPEG2"; + break; + + default: + videoCodec = episodeFile.MediaInfo.VideoCodec; + break; + } + + string audioCodec; + switch (episodeFile.MediaInfo.AudioFormat) + { + case "AC-3": + audioCodec = "AC3"; + break; + + case "E-AC-3": + audioCodec = "EAC3"; + break; + + case "MPEG Audio": + if (episodeFile.MediaInfo.AudioProfile == "Layer 3") + { + audioCodec = "MP3"; + } + else + { + audioCodec = episodeFile.MediaInfo.AudioFormat; + } + break; + + case "DTS": + audioCodec = episodeFile.MediaInfo.AudioFormat; + break; + + default: + audioCodec = episodeFile.MediaInfo.AudioFormat; + break; + } + + var mediaInfoAudioLanguages = GetLanguagesToken(episodeFile.MediaInfo.AudioLanguages); + if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace()) + { + mediaInfoAudioLanguages = string.Format("[{0}]", mediaInfoAudioLanguages); + } + + if (mediaInfoAudioLanguages == "[EN]") + { + mediaInfoAudioLanguages = string.Empty; + } + + var mediaInfoSubtitleLanguages = GetLanguagesToken(episodeFile.MediaInfo.Subtitles); + if (!mediaInfoSubtitleLanguages.IsNullOrWhiteSpace()) + { + mediaInfoSubtitleLanguages = string.Format("[{0}]", mediaInfoSubtitleLanguages); + } + + var videoBitDepth = episodeFile.MediaInfo.VideoBitDepth > 0 ? episodeFile.MediaInfo.VideoBitDepth.ToString() : string.Empty; + var audioChannels = episodeFile.MediaInfo.FormattedAudioChannels > 0 ? + episodeFile.MediaInfo.FormattedAudioChannels.ToString("F1", CultureInfo.InvariantCulture) : + string.Empty; + + tokenHandlers["{MediaInfo Video}"] = m => videoCodec; + tokenHandlers["{MediaInfo VideoCodec}"] = m => videoCodec; + tokenHandlers["{MediaInfo VideoBitDepth}"] = m => videoBitDepth; + + tokenHandlers["{MediaInfo Audio}"] = m => audioCodec; + tokenHandlers["{MediaInfo AudioCodec}"] = m => audioCodec; + tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannels; + + tokenHandlers["{MediaInfo Simple}"] = m => string.Format("{0} {1}", videoCodec, audioCodec); + + tokenHandlers["{MediaInfo Full}"] = m => string.Format("{0} {1}{2} {3}", videoCodec, audioCodec, mediaInfoAudioLanguages, mediaInfoSubtitleLanguages); + } + + private void AddMediaInfoTokens(Dictionary> tokenHandlers, MovieFile movieFile) + { + if (movieFile.MediaInfo == null) return; + + string videoCodec; + switch (movieFile.MediaInfo.VideoCodec) + { + case "AVC": + if (movieFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(movieFile.SceneName).Contains("h264")) + { + videoCodec = "h264"; + } + else + { + videoCodec = "x264"; + } + break; + + case "V_MPEGH/ISO/HEVC": + if (movieFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(movieFile.SceneName).Contains("h265")) + { + videoCodec = "h265"; + } + else + { + videoCodec = "x265"; + } + break; + + case "MPEG-2 Video": + videoCodec = "MPEG2"; + break; + + default: + videoCodec = movieFile.MediaInfo.VideoCodec; + break; + } + + string audioCodec; + switch (movieFile.MediaInfo.AudioFormat) + { + case "AC-3": + audioCodec = "AC3"; + break; + + case "E-AC-3": + audioCodec = "EAC3"; + break; + + case "MPEG Audio": + if (movieFile.MediaInfo.AudioProfile == "Layer 3") + { + audioCodec = "MP3"; + } + else + { + audioCodec = movieFile.MediaInfo.AudioFormat; + } + break; + + case "DTS": + audioCodec = movieFile.MediaInfo.AudioFormat; + break; + + default: + audioCodec = movieFile.MediaInfo.AudioFormat; + break; + } + + var mediaInfoAudioLanguages = GetLanguagesToken(movieFile.MediaInfo.AudioLanguages); + if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace()) + { + mediaInfoAudioLanguages = string.Format("[{0}]", mediaInfoAudioLanguages); + } + + if (mediaInfoAudioLanguages == "[EN]") + { + mediaInfoAudioLanguages = string.Empty; + } + + var mediaInfoSubtitleLanguages = GetLanguagesToken(movieFile.MediaInfo.Subtitles); + if (!mediaInfoSubtitleLanguages.IsNullOrWhiteSpace()) + { + mediaInfoSubtitleLanguages = string.Format("[{0}]", mediaInfoSubtitleLanguages); + } + + var videoBitDepth = movieFile.MediaInfo.VideoBitDepth > 0 ? movieFile.MediaInfo.VideoBitDepth.ToString() : string.Empty; + var audioChannels = movieFile.MediaInfo.FormattedAudioChannels > 0 ? + movieFile.MediaInfo.FormattedAudioChannels.ToString("F1", CultureInfo.InvariantCulture) : + string.Empty; + + tokenHandlers["{MediaInfo Video}"] = m => videoCodec; + tokenHandlers["{MediaInfo VideoCodec}"] = m => videoCodec; + tokenHandlers["{MediaInfo VideoBitDepth}"] = m => videoBitDepth; + + tokenHandlers["{MediaInfo Audio}"] = m => audioCodec; + tokenHandlers["{MediaInfo AudioCodec}"] = m => audioCodec; + tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannels; + + tokenHandlers["{MediaInfo Simple}"] = m => string.Format("{0} {1}", videoCodec, audioCodec); + + tokenHandlers["{MediaInfo Full}"] = m => string.Format("{0} {1}{2} {3}", videoCodec, audioCodec, mediaInfoAudioLanguages, mediaInfoSubtitleLanguages); + } + + private string GetLanguagesToken(string mediaInfoLanguages) + { + List tokens = new List(); + foreach (var item in mediaInfoLanguages.Split('/')) + { + if (!string.IsNullOrWhiteSpace(item)) + tokens.Add(item.Trim()); + } + + var cultures = System.Globalization.CultureInfo.GetCultures(System.Globalization.CultureTypes.NeutralCultures); + for (int i = 0; i < tokens.Count; i++) + { + try + { + var cultureInfo = cultures.FirstOrDefault(p => p.EnglishName == tokens[i]); + + if (cultureInfo != null) + tokens[i] = cultureInfo.TwoLetterISOLanguageName.ToUpper(); + } + catch + { + } + } + + return string.Join("+", tokens.Distinct()); + } + + private string ReplaceTokens(string pattern, Dictionary> tokenHandlers, NamingConfig namingConfig) + { + return TitleRegex.Replace(pattern, match => ReplaceToken(match, tokenHandlers, namingConfig)); + } + + private string ReplaceToken(Match match, Dictionary> tokenHandlers, NamingConfig namingConfig) + { + var tokenMatch = new TokenMatch + { + RegexMatch = match, + Prefix = match.Groups["prefix"].Value, + Separator = match.Groups["separator"].Value, + Suffix = match.Groups["suffix"].Value, + Token = match.Groups["token"].Value, + CustomFormat = match.Groups["customFormat"].Value + }; + + if (tokenMatch.CustomFormat.IsNullOrWhiteSpace()) + { + tokenMatch.CustomFormat = null; + } + + var tokenHandler = tokenHandlers.GetValueOrDefault(tokenMatch.Token, m => string.Empty); + + var replacementText = tokenHandler(tokenMatch).Trim(); + + if (tokenMatch.Token.All(t => !char.IsLetter(t) || char.IsLower(t))) + { + replacementText = replacementText.ToLower(); + } + else if (tokenMatch.Token.All(t => !char.IsLetter(t) || char.IsUpper(t))) + { + replacementText = replacementText.ToUpper(); + } + + if (!tokenMatch.Separator.IsNullOrWhiteSpace()) + { + replacementText = replacementText.Replace(" ", tokenMatch.Separator); + } + + replacementText = CleanFileName(replacementText, namingConfig.ReplaceIllegalCharacters); + + if (!replacementText.IsNullOrWhiteSpace()) + { + replacementText = tokenMatch.Prefix + replacementText + tokenMatch.Suffix; + } + + return replacementText; + } + + private string FormatNumberTokens(string basePattern, string formatPattern, List episodes) + { + var pattern = string.Empty; + + for (int i = 0; i < episodes.Count; i++) + { + var patternToReplace = i == 0 ? basePattern : formatPattern; + + pattern += EpisodeRegex.Replace(patternToReplace, match => ReplaceNumberToken(match.Groups["episode"].Value, episodes[i].EpisodeNumber)); + } + + return ReplaceSeasonTokens(pattern, episodes.First().SeasonNumber); + } + + private string FormatAbsoluteNumberTokens(string basePattern, string formatPattern, List episodes) + { + var pattern = string.Empty; + + for (int i = 0; i < episodes.Count; i++) + { + var patternToReplace = i == 0 ? basePattern : formatPattern; + + pattern += AbsoluteEpisodeRegex.Replace(patternToReplace, match => ReplaceNumberToken(match.Groups["absolute"].Value, episodes[i].AbsoluteEpisodeNumber.Value)); + } + + return ReplaceSeasonTokens(pattern, episodes.First().SeasonNumber); + } + + private string FormatRangeNumberTokens(string seasonEpisodePattern, string formatPattern, List episodes) + { + var eps = new List { episodes.First() }; + + if (episodes.Count > 1) eps.Add(episodes.Last()); + + return FormatNumberTokens(seasonEpisodePattern, formatPattern, eps); + } + + private string ReplaceSeasonTokens(string pattern, int seasonNumber) + { + return SeasonRegex.Replace(pattern, match => ReplaceNumberToken(match.Groups["season"].Value, seasonNumber)); + } + + private string ReplaceNumberToken(string token, int value) + { + var split = token.Trim('{', '}').Split(':'); + if (split.Length == 1) return value.ToString("0"); + + return value.ToString(split[1]); + } + + private EpisodeFormat[] GetEpisodeFormat(string pattern) + { + return _episodeFormatCache.Get(pattern, () => SeasonEpisodePatternRegex.Matches(pattern).OfType() + .Select(match => new EpisodeFormat + { + EpisodeSeparator = match.Groups["episodeSeparator"].Value, + Separator = match.Groups["separator"].Value, + EpisodePattern = match.Groups["episode"].Value, + SeasonEpisodePattern = match.Groups["seasonEpisode"].Value, + }).ToArray()); + } + + private AbsoluteEpisodeFormat[] GetAbsoluteFormat(string pattern) + { + return _absoluteEpisodeFormatCache.Get(pattern, () => AbsoluteEpisodePatternRegex.Matches(pattern).OfType() + .Select(match => new AbsoluteEpisodeFormat + { + Separator = match.Groups["separator"].Value.IsNotNullOrWhiteSpace() ? match.Groups["separator"].Value : "-", + AbsoluteEpisodePattern = match.Groups["absolute"].Value + }).ToArray()); + } + + private string GetEpisodeTitle(List episodes, string separator) + { + separator = string.Format(" {0} ", separator.Trim()); + + if (episodes.Count == 1) + { + return episodes.First().Title.TrimEnd(EpisodeTitleTrimCharacters); + } + + var titles = episodes.Select(c => c.Title.TrimEnd(EpisodeTitleTrimCharacters)) + .Select(CleanupEpisodeTitle) + .Distinct() + .ToList(); + + if (titles.All(t => t.IsNullOrWhiteSpace())) + { + titles = episodes.Select(c => c.Title.TrimEnd(EpisodeTitleTrimCharacters)) + .Distinct() + .ToList(); + } + + return string.Join(separator, titles); + } + + private string CleanupEpisodeTitle(string title) + { + //this will remove (1),(2) from the end of multi part episodes. + return MultiPartCleanupRegex.Replace(title, string.Empty).Trim(); + } + + private string GetQualityProper(Movie movie, QualityModel quality) + { + if (quality.Revision.Version > 1) + { + return "Proper"; + } + + return String.Empty; + } + + private string GetQualityProper(Series series, QualityModel quality) + { + if (quality.Revision.Version > 1) + { + if (series.SeriesType == SeriesTypes.Anime) + { + return "v" + quality.Revision.Version; + } + + return "Proper"; + } + + return String.Empty; + } + + private string GetQualityReal(Series series, QualityModel quality) + { + if (quality.Revision.Real > 0) + { + return "REAL"; + } + + return string.Empty; + } + + private string GetQualityReal(Movie movie, QualityModel quality) + { + if (quality.Revision.Real > 0) + { + return "REAL"; + } + + return string.Empty; + } + + private string GetOriginalTitle(EpisodeFile episodeFile) + { + if (episodeFile.SceneName.IsNullOrWhiteSpace()) + { + return GetOriginalFileName(episodeFile); + } + + return episodeFile.SceneName; + } + + private string GetOriginalFileName(EpisodeFile episodeFile) + { + if (episodeFile.RelativePath.IsNullOrWhiteSpace()) + { + return Path.GetFileNameWithoutExtension(episodeFile.Path); + } + + return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); + } + + private string GetOriginalTitle(MovieFile episodeFile) + { + if (episodeFile.SceneName.IsNullOrWhiteSpace()) + { + return GetOriginalFileName(episodeFile); + } + + return episodeFile.SceneName; + } + + private string GetOriginalFileName(MovieFile episodeFile) + { + if (episodeFile.RelativePath.IsNullOrWhiteSpace()) + { + return Path.GetFileNameWithoutExtension(episodeFile.Path); + } + + return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); + } + } + + internal sealed class TokenMatch + { + public Match RegexMatch { get; set; } + public string Prefix { get; set; } + public string Separator { get; set; } + public string Suffix { get; set; } + public string Token { get; set; } + public string CustomFormat { get; set; } + + public string DefaultValue(string defaultValue) + { + if (string.IsNullOrEmpty(Prefix) && string.IsNullOrEmpty(Suffix)) + { + return defaultValue; + } + else + { + return string.Empty; + } + } + } + + public enum MultiEpisodeStyle + { + Extend = 0, + Duplicate = 1, + Repeat = 2, + Scene = 3, + Range = 4, + PrefixedRange = 5 + } +} From e2d6e3916862ce6a4a113740c5b99d295fa66c8b Mon Sep 17 00:00:00 2001 From: Tim Turner Date: Fri, 27 Jan 2017 20:32:16 -0500 Subject: [PATCH 62/83] Add movie year to NotificationService (#489) --- src/NzbDrone.Core/Notifications/NotificationService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs index 0ee596330..9b0c80f3d 100644 --- a/src/NzbDrone.Core/Notifications/NotificationService.cs +++ b/src/NzbDrone.Core/Notifications/NotificationService.cs @@ -80,8 +80,9 @@ namespace NzbDrone.Core.Notifications qualityString += " Proper"; } - return string.Format("{0} [{1}]", + return string.Format("{0} ({1}) [{2}]", movie.Title, + movie.Year, qualityString); } From 8168cf82c514ea3c35de943fd1b9bbe8f2212dcb Mon Sep 17 00:00:00 2001 From: Tim Turner Date: Fri, 27 Jan 2017 21:24:01 -0500 Subject: [PATCH 63/83] Ensure the movie isn't delete when the folder isn't renamed (#491) --- src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs index c5a751420..3be8a8e17 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs @@ -138,7 +138,9 @@ namespace NzbDrone.Core.MediaFiles _mediaFileAttributeService.SetFilePermissions(destinationFilePath); - _diskProvider.DeleteFolder(oldMoviePath, true); + + if(oldMoviePath != newMoviePath) + _diskProvider.DeleteFolder(oldMoviePath, true); return movieFile; } From dd194d41be4e6c068343bf50ba8424bc42faa674 Mon Sep 17 00:00:00 2001 From: Tim Turner Date: Sat, 28 Jan 2017 06:29:58 -0500 Subject: [PATCH 64/83] Revert "Ensure the movie isn't delete when the folder isn't renamed (#491)" This reverts commit 8168cf82c514ea3c35de943fd1b9bbe8f2212dcb. --- src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs index 3be8a8e17..c5a751420 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs @@ -138,9 +138,7 @@ namespace NzbDrone.Core.MediaFiles _mediaFileAttributeService.SetFilePermissions(destinationFilePath); - - if(oldMoviePath != newMoviePath) - _diskProvider.DeleteFolder(oldMoviePath, true); + _diskProvider.DeleteFolder(oldMoviePath, true); return movieFile; } From 9953a5ed0658184c59583a88f58031997481fb5f Mon Sep 17 00:00:00 2001 From: Tim Turner Date: Sat, 28 Jan 2017 06:33:15 -0500 Subject: [PATCH 65/83] Revert "Add movie year to NotificationService (#489)" This reverts commit e2d6e3916862ce6a4a113740c5b99d295fa66c8b. --- src/NzbDrone.Core/Notifications/NotificationService.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs index 9b0c80f3d..0ee596330 100644 --- a/src/NzbDrone.Core/Notifications/NotificationService.cs +++ b/src/NzbDrone.Core/Notifications/NotificationService.cs @@ -80,9 +80,8 @@ namespace NzbDrone.Core.Notifications qualityString += " Proper"; } - return string.Format("{0} ({1}) [{2}]", + return string.Format("{0} [{1}]", movie.Title, - movie.Year, qualityString); } From 5d2ef4786bba512d6fcd9f232763ec6f5dcb3ff9 Mon Sep 17 00:00:00 2001 From: Tim Turner Date: Sat, 28 Jan 2017 06:49:02 -0500 Subject: [PATCH 66/83] Revert "Merge branch 'rename-existing-folder' into develop" This reverts commit a340bc4da3c90dcfa526dc49817d8f5ab54fe75e, reversing changes made to 4be7772d53073b49abfd2d3a784ec50b7c5e1064. --- .../Extensions/PathExtensions.cs | 5 - src/NzbDrone.Core/Extras/ExtraService.cs | 11 - .../MediaFiles/MovieFileMovingService.cs | 11 +- .../MediaFiles/RenameMovieFileService.cs | 8 +- .../Notifications/NotificationService.cs | 2 +- .../Notifications/Plex/PlexServerProxy.cs | 14 +- .../Notifications/Slack/Slack.cs | 4 +- .../Organizer/FileNameBuilder.cs | 655 +++++++++--------- src/UI/Movies/Files/FilesLayout.js | 13 - src/UI/Rename/RenamePreviewFormatView.js | 1 - .../RenamePreviewFormatViewTemplate.hbs | 1 - src/UI/Rename/RenamePreviewLayoutTemplate.hbs | 2 +- 12 files changed, 330 insertions(+), 397 deletions(-) diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index bee7a0533..63dc57884 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -64,11 +64,6 @@ namespace NzbDrone.Common.Extensions return childPath.Substring(parentPath.Length).Trim(Path.DirectorySeparatorChar); } - public static string GetRelativePathWithoutChildCheck(this string parentPath, string childPath) - { - return childPath.Substring(parentPath.Length).Trim(Path.DirectorySeparatorChar); - } - public static string GetParentPath(this string childPath) { var parentPath = childPath.TrimEnd('\\', '/'); diff --git a/src/NzbDrone.Core/Extras/ExtraService.cs b/src/NzbDrone.Core/Extras/ExtraService.cs index 8ba14e3d8..3b3a1f7c7 100644 --- a/src/NzbDrone.Core/Extras/ExtraService.cs +++ b/src/NzbDrone.Core/Extras/ExtraService.cs @@ -123,17 +123,6 @@ namespace NzbDrone.Core.Extras // } //} - // TODO - public void Handle(MovieFolderCreatedEvent message) - { - var movie = message.Movie; - - foreach(var extraFileManager in _extraFileManagers) - { - //extraFileManager.CreateAfterMovieImport(movie, message.MovieFolder); - } - } - public void Handle(EpisodeFolderCreatedEvent message) { var series = message.Series; diff --git a/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs index c5a751420..cf8acd6f9 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs @@ -116,12 +116,6 @@ namespace NzbDrone.Core.MediaFiles _diskTransferService.TransferFile(movieFilePath, destinationFilePath, mode); - var oldMoviePath = new OsPath(movieFilePath).Directory.FullPath.TrimEnd(Path.DirectorySeparatorChar); - - - var newMoviePath = new OsPath(destinationFilePath).Directory.FullPath.TrimEnd(Path.DirectorySeparatorChar); - movie.Path = newMoviePath; - movieFile.RelativePath = movie.Path.GetRelativePath(destinationFilePath); _updateMovieFileService.ChangeFileDateForFile(movieFile, movie); @@ -138,8 +132,6 @@ namespace NzbDrone.Core.MediaFiles _mediaFileAttributeService.SetFilePermissions(destinationFilePath); - _diskProvider.DeleteFolder(oldMoviePath, true); - return movieFile; } @@ -152,7 +144,6 @@ namespace NzbDrone.Core.MediaFiles { var movieFolder = Path.GetDirectoryName(filePath); var rootFolder = new OsPath(movieFolder).Directory.FullPath; - var fileName = Path.GetFileName(filePath); if (!_diskProvider.FolderExists(rootFolder)) { @@ -165,7 +156,7 @@ namespace NzbDrone.Core.MediaFiles if (!_diskProvider.FolderExists(movieFolder)) { CreateFolder(movieFolder); - newEvent.MovieFolder = movieFolder; + newEvent.SeriesFolder = movieFolder; changed = true; } diff --git a/src/NzbDrone.Core/MediaFiles/RenameMovieFileService.cs b/src/NzbDrone.Core/MediaFiles/RenameMovieFileService.cs index 4742922c4..9a4019f56 100644 --- a/src/NzbDrone.Core/MediaFiles/RenameMovieFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/RenameMovieFileService.cs @@ -71,9 +71,8 @@ namespace NzbDrone.Core.MediaFiles { MovieId = movie.Id, MovieFileId = file.Id, - ExistingPath = movieFilePath, - //NewPath = movie.Path.GetRelativePath(newPath) - NewPath = newPath + ExistingPath = file.RelativePath, + NewPath = movie.Path.GetRelativePath(newPath) }; } @@ -95,7 +94,6 @@ namespace NzbDrone.Core.MediaFiles _movieFileMover.MoveMovieFile(movieFile, movie); _mediaFileService.Update(movieFile); - _movieService.UpdateMovie(movie); renamed.Add(movieFile); _logger.Debug("Renamed movie file: {0}", movieFile); @@ -124,7 +122,7 @@ namespace NzbDrone.Core.MediaFiles public void Execute(RenameMovieCommand message) { - _logger.Debug("Renaming all files for selected movies"); + _logger.Debug("Renaming all files for selected movie"); var moviesToRename = _movieService.GetMovies(message.MovieIds); foreach(var movie in moviesToRename) diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs index 0ee596330..53c237159 100644 --- a/src/NzbDrone.Core/Notifications/NotificationService.cs +++ b/src/NzbDrone.Core/Notifications/NotificationService.cs @@ -210,7 +210,7 @@ namespace NzbDrone.Core.Notifications public void Handle(MovieDownloadedEvent message) { var downloadMessage = new DownloadMessage(); - downloadMessage.Message = GetMessage(message.Movie.Movie, message.Movie.Quality); + downloadMessage.Message = GetMessage(message.Movie.Movie, message.Movie.Quality); downloadMessage.Series = null; downloadMessage.EpisodeFile = null; downloadMessage.MovieFile = message.MovieFile; diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs index aa9e660fd..a0c49452f 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs @@ -17,7 +17,7 @@ namespace NzbDrone.Core.Notifications.Plex public interface IPlexServerProxy { List GetTvSections(PlexServerSettings settings); - List GetMovieSections(PlexServerSettings settings); + List GetMovieSections(PlexServerSettings settings); void Update(int sectionId, PlexServerSettings settings); void UpdateSeries(int metadataId, PlexServerSettings settings); string Version(PlexServerSettings settings); @@ -81,12 +81,12 @@ namespace NzbDrone.Core.Notifications.Plex return Json.Deserialize(response.Content) .Sections .Where(d => d.Type == "movie") - .Select(s => new PlexSection - { - Id = s.Id, - Language = s.Language, - Locations = s.Locations, - Type = s.Type + .Select(s => new PlexSection + { + Id = s.Id, + Language = s.Language, + Locations = s.Locations, + Type = s.Type }) .ToList(); } diff --git a/src/NzbDrone.Core/Notifications/Slack/Slack.cs b/src/NzbDrone.Core/Notifications/Slack/Slack.cs index 13e69f5a0..03b74c27f 100644 --- a/src/NzbDrone.Core/Notifications/Slack/Slack.cs +++ b/src/NzbDrone.Core/Notifications/Slack/Slack.cs @@ -86,8 +86,8 @@ namespace NzbDrone.Core.Notifications.Slack }; NotifySlack(payload); - } - + } + public override void OnRename(Series series) { var payload = new SlackPayload diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 85108ff49..c247555e6 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -1,349 +1,324 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using NLog; -using NzbDrone.Common.Cache; -using NzbDrone.Common.EnsureThat; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Organizer -{ - public interface IBuildFileNames - { - string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null); - string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namingConfig = null); - string BuildFilePath(Movie movie, string fileName, string extension); - string BuildFilePath(Series series, int seasonNumber, string fileName, string extension); - string BuildSeasonPath(Series series, int seasonNumber); - BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); - string GetSeriesFolder(Series series, NamingConfig namingConfig = null); - string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null); - string GetMovieFolder(Movie movie, NamingConfig namingConfig = null); - } - - public class FileNameBuilder : IBuildFileNames - { - private readonly INamingConfigService _namingConfigService; - private readonly IQualityDefinitionService _qualityDefinitionService; - private readonly ICached _episodeFormatCache; - private readonly ICached _absoluteEpisodeFormatCache; - private readonly Logger _logger; - - private static readonly Regex TitleRegex = new Regex(@"\{(?[- ._\[(]*)(?(?:[a-z0-9]+)(?:(?[- ._]+)(?:[a-z0-9]+))?)(?::(?[a-z0-9]+))?(?[- ._)\]]*)\}", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly Regex EpisodeRegex = new Regex(@"(?\{episode(?:\:0+)?})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using NLog; +using NzbDrone.Common.Cache; +using NzbDrone.Common.EnsureThat; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Organizer +{ + public interface IBuildFileNames + { + string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null); + string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namingConfig = null); + string BuildFilePath(Movie movie, string fileName, string extension); + string BuildFilePath(Series series, int seasonNumber, string fileName, string extension); + string BuildSeasonPath(Series series, int seasonNumber); + BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); + string GetSeriesFolder(Series series, NamingConfig namingConfig = null); + string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null); + string GetMovieFolder(Movie movie, NamingConfig namingConfig = null); + } + + public class FileNameBuilder : IBuildFileNames + { + private readonly INamingConfigService _namingConfigService; + private readonly IQualityDefinitionService _qualityDefinitionService; + private readonly ICached _episodeFormatCache; + private readonly ICached _absoluteEpisodeFormatCache; + private readonly Logger _logger; + + private static readonly Regex TitleRegex = new Regex(@"\{(?[- ._\[(]*)(?(?:[a-z0-9]+)(?:(?[- ._]+)(?:[a-z0-9]+))?)(?::(?[a-z0-9]+))?(?[- ._)\]]*)\}", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex EpisodeRegex = new Regex(@"(?\{episode(?:\:0+)?})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex TagsRegex = new Regex(@"(?\{tags(?:\:0+)?})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly Regex SeasonRegex = new Regex(@"(?\{season(?:\:0+)?})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly Regex AbsoluteEpisodeRegex = new Regex(@"(?\{absolute(?:\:0+)?})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?(?<=})[- ._]+?)?(?s?{season(?:\:0+)?}(?[- ._]?[ex])(?{episode(?:\:0+)?}))(?[- ._]+?(?={))?", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static readonly Regex AbsoluteEpisodePatternRegex = new Regex(@"(?(?<=})[- ._]+?)?(?{absolute(?:\:0+)?})(?[- ._]+?(?={))?", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static readonly Regex AirDateRegex = new Regex(@"\{Air(\s|\W|_)Date\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static readonly Regex SeriesTitleRegex = new Regex(@"(?\{(?:Series)(?[- ._])(Clean)?Title\})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static readonly Regex MovieTitleRegex = new Regex(@"(?\{((?:(Movie|Original))(?[- ._])(Clean)?Title(The)?)\})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled); - private static readonly Regex TrimSeparatorsRegex = new Regex(@"[- ._]$", RegexOptions.Compiled); - - private static readonly Regex ScenifyRemoveChars = new Regex(@"(?<=\s)(,|<|>|\/|\\|;|:|'|""|\||`|~|!|\?|@|$|%|^|\*|-|_|=){1}(?=\s)|('|:|\?|,)(?=(?:(?:s|m)\s)|\s|$)|(\(|\)|\[|\]|\{|\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex ScenifyReplaceChars = new Regex(@"[\/]", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - //TODO: Support Written numbers (One, Two, etc) and Roman Numerals (I, II, III etc) - private static readonly Regex MultiPartCleanupRegex = new Regex(@"(?:\(\d+\)|(Part|Pt\.?)\s?\d+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly char[] EpisodeTitleTrimCharacters = new[] { ' ', '.', '?' }; - - public FileNameBuilder(INamingConfigService namingConfigService, - IQualityDefinitionService qualityDefinitionService, - ICacheManager cacheManager, - Logger logger) - { - _namingConfigService = namingConfigService; - _qualityDefinitionService = qualityDefinitionService; - //_movieFormatCache = cacheManager.GetCache(GetType(), "movieFormat"); - _episodeFormatCache = cacheManager.GetCache(GetType(), "episodeFormat"); - _absoluteEpisodeFormatCache = cacheManager.GetCache(GetType(), "absoluteEpisodeFormat"); - _logger = logger; - } - - public string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null) - { - if (namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - if (!namingConfig.RenameEpisodes) - { - return GetOriginalTitle(episodeFile); - } - - if (namingConfig.StandardEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Standard) - { - throw new NamingFormatException("Standard episode format cannot be empty"); - } - - if (namingConfig.DailyEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Daily) - { - throw new NamingFormatException("Daily episode format cannot be empty"); - } - - if (namingConfig.AnimeEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Anime) - { - throw new NamingFormatException("Anime episode format cannot be empty"); - } - - var pattern = namingConfig.StandardEpisodeFormat; - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - episodes = episodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber).ToList(); - - if (series.SeriesType == SeriesTypes.Daily) - { - pattern = namingConfig.DailyEpisodeFormat; - } - - if (series.SeriesType == SeriesTypes.Anime && episodes.All(e => e.AbsoluteEpisodeNumber.HasValue)) - { - pattern = namingConfig.AnimeEpisodeFormat; - } - - pattern = AddSeasonEpisodeNumberingTokens(pattern, tokenHandlers, episodes, namingConfig); - pattern = AddAbsoluteNumberingTokens(pattern, tokenHandlers, series, episodes, namingConfig); - - AddSeriesTokens(tokenHandlers, series); - AddEpisodeTokens(tokenHandlers, episodes); - AddEpisodeFileTokens(tokenHandlers, episodeFile); - AddQualityTokens(tokenHandlers, series, episodeFile); - AddMediaInfoTokens(tokenHandlers, episodeFile); - - var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); - fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); - fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); - - return fileName; - } - - public string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namingConfig = null) - { - if (namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - if (!namingConfig.RenameEpisodes) - { - return GetOriginalTitle(movieFile); - } - - var pattern = namingConfig.StandardMovieFormat; - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - AddMovieTokens(tokenHandlers, movie); - AddReleaseDateTokens(tokenHandlers, movie.Year); - AddImdbIdTokens(tokenHandlers, movie.ImdbId); - AddQualityTokens(tokenHandlers, movie, movieFile); - AddMediaInfoTokens(tokenHandlers, movieFile); - AddMovieFileTokens(tokenHandlers, movieFile); - AddTagsTokens(tokenHandlers, movieFile); - - var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); - fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); - fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); - - return fileName; - } - - public string BuildFilePath(Series series, int seasonNumber, string fileName, string extension) - { - Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); - - var path = BuildSeasonPath(series, seasonNumber); - - return Path.Combine(path, fileName + extension); - } - - public string BuildFilePath(Movie movie, string fileName, string extension) - { - Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); - - var path = BuildMoviePath(movie); - - return Path.Combine(path, fileName + extension); - } - - public string BuildMoviePath(Movie movie) + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex SeasonRegex = new Regex(@"(?\{season(?:\:0+)?})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex AbsoluteEpisodeRegex = new Regex(@"(?\{absolute(?:\:0+)?})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?(?<=})[- ._]+?)?(?s?{season(?:\:0+)?}(?[- ._]?[ex])(?{episode(?:\:0+)?}))(?[- ._]+?(?={))?", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex AbsoluteEpisodePatternRegex = new Regex(@"(?(?<=})[- ._]+?)?(?{absolute(?:\:0+)?})(?[- ._]+?(?={))?", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex AirDateRegex = new Regex(@"\{Air(\s|\W|_)Date\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex SeriesTitleRegex = new Regex(@"(?\{(?:Series)(?[- ._])(Clean)?Title\})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex MovieTitleRegex = new Regex(@"(?\{((?:(Movie|Original))(?[- ._])(Clean)?Title(The)?)\})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled); + private static readonly Regex TrimSeparatorsRegex = new Regex(@"[- ._]$", RegexOptions.Compiled); + + private static readonly Regex ScenifyRemoveChars = new Regex(@"(?<=\s)(,|<|>|\/|\\|;|:|'|""|\||`|~|!|\?|@|$|%|^|\*|-|_|=){1}(?=\s)|('|:|\?|,)(?=(?:(?:s|m)\s)|\s|$)|(\(|\)|\[|\]|\{|\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex ScenifyReplaceChars = new Regex(@"[\/]", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + //TODO: Support Written numbers (One, Two, etc) and Roman Numerals (I, II, III etc) + private static readonly Regex MultiPartCleanupRegex = new Regex(@"(?:\(\d+\)|(Part|Pt\.?)\s?\d+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly char[] EpisodeTitleTrimCharacters = new[] { ' ', '.', '?' }; + + public FileNameBuilder(INamingConfigService namingConfigService, + IQualityDefinitionService qualityDefinitionService, + ICacheManager cacheManager, + Logger logger) { + _namingConfigService = namingConfigService; + _qualityDefinitionService = qualityDefinitionService; + //_movieFormatCache = cacheManager.GetCache(GetType(), "movieFormat"); + _episodeFormatCache = cacheManager.GetCache(GetType(), "episodeFormat"); + _absoluteEpisodeFormatCache = cacheManager.GetCache(GetType(), "absoluteEpisodeFormat"); + _logger = logger; + } + + public string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null) + { + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + if (!namingConfig.RenameEpisodes) + { + return GetOriginalTitle(episodeFile); + } + + if (namingConfig.StandardEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Standard) + { + throw new NamingFormatException("Standard episode format cannot be empty"); + } + + if (namingConfig.DailyEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Daily) + { + throw new NamingFormatException("Daily episode format cannot be empty"); + } + + if (namingConfig.AnimeEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Anime) + { + throw new NamingFormatException("Anime episode format cannot be empty"); + } + + var pattern = namingConfig.StandardEpisodeFormat; + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + episodes = episodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber).ToList(); + + if (series.SeriesType == SeriesTypes.Daily) + { + pattern = namingConfig.DailyEpisodeFormat; + } + + if (series.SeriesType == SeriesTypes.Anime && episodes.All(e => e.AbsoluteEpisodeNumber.HasValue)) + { + pattern = namingConfig.AnimeEpisodeFormat; + } + + pattern = AddSeasonEpisodeNumberingTokens(pattern, tokenHandlers, episodes, namingConfig); + pattern = AddAbsoluteNumberingTokens(pattern, tokenHandlers, series, episodes, namingConfig); + + AddSeriesTokens(tokenHandlers, series); + AddEpisodeTokens(tokenHandlers, episodes); + AddEpisodeFileTokens(tokenHandlers, episodeFile); + AddQualityTokens(tokenHandlers, series, episodeFile); + AddMediaInfoTokens(tokenHandlers, episodeFile); + + var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); + fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); + fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); + + return fileName; + } + + public string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namingConfig = null) + { + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + if (!namingConfig.RenameEpisodes) + { + return GetOriginalTitle(movieFile); + } + + //TODO: Update namingConfig for Movies! + var pattern = namingConfig.StandardMovieFormat; + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + AddMovieTokens(tokenHandlers, movie); + AddReleaseDateTokens(tokenHandlers, movie.Year); //In case we want to separate the year + AddImdbIdTokens(tokenHandlers, movie.ImdbId); + AddQualityTokens(tokenHandlers, movie, movieFile); + AddMediaInfoTokens(tokenHandlers, movieFile); + AddMovieFileTokens(tokenHandlers, movieFile); + AddTagsTokens(tokenHandlers, movieFile); + + var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); + fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); + fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); + + return fileName; + } + + public string BuildFilePath(Series series, int seasonNumber, string fileName, string extension) + { + Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); + + var path = BuildSeasonPath(series, seasonNumber); + + return Path.Combine(path, fileName + extension); + } + + public string BuildFilePath(Movie movie, string fileName, string extension) + { + Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); + var path = movie.Path; - var directory = new DirectoryInfo(path).Name; - var parentDirectoryPath = new DirectoryInfo(path).Parent.FullName; - var namingConfig = _namingConfigService.GetConfig(); - var movieFile = movie.MovieFile; + return Path.Combine(path, fileName + extension); + } - var pattern = namingConfig.MovieFolderFormat; - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - AddMovieTokens(tokenHandlers, movie); - AddReleaseDateTokens(tokenHandlers, movie.Year); - AddImdbIdTokens(tokenHandlers, movie.ImdbId); - AddQualityTokens(tokenHandlers, movie, movieFile); - AddMediaInfoTokens(tokenHandlers, movieFile); - AddMovieFileTokens(tokenHandlers, movieFile); - - var directoryName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); - directoryName = FileNameCleanupRegex.Replace(directoryName, match => match.Captures[0].Value[0].ToString()); - directoryName = TrimSeparatorsRegex.Replace(directoryName, string.Empty); + public string BuildSeasonPath(Series series, int seasonNumber) + { + var path = series.Path; - return Path.Combine(parentDirectoryPath, directoryName); - } - - public string BuildSeasonPath(Series series, int seasonNumber) - { - var path = series.Path; - - if (series.SeasonFolder) - { - if (seasonNumber == 0) - { - path = Path.Combine(path, "Specials"); - } - else - { - var seasonFolder = GetSeasonFolder(series, seasonNumber); - - seasonFolder = CleanFileName(seasonFolder); - - path = Path.Combine(path, seasonFolder); - } - } - - return path; - } - - public BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec) - { - return new BasicNamingConfig(); //For now let's be lazy - - var episodeFormat = GetEpisodeFormat(nameSpec.StandardEpisodeFormat).LastOrDefault(); - - if (episodeFormat == null) - { - return new BasicNamingConfig(); - } - - var basicNamingConfig = new BasicNamingConfig - { - Separator = episodeFormat.Separator, - NumberStyle = episodeFormat.SeasonEpisodePattern - }; - - var titleTokens = TitleRegex.Matches(nameSpec.StandardEpisodeFormat); - - foreach (Match match in titleTokens) - { - var separator = match.Groups["separator"].Value; - var token = match.Groups["token"].Value; - - if (!separator.Equals(" ")) - { - basicNamingConfig.ReplaceSpaces = true; - } - - if (token.StartsWith("{Series", StringComparison.InvariantCultureIgnoreCase)) - { - basicNamingConfig.IncludeSeriesTitle = true; - } - - if (token.StartsWith("{Episode", StringComparison.InvariantCultureIgnoreCase)) - { - basicNamingConfig.IncludeEpisodeTitle = true; - } - - if (token.StartsWith("{Quality", StringComparison.InvariantCultureIgnoreCase)) - { - basicNamingConfig.IncludeQuality = true; - } - } - - return basicNamingConfig; - } - - public string GetSeriesFolder(Series series, NamingConfig namingConfig = null) - { - if (namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - AddSeriesTokens(tokenHandlers, series); - - return CleanFolderName(ReplaceTokens(namingConfig.SeriesFolderFormat, tokenHandlers, namingConfig)); - } - - public string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null) - { - if (namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - AddSeriesTokens(tokenHandlers, series); - AddSeasonTokens(tokenHandlers, seasonNumber); - - return CleanFolderName(ReplaceTokens(namingConfig.SeasonFolderFormat, tokenHandlers, namingConfig)); - } - - public string GetMovieFolder(Movie movie, NamingConfig namingConfig = null) - { - if(namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - AddMovieTokens(tokenHandlers, movie); - AddReleaseDateTokens(tokenHandlers, movie.Year); - AddImdbIdTokens(tokenHandlers, movie.ImdbId); - - return CleanFolderName(ReplaceTokens(namingConfig.MovieFolderFormat, tokenHandlers, namingConfig)); - } - - public static string CleanTitle(string title) - { - title = title.Replace("&", "and"); - title = ScenifyReplaceChars.Replace(title, " "); - title = ScenifyRemoveChars.Replace(title, string.Empty); - - return title; - } + if (series.SeasonFolder) + { + if (seasonNumber == 0) + { + path = Path.Combine(path, "Specials"); + } + else + { + var seasonFolder = GetSeasonFolder(series, seasonNumber); + + seasonFolder = CleanFileName(seasonFolder); + + path = Path.Combine(path, seasonFolder); + } + } + + return path; + } + + public BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec) + { + return new BasicNamingConfig(); //For now let's be lazy + + var episodeFormat = GetEpisodeFormat(nameSpec.StandardEpisodeFormat).LastOrDefault(); + + if (episodeFormat == null) + { + return new BasicNamingConfig(); + } + + var basicNamingConfig = new BasicNamingConfig + { + Separator = episodeFormat.Separator, + NumberStyle = episodeFormat.SeasonEpisodePattern + }; + + var titleTokens = TitleRegex.Matches(nameSpec.StandardEpisodeFormat); + + foreach (Match match in titleTokens) + { + var separator = match.Groups["separator"].Value; + var token = match.Groups["token"].Value; + + if (!separator.Equals(" ")) + { + basicNamingConfig.ReplaceSpaces = true; + } + + if (token.StartsWith("{Series", StringComparison.InvariantCultureIgnoreCase)) + { + basicNamingConfig.IncludeSeriesTitle = true; + } + + if (token.StartsWith("{Episode", StringComparison.InvariantCultureIgnoreCase)) + { + basicNamingConfig.IncludeEpisodeTitle = true; + } + + if (token.StartsWith("{Quality", StringComparison.InvariantCultureIgnoreCase)) + { + basicNamingConfig.IncludeQuality = true; + } + } + + return basicNamingConfig; + } + + public string GetSeriesFolder(Series series, NamingConfig namingConfig = null) + { + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + AddSeriesTokens(tokenHandlers, series); + + return CleanFolderName(ReplaceTokens(namingConfig.SeriesFolderFormat, tokenHandlers, namingConfig)); + } + + public string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null) + { + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + AddSeriesTokens(tokenHandlers, series); + AddSeasonTokens(tokenHandlers, seasonNumber); + + return CleanFolderName(ReplaceTokens(namingConfig.SeasonFolderFormat, tokenHandlers, namingConfig)); + } + + public string GetMovieFolder(Movie movie, NamingConfig namingConfig = null) + { + if(namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + AddMovieTokens(tokenHandlers, movie); + AddReleaseDateTokens(tokenHandlers, movie.Year); + AddImdbIdTokens(tokenHandlers, movie.ImdbId); + + return CleanFolderName(ReplaceTokens(namingConfig.MovieFolderFormat, tokenHandlers, namingConfig)); + } + + public static string CleanTitle(string title) + { + title = title.Replace("&", "and"); + title = ScenifyReplaceChars.Replace(title, " "); + title = ScenifyRemoveChars.Replace(title, string.Empty); + + return title; + } public static string TitleThe(string title) { diff --git a/src/UI/Movies/Files/FilesLayout.js b/src/UI/Movies/Files/FilesLayout.js index 15b1e32af..3e6dd2bdd 100644 --- a/src/UI/Movies/Files/FilesLayout.js +++ b/src/UI/Movies/Files/FilesLayout.js @@ -74,19 +74,6 @@ module.exports = Marionette.Layout.extend({ var file = movie.model.get("movieFile"); this.filesCollection.add(file); //this.listenTo(this.releaseCollection, 'sync', this._showSearchResults); - - this.listenTo(this.model, 'change', function(model, options) { - if (options && options.changeSource === 'signalr') { - this._refresh(movie); - } - }); - }, - - _refresh : function(movie) { - this.filesCollection = new FilesCollection(); - var file = movie.model.get("movieFile"); - this.filesCollection.add(file); - this.onShow(); }, onShow : function() { diff --git a/src/UI/Rename/RenamePreviewFormatView.js b/src/UI/Rename/RenamePreviewFormatView.js index 141b42f8b..5796e6748 100644 --- a/src/UI/Rename/RenamePreviewFormatView.js +++ b/src/UI/Rename/RenamePreviewFormatView.js @@ -9,7 +9,6 @@ module.exports = Marionette.ItemView.extend({ //var type = this.model.get('seriesType'); return { rename : this.naming.get('renameEpisodes'), - folderFormat: this.naming.get('movieFolderFormat'), format : this.naming.get('standardMovieFormat') }; }, diff --git a/src/UI/Rename/RenamePreviewFormatViewTemplate.hbs b/src/UI/Rename/RenamePreviewFormatViewTemplate.hbs index 99a1f6462..77297f56b 100644 --- a/src/UI/Rename/RenamePreviewFormatViewTemplate.hbs +++ b/src/UI/Rename/RenamePreviewFormatViewTemplate.hbs @@ -1,4 +1,3 @@ {{#if rename}} -Folder Naming pattern: {{folderFormat}}
Naming pattern: {{format}} {{/if}} diff --git a/src/UI/Rename/RenamePreviewLayoutTemplate.hbs b/src/UI/Rename/RenamePreviewLayoutTemplate.hbs index ee1f2571c..a3aa41d51 100644 --- a/src/UI/Rename/RenamePreviewLayoutTemplate.hbs +++ b/src/UI/Rename/RenamePreviewLayoutTemplate.hbs @@ -9,7 +9,7 @@
-
-
-
- +
+
+
+
+ -
-
-
+
+
From 1fd909cff6f0d4a1025e7b0d2fd5ef792e4446d1 Mon Sep 17 00:00:00 2001 From: Tim Turner Date: Sat, 28 Jan 2017 13:27:54 -0500 Subject: [PATCH 77/83] Net Import UI Updates - Change name to "StevenLu" to fix - Hide header on New List modal - Show "Import Selected" only after a list has been fetched --- src/NzbDrone.Core/NetImport/StevenLu/StevenLuImport.cs | 2 +- src/UI/AddMovies/List/AddFromListView.js | 3 +++ src/UI/Settings/NetImport/Add/NetImportSchemaModal.js | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/NetImport/StevenLu/StevenLuImport.cs b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuImport.cs index 1f3b76039..95c1d9b9e 100644 --- a/src/NzbDrone.Core/NetImport/StevenLu/StevenLuImport.cs +++ b/src/NzbDrone.Core/NetImport/StevenLu/StevenLuImport.cs @@ -15,7 +15,7 @@ namespace NzbDrone.Core.NetImport.StevenLu { public class StevenLuImport : HttpNetImportBase { - public override string Name => "Popular movies from StevenLu"; + public override string Name => "StevenLu"; public override bool Enabled => true; public override bool EnableAuto => true; diff --git a/src/UI/AddMovies/List/AddFromListView.js b/src/UI/AddMovies/List/AddFromListView.js index d2605c74f..335380f94 100644 --- a/src/UI/AddMovies/List/AddFromListView.js +++ b/src/UI/AddMovies/List/AddFromListView.js @@ -102,10 +102,12 @@ module.exports = Marionette.Layout.extend({ onRender : function() { var self = this; + this.ui.importSelected.hide(); }, onShow : function() { this.ui.moviesSearch.focus(); + }, search : function(options) { @@ -232,6 +234,7 @@ module.exports = Marionette.Layout.extend({ className : 'table table-hover' }); this.fetchResult.show(this.importGrid); + this.ui.importSelected.show(); } }, diff --git a/src/UI/Settings/NetImport/Add/NetImportSchemaModal.js b/src/UI/Settings/NetImport/Add/NetImportSchemaModal.js index bd75e704c..42423ef18 100644 --- a/src/UI/Settings/NetImport/Add/NetImportSchemaModal.js +++ b/src/UI/Settings/NetImport/Add/NetImportSchemaModal.js @@ -19,9 +19,10 @@ module.exports = { var groups = schemaCollection.groupBy(function(model, iterator) { return model.get('protocol'); }); + //key is "undefined", which is being placed in the header var modelCollection = _.map(groups, function(values, key, list) { return { - "header" : key, + //"header" : key, collection : values }; }); From b6e4f5359790b275e3c194d99264401645af12d2 Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Sat, 28 Jan 2017 14:59:21 -0500 Subject: [PATCH 78/83] Make NetImport sync interval work (needs some testing) --- .../Config/NetImportConfigModule.cs | 2 + .../Config/NetImportConfigResource.cs | 2 + src/NzbDrone.Api/NzbDrone.Api.csproj | 1 + .../NetImportSyncIntervalValidator.cs | 34 ++ .../Validation/RuleBuilderExtensions.cs | 5 + .../Configuration/ConfigService.cs | 7 + .../Configuration/IConfigService.cs | 2 + src/NzbDrone.Core/Jobs/TaskManager.cs | 24 +- .../Organizer/FileNameBuilder.cs | 370 ++---------------- .../Options/NetImportOptionsViewTemplate.hbs | 2 +- 10 files changed, 102 insertions(+), 347 deletions(-) create mode 100644 src/NzbDrone.Api/Validation/NetImportSyncIntervalValidator.cs diff --git a/src/NzbDrone.Api/Config/NetImportConfigModule.cs b/src/NzbDrone.Api/Config/NetImportConfigModule.cs index 3cd194116..f805e8c2d 100644 --- a/src/NzbDrone.Api/Config/NetImportConfigModule.cs +++ b/src/NzbDrone.Api/Config/NetImportConfigModule.cs @@ -10,6 +10,8 @@ namespace NzbDrone.Api.Config public NetImportConfigModule(IConfigService configService) : base(configService) { + SharedValidator.RuleFor(c => c.NetImportSyncInterval) + .IsValidNetImportSyncInterval(); } protected override NetImportConfigResource ToResource(IConfigService model) diff --git a/src/NzbDrone.Api/Config/NetImportConfigResource.cs b/src/NzbDrone.Api/Config/NetImportConfigResource.cs index 7b32aefe3..a1502375c 100644 --- a/src/NzbDrone.Api/Config/NetImportConfigResource.cs +++ b/src/NzbDrone.Api/Config/NetImportConfigResource.cs @@ -5,6 +5,7 @@ namespace NzbDrone.Api.Config { public class NetImportConfigResource : RestResource { + public int NetImportSyncInterval { get; set; } } public static class NetImportConfigResourceMapper @@ -13,6 +14,7 @@ namespace NzbDrone.Api.Config { return new NetImportConfigResource { + NetImportSyncInterval = model.NetImportSyncInterval }; } } diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 1f0542788..d467e397e 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -260,6 +260,7 @@ + diff --git a/src/NzbDrone.Api/Validation/NetImportSyncIntervalValidator.cs b/src/NzbDrone.Api/Validation/NetImportSyncIntervalValidator.cs new file mode 100644 index 000000000..b44b3f9e4 --- /dev/null +++ b/src/NzbDrone.Api/Validation/NetImportSyncIntervalValidator.cs @@ -0,0 +1,34 @@ +using FluentValidation.Validators; + +namespace NzbDrone.Api.Validation +{ + public class NetImportSyncIntervalValidator : PropertyValidator + { + public NetImportSyncIntervalValidator() + : base("Must be between 10 and 1440 or 0 to disable") + { + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) + { + return true; + } + + var value = (int)context.PropertyValue; + + if (value == 0) + { + return true; + } + + if (value >= 10 && value <= 1440) + { + return true; + } + + return false; + } + } +} diff --git a/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs b/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs index 01a3a4f75..4684d3f12 100644 --- a/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs +++ b/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs @@ -36,5 +36,10 @@ namespace NzbDrone.Api.Validation { return ruleBuilder.SetValidator(new RssSyncIntervalValidator()); } + + public static IRuleBuilderOptions IsValidNetImportSyncInterval(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.SetValidator(new NetImportSyncIntervalValidator()); + } } } diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index d19cddd67..639bb69d6 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -105,6 +105,13 @@ namespace NzbDrone.Core.Configuration set { SetValue("RssSyncInterval", value); } } + public int NetImportSyncInterval + { + get { return GetValueInt("NetImportSyncInterval", 60); } + + set { SetValue("NetImportSyncInterval", value); } + } + public int MinimumAge { get { return GetValueInt("MinimumAge", 0); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index e17d8d6dc..a2d56e778 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -46,6 +46,8 @@ namespace NzbDrone.Core.Configuration int RssSyncInterval { get; set; } int MinimumAge { get; set; } + int NetImportSyncInterval { get; set; } + //UI int FirstDayOfWeek { get; set; } string CalendarWeekColumnHeader { get; set; } diff --git a/src/NzbDrone.Core/Jobs/TaskManager.cs b/src/NzbDrone.Core/Jobs/TaskManager.cs index 52e564e5d..184dfedb9 100644 --- a/src/NzbDrone.Core/Jobs/TaskManager.cs +++ b/src/NzbDrone.Core/Jobs/TaskManager.cs @@ -75,7 +75,6 @@ namespace NzbDrone.Core.Jobs new ScheduledTask{ Interval = 5, TypeName = typeof(MessagingCleanupCommand).FullName}, new ScheduledTask{ Interval = updateInterval, TypeName = typeof(ApplicationUpdateCommand).FullName}, // new ScheduledTask{ Interval = 3*60, TypeName = typeof(UpdateSceneMappingCommand).FullName}, - new ScheduledTask{ Interval = 12*60, TypeName = typeof(NetImportSyncCommand).FullName}, new ScheduledTask{ Interval = 6*60, TypeName = typeof(CheckHealthCommand).FullName}, new ScheduledTask{ Interval = 24*60, TypeName = typeof(RefreshMovieCommand).FullName}, new ScheduledTask{ Interval = 24*60, TypeName = typeof(HousekeepingCommand).FullName}, @@ -87,6 +86,12 @@ namespace NzbDrone.Core.Jobs TypeName = typeof(RssSyncCommand).FullName }, + new ScheduledTask + { + Interval = GetNetImportSyncInterval(), + TypeName = typeof(NetImportSyncCommand).FullName + }, + new ScheduledTask { Interval = _configService.DownloadedEpisodesScanInterval, @@ -140,6 +145,23 @@ namespace NzbDrone.Core.Jobs return interval; } + private int GetNetImportSyncInterval() + { + var interval = _configService.NetImportSyncInterval; + + if (interval > 0 && interval < 10) + { + return 10; + } + + if (interval < 0) + { + return 0; + } + + return interval; + } + public void Handle(CommandExecutedEvent message) { var scheduledTask = _scheduledTaskRepository.All().SingleOrDefault(c => c.TypeName == message.Command.Body.GetType().FullName); diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index f6d5b1ecd..32422092e 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -1,337 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using NLog; -using NzbDrone.Common.Cache; -using NzbDrone.Common.EnsureThat; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Organizer -{ - public interface IBuildFileNames - { - string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null); - string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namingConfig = null); - string BuildFilePath(Movie movie, string fileName, string extension); - string BuildFilePath(Series series, int seasonNumber, string fileName, string extension); - string BuildSeasonPath(Series series, int seasonNumber); - BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); - string GetSeriesFolder(Series series, NamingConfig namingConfig = null); - string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null); - string GetMovieFolder(Movie movie, NamingConfig namingConfig = null); - } - - public class FileNameBuilder : IBuildFileNames - { - private readonly INamingConfigService _namingConfigService; - private readonly IQualityDefinitionService _qualityDefinitionService; - private readonly ICached _episodeFormatCache; - private readonly ICached _absoluteEpisodeFormatCache; - private readonly Logger _logger; - - private static readonly Regex TitleRegex = new Regex(@"\{(?[- ._\[(]*)(?(?:[a-z0-9]+)(?:(?[- ._]+)(?:[a-z0-9]+))?)(?::(?[a-z0-9]+))?(?[- ._)\]]*)\}", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly Regex EpisodeRegex = new Regex(@"(?\{episode(?:\:0+)?})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly Regex SeasonRegex = new Regex(@"(?\{season(?:\:0+)?})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly Regex AbsoluteEpisodeRegex = new Regex(@"(?\{absolute(?:\:0+)?})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?(?<=})[- ._]+?)?(?s?{season(?:\:0+)?}(?[- ._]?[ex])(?{episode(?:\:0+)?}))(?[- ._]+?(?={))?", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static readonly Regex AbsoluteEpisodePatternRegex = new Regex(@"(?(?<=})[- ._]+?)?(?{absolute(?:\:0+)?})(?[- ._]+?(?={))?", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static readonly Regex AirDateRegex = new Regex(@"\{Air(\s|\W|_)Date\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static readonly Regex SeriesTitleRegex = new Regex(@"(?\{(?:Series)(?[- ._])(Clean)?Title\})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static readonly Regex MovieTitleRegex = new Regex(@"(?\{((?:(Movie|Original))(?[- ._])(Clean)?Title(The)?)\})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled); - private static readonly Regex TrimSeparatorsRegex = new Regex(@"[- ._]$", RegexOptions.Compiled); - - private static readonly Regex ScenifyRemoveChars = new Regex(@"(?<=\s)(,|<|>|\/|\\|;|:|'|""|\||`|~|!|\?|@|$|%|^|\*|-|_|=){1}(?=\s)|('|:|\?|,)(?=(?:(?:s|m)\s)|\s|$)|(\(|\)|\[|\]|\{|\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex ScenifyReplaceChars = new Regex(@"[\/]", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - //TODO: Support Written numbers (One, Two, etc) and Roman Numerals (I, II, III etc) - private static readonly Regex MultiPartCleanupRegex = new Regex(@"(?:\(\d+\)|(Part|Pt\.?)\s?\d+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly char[] EpisodeTitleTrimCharacters = new[] { ' ', '.', '?' }; - - public FileNameBuilder(INamingConfigService namingConfigService, - IQualityDefinitionService qualityDefinitionService, - ICacheManager cacheManager, - Logger logger) - { - _namingConfigService = namingConfigService; - _qualityDefinitionService = qualityDefinitionService; - //_movieFormatCache = cacheManager.GetCache(GetType(), "movieFormat"); - _episodeFormatCache = cacheManager.GetCache(GetType(), "episodeFormat"); - _absoluteEpisodeFormatCache = cacheManager.GetCache(GetType(), "absoluteEpisodeFormat"); - _logger = logger; - } - - public string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null) - { - if (namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - if (!namingConfig.RenameEpisodes) - { - return GetOriginalTitle(episodeFile); - } - - if (namingConfig.StandardEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Standard) - { - throw new NamingFormatException("Standard episode format cannot be empty"); - } - - if (namingConfig.DailyEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Daily) - { - throw new NamingFormatException("Daily episode format cannot be empty"); - } - - if (namingConfig.AnimeEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Anime) - { - throw new NamingFormatException("Anime episode format cannot be empty"); - } - - var pattern = namingConfig.StandardEpisodeFormat; - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - episodes = episodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber).ToList(); - - if (series.SeriesType == SeriesTypes.Daily) - { - pattern = namingConfig.DailyEpisodeFormat; - } - - if (series.SeriesType == SeriesTypes.Anime && episodes.All(e => e.AbsoluteEpisodeNumber.HasValue)) - { - pattern = namingConfig.AnimeEpisodeFormat; - } - - pattern = AddSeasonEpisodeNumberingTokens(pattern, tokenHandlers, episodes, namingConfig); - pattern = AddAbsoluteNumberingTokens(pattern, tokenHandlers, series, episodes, namingConfig); - - AddSeriesTokens(tokenHandlers, series); - AddEpisodeTokens(tokenHandlers, episodes); - AddEpisodeFileTokens(tokenHandlers, episodeFile); - AddQualityTokens(tokenHandlers, series, episodeFile); - AddMediaInfoTokens(tokenHandlers, episodeFile); - - var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); - fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); - fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); - - return fileName; - } - - public string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namingConfig = null) - { - if (namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - if (!namingConfig.RenameEpisodes) - { - return GetOriginalTitle(movieFile); - } - - //TODO: Update namingConfig for Movies! - var pattern = namingConfig.StandardMovieFormat; - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - AddMovieTokens(tokenHandlers, movie); - AddReleaseDateTokens(tokenHandlers, movie.Year); //In case we want to separate the year - AddImdbIdTokens(tokenHandlers, movie.ImdbId); - AddQualityTokens(tokenHandlers, movie, movieFile); - AddMediaInfoTokens(tokenHandlers, movieFile); - AddMovieFileTokens(tokenHandlers, movieFile); - - var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); - fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); - fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); - - return fileName; - } - - public string BuildFilePath(Series series, int seasonNumber, string fileName, string extension) - { - Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); - - var path = BuildSeasonPath(series, seasonNumber); - - return Path.Combine(path, fileName + extension); - } - - public string BuildFilePath(Movie movie, string fileName, string extension) - { - Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); - - var path = movie.Path; - - return Path.Combine(path, fileName + extension); - } - - public string BuildSeasonPath(Series series, int seasonNumber) - { - var path = series.Path; - - if (series.SeasonFolder) - { - if (seasonNumber == 0) - { - path = Path.Combine(path, "Specials"); - } - else - { - var seasonFolder = GetSeasonFolder(series, seasonNumber); - - seasonFolder = CleanFileName(seasonFolder); - - path = Path.Combine(path, seasonFolder); - } - } - - return path; - } - - public BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec) - { - return new BasicNamingConfig(); //For now let's be lazy - - var episodeFormat = GetEpisodeFormat(nameSpec.StandardEpisodeFormat).LastOrDefault(); - - if (episodeFormat == null) - { - return new BasicNamingConfig(); - } - - var basicNamingConfig = new BasicNamingConfig - { - Separator = episodeFormat.Separator, - NumberStyle = episodeFormat.SeasonEpisodePattern - }; - - var titleTokens = TitleRegex.Matches(nameSpec.StandardEpisodeFormat); - - foreach (Match match in titleTokens) - { - var separator = match.Groups["separator"].Value; - var token = match.Groups["token"].Value; - - if (!separator.Equals(" ")) - { - basicNamingConfig.ReplaceSpaces = true; - } - - if (token.StartsWith("{Series", StringComparison.InvariantCultureIgnoreCase)) - { - basicNamingConfig.IncludeSeriesTitle = true; - } - - if (token.StartsWith("{Episode", StringComparison.InvariantCultureIgnoreCase)) - { - basicNamingConfig.IncludeEpisodeTitle = true; - } - - if (token.StartsWith("{Quality", StringComparison.InvariantCultureIgnoreCase)) - { - basicNamingConfig.IncludeQuality = true; - } - } - - return basicNamingConfig; - } - - public string GetSeriesFolder(Series series, NamingConfig namingConfig = null) - { - if (namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - AddSeriesTokens(tokenHandlers, series); - - return CleanFolderName(ReplaceTokens(namingConfig.SeriesFolderFormat, tokenHandlers, namingConfig)); - } - - public string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null) - { - if (namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - AddSeriesTokens(tokenHandlers, series); - AddSeasonTokens(tokenHandlers, seasonNumber); - - return CleanFolderName(ReplaceTokens(namingConfig.SeasonFolderFormat, tokenHandlers, namingConfig)); - } - - public string GetMovieFolder(Movie movie, NamingConfig namingConfig = null) - { - if(namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - AddMovieTokens(tokenHandlers, movie); - AddReleaseDateTokens(tokenHandlers, movie.Year); - AddImdbIdTokens(tokenHandlers, movie.ImdbId); - - return CleanFolderName(ReplaceTokens(namingConfig.MovieFolderFormat, tokenHandlers, namingConfig)); - } - - public static string CleanTitle(string title) - { - title = title.Replace("&", "and"); - title = ScenifyReplaceChars.Replace(title, " "); - title = ScenifyRemoveChars.Replace(title, string.Empty); - - return title; - } - - public static string TitleThe(string title) - { - string[] prefixes = { "The ", "An ", "A " }; - foreach (string prefix in prefixes) - { - int prefix_length = prefix.Length; - if (prefix.ToLower() == title.Substring(0, prefix_length).ToLower()) - { - title = title.Substring(prefix_length) + ", " + prefix.Trim(); - break; - } - } - - return title.Trim(); - } - using System; using System.Collections.Generic; using System.Globalization; @@ -472,7 +138,7 @@ namespace NzbDrone.Core.Organizer AddEpisodeFileTokens(tokenHandlers, episodeFile); AddQualityTokens(tokenHandlers, series, episodeFile); AddMediaInfoTokens(tokenHandlers, episodeFile); - + var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); @@ -564,10 +230,10 @@ namespace NzbDrone.Core.Organizer } var basicNamingConfig = new BasicNamingConfig - { - Separator = episodeFormat.Separator, - NumberStyle = episodeFormat.SeasonEpisodePattern - }; + { + Separator = episodeFormat.Separator, + NumberStyle = episodeFormat.SeasonEpisodePattern + }; var titleTokens = TitleRegex.Matches(nameSpec.StandardEpisodeFormat); @@ -631,7 +297,7 @@ namespace NzbDrone.Core.Organizer public string GetMovieFolder(Movie movie, NamingConfig namingConfig = null) { - if(namingConfig == null) + if (namingConfig == null) { namingConfig = _namingConfigService.GetConfig(); } @@ -654,6 +320,20 @@ namespace NzbDrone.Core.Organizer return title; } + public static string TitleThe(string title) + { + string[] prefixes = { "The ", "An ", "A " }; + foreach (string prefix in prefixes) + { + int prefix_length = prefix.Length; + if (prefix.ToLower() == title.Substring(0, prefix_length).ToLower()) + { + title = title.Substring(prefix_length) + ", " + prefix.Trim(); + break; + } + } + + return title.Trim(); } public static string CleanFileName(string name, bool replace = true) @@ -763,7 +443,7 @@ namespace NzbDrone.Core.Organizer var absoluteEpisodePattern = absoluteEpisodeFormat.AbsoluteEpisodePattern; string formatPattern; - switch ((MultiEpisodeStyle) namingConfig.MultiEpisodeStyle) + switch ((MultiEpisodeStyle)namingConfig.MultiEpisodeStyle) { case MultiEpisodeStyle.Duplicate: @@ -786,14 +466,14 @@ namespace NzbDrone.Core.Organizer case MultiEpisodeStyle.Range: case MultiEpisodeStyle.PrefixedRange: formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; - var eps = new List {episodes.First()}; + var eps = new List { episodes.First() }; if (episodes.Count > 1) eps.Add(episodes.Last()); absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, eps); break; - //MultiEpisodeStyle.Extend + //MultiEpisodeStyle.Extend default: formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); @@ -1241,7 +921,7 @@ namespace NzbDrone.Core.Organizer private AbsoluteEpisodeFormat[] GetAbsoluteFormat(string pattern) { - return _absoluteEpisodeFormatCache.Get(pattern, () => AbsoluteEpisodePatternRegex.Matches(pattern).OfType() + return _absoluteEpisodeFormatCache.Get(pattern, () => AbsoluteEpisodePatternRegex.Matches(pattern).OfType() .Select(match => new AbsoluteEpisodeFormat { Separator = match.Groups["separator"].Value.IsNotNullOrWhiteSpace() ? match.Groups["separator"].Value : "-", @@ -1396,4 +1076,4 @@ namespace NzbDrone.Core.Organizer Range = 4, PrefixedRange = 5 } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/UI/Settings/NetImport/Options/NetImportOptionsViewTemplate.hbs b/src/UI/Settings/NetImport/Options/NetImportOptionsViewTemplate.hbs index ab42a0351..6e6072609 100644 --- a/src/UI/Settings/NetImport/Options/NetImportOptionsViewTemplate.hbs +++ b/src/UI/Settings/NetImport/Options/NetImportOptionsViewTemplate.hbs @@ -10,7 +10,7 @@
- +
From d458c4ecc819b8b3e327eb8d637c8b34b70ba0ad Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Sat, 28 Jan 2017 15:30:46 -0500 Subject: [PATCH 79/83] update taskscheduler when config is saved with netimportsynccommand --- src/NzbDrone.Core/Jobs/TaskManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Jobs/TaskManager.cs b/src/NzbDrone.Core/Jobs/TaskManager.cs index 184dfedb9..c2739b506 100644 --- a/src/NzbDrone.Core/Jobs/TaskManager.cs +++ b/src/NzbDrone.Core/Jobs/TaskManager.cs @@ -181,7 +181,10 @@ namespace NzbDrone.Core.Jobs var downloadedEpisodes = _scheduledTaskRepository.GetDefinition(typeof(DownloadedEpisodesScanCommand)); downloadedEpisodes.Interval = _configService.DownloadedEpisodesScanInterval; - _scheduledTaskRepository.UpdateMany(new List { rss, downloadedEpisodes }); + var netImport = _scheduledTaskRepository.GetDefinition(typeof(NetImportSyncCommand)); + netImport.Interval = _configService.NetImportSyncInterval; + + _scheduledTaskRepository.UpdateMany(new List { rss, downloadedEpisodes, netImport }); } } } From b88281b458961e46c00f86e8cefc086d755b5291 Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Sat, 28 Jan 2017 16:46:54 -0500 Subject: [PATCH 80/83] Update HDBits to work with Radarr --- src/NzbDrone.Core/Indexers/HDBits/HDBits.cs | 2 +- .../Indexers/HDBits/HDBitsApi.cs | 2 +- .../Indexers/HDBits/HDBitsParser.cs | 2 +- .../Indexers/HDBits/HDBitsRequestGenerator.cs | 30 ++++++++++++++----- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBits.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBits.cs index 5185433a5..1574d53e0 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBits.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBits.cs @@ -27,4 +27,4 @@ namespace NzbDrone.Core.Indexers.HDBits return new HDBitsParser(Settings); } } -} +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsApi.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsApi.cs index 9bb6d624b..aba22a1f2 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsApi.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsApi.cs @@ -129,4 +129,4 @@ namespace NzbDrone.Core.Indexers.HDBits ImdbImportFail = 8, ImdbTvNotAllowed = 9 } -} +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs index c5a6dfa4a..0a183ff77 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsParser.cs @@ -87,4 +87,4 @@ namespace NzbDrone.Core.Indexers.HDBits } } -} +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs index 84fef6bb8..cd1ff7ccb 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Http; @@ -45,17 +44,34 @@ namespace NzbDrone.Core.Indexers.HDBits return new IndexerPageableRequestChain(); } - public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + public virtual IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) { - var pageableRequests = new IndexerPageableRequestChain(); var queryBase = new TorrentQuery(); - var query = queryBase.Clone(); - query.ImdbInfo.Id = int.Parse(searchCriteria.Movie.ImdbId.Substring(2)); - pageableRequests.Add(GetRequest(query)); + + if (TryAddSearchParameters(queryBase, searchCriteria)) + { + var query = queryBase.Clone(); + query.ImdbInfo.Id = int.Parse(searchCriteria.Movie.ImdbId.Substring(2)); + pageableRequests.Add(GetRequest(query)); + } + return pageableRequests; } + private bool TryAddSearchParameters(TorrentQuery query, SearchCriteriaBase searchCriteria) + { + var imdbId = int.Parse(searchCriteria.Movie.ImdbId.Substring(2)); + + if (imdbId != 0) + { + query.ImdbInfo = query.ImdbInfo ?? new ImdbInfo(); + query.ImdbInfo.Id = imdbId; + return true; + } + return false; + } + private IEnumerable GetRequest(TorrentQuery query) { var request = new HttpRequestBuilder(Settings.BaseUrl) @@ -75,4 +91,4 @@ namespace NzbDrone.Core.Indexers.HDBits yield return new IndexerRequest(request); } } -} +} \ No newline at end of file From 8a0820ad1e73b85eca5a07b4b3d0a1b2d4913d3b Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Sat, 28 Jan 2017 17:14:51 -0500 Subject: [PATCH 81/83] make year nullable, and rmember the profileid --- src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs | 2 +- src/UI/Settings/NetImport/Edit/NetImportEditView.js | 2 +- .../Settings/NetImport/Edit/NetImportEditViewTemplate.hbs | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs index 175ff0555..e09ce89be 100644 --- a/src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs @@ -15,7 +15,7 @@ namespace NzbDrone.Core.NetImport.Trakt public class Movie { public string title { get; set; } - public int year { get; set; } + public int? year { get; set; } public Ids ids { get; set; } } diff --git a/src/UI/Settings/NetImport/Edit/NetImportEditView.js b/src/UI/Settings/NetImport/Edit/NetImportEditView.js index 9c6c25101..ba08dd809 100644 --- a/src/UI/Settings/NetImport/Edit/NetImportEditView.js +++ b/src/UI/Settings/NetImport/Edit/NetImportEditView.js @@ -25,7 +25,7 @@ var view = Marionette.ItemView.extend({ events : { 'click .x-back' : '_back', 'click .x-captcha-refresh' : '_onRefreshCaptcha', - 'change .x-root-folder' : '_rootFolderChanged', + 'change .x-root-folder' : '_rootFolderChanged', }, _deleteView : DeleteView, diff --git a/src/UI/Settings/NetImport/Edit/NetImportEditViewTemplate.hbs b/src/UI/Settings/NetImport/Edit/NetImportEditViewTemplate.hbs index 7653fa0e0..1d18dea5c 100644 --- a/src/UI/Settings/NetImport/Edit/NetImportEditViewTemplate.hbs +++ b/src/UI/Settings/NetImport/Edit/NetImportEditViewTemplate.hbs @@ -66,7 +66,11 @@
- {{> ProfileSelectionPartial profiles}} +
From e8065d07b315eb0ff17a7ed6f7c681aa4df8a255 Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Sat, 28 Jan 2017 17:27:53 -0500 Subject: [PATCH 82/83] Make year nullable for trakt --- src/NzbDrone.Core/NetImport/Trakt/TraktParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktParser.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktParser.cs index 611dfc5b8..f6e80bad2 100644 --- a/src/NzbDrone.Core/NetImport/Trakt/TraktParser.cs +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktParser.cs @@ -57,7 +57,7 @@ namespace NzbDrone.Core.NetImport.Trakt Title = movie.movie.title, ImdbId = movie.movie.ids.imdb, TmdbId = movie.movie.ids.tmdb, - Year = movie.movie.year + Year = (movie.movie.year ?? 0) }); } From 5b7d5139863d20f5c31f4dfa58de28b509046b37 Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Sat, 28 Jan 2017 21:17:55 -0500 Subject: [PATCH 83/83] Added options for watched, and watchlist, and customlist to trakt --- .../CouchPotato/CouchPotatoSettings.cs | 10 ++++++++++ .../NetImport/Trakt/TraktImport.cs | 2 +- .../NetImport/Trakt/TraktListType.cs | 9 +++++++++ .../NetImport/Trakt/TraktRequestGenerator.cs | 19 +++++++++++++++---- .../NetImport/Trakt/TraktSettings.cs | 16 ++++++++++++++-- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + 6 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 src/NzbDrone.Core/NetImport/Trakt/TraktListType.cs diff --git a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs index 98ab00bff..5fb9c332f 100644 --- a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs +++ b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoSettings.cs @@ -7,6 +7,16 @@ using NzbDrone.Core.Validation; namespace NzbDrone.Core.NetImport.CouchPotato { + public class CouchPotatoSettingsValidator : AbstractValidator + { + public CouchPotatoSettingsValidator() + { + RuleFor(c => c.Link).ValidRootUrl(); + RuleFor(c => c.Port).InclusiveBetween(1, 65535); + RuleFor(c => c.ApiKey).NotEmpty(); + } + } + public class CouchPotatoSettings : NetImportBaseSettings { public CouchPotatoSettings() diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktImport.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktImport.cs index 1e0e083ae..2add8de16 100644 --- a/src/NzbDrone.Core/NetImport/Trakt/TraktImport.cs +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktImport.cs @@ -15,7 +15,7 @@ namespace NzbDrone.Core.NetImport.Trakt { public class TraktImport : HttpNetImportBase { - public override string Name => "Trakt User List"; + public override string Name => "Trakt List"; public override bool Enabled => true; public override bool EnableAuto => false; diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktListType.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktListType.cs new file mode 100644 index 000000000..bf818d06a --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktListType.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.NetImport.Trakt +{ + public enum TraktListType + { + WatchList = 0, + Watched = 1, + CustomList = 2 + } +} diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktRequestGenerator.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktRequestGenerator.cs index 152da8986..61166f8b3 100644 --- a/src/NzbDrone.Core/NetImport/Trakt/TraktRequestGenerator.cs +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktRequestGenerator.cs @@ -21,11 +21,22 @@ namespace NzbDrone.Core.NetImport.Trakt private IEnumerable GetMovies(string searchParameters) { - // https://api.trakt.tv/users/timdturner/lists/custom1/items/movies - // trakt-api-version = 2 - // trakt-api-key = 657bb899dcb81ec8ee838ff09f6e013ff7c740bf0ccfa54dd41e791b9a70b2f0 + var link = $"{Settings.Link.Trim()}{Settings.Username.Trim()}"; - var request = new NetImportRequest($"{Settings.Link.Trim()}{Settings.Username.Trim()}/lists/{Settings.Listname.Trim()}/items/movies", HttpAccept.Json); + switch (Settings.ListType) + { + case (int)TraktListType.CustomList: + link = link + $"/lists/{Settings.Listname.Trim()}/items/movies"; + break; + case (int)TraktListType.WatchList: + link = link + "/watchlist/movies"; + break; + case (int)TraktListType.Watched: + link = link + "/watched/movies"; + break; + } + + var request = new NetImportRequest($"{link}", HttpAccept.Json); request.HttpRequest.Headers.Add("trakt-api-version", "2"); request.HttpRequest.Headers.Add("trakt-api-key", "657bb899dcb81ec8ee838ff09f6e013ff7c740bf0ccfa54dd41e791b9a70b2f0"); diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktSettings.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktSettings.cs index 2279cf373..8af9bd25f 100644 --- a/src/NzbDrone.Core/NetImport/Trakt/TraktSettings.cs +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktSettings.cs @@ -7,6 +7,15 @@ using NzbDrone.Core.Validation; namespace NzbDrone.Core.NetImport.Trakt { + public class TraktSettingsValidator : AbstractValidator + { + public TraktSettingsValidator() + { + RuleFor(c => c.Link).ValidRootUrl(); + RuleFor(c => c.Username).NotEmpty(); + } + } + public class TraktSettings : NetImportBaseSettings { public TraktSettings() @@ -19,10 +28,13 @@ namespace NzbDrone.Core.NetImport.Trakt [FieldDefinition(0, Label = "Trakt API URL", HelpText = "Link to to Trakt API URL, do not change unless you know what you are doing.")] public new string Link { get; set; } - [FieldDefinition(1, Label = "Trakt Username", HelpText = "Trakt Username the list belongs to.")] + [FieldDefinition(1, Label = "Trakt List Type", Type = FieldType.Select, SelectOptions = typeof(TraktListType), HelpText = "Trakt list type, custom or watchlist")] + public int ListType { get; set; } + + [FieldDefinition(2, Label = "Trakt Username", HelpText = "Trakt Username the list belongs to.")] public string Username { get; set; } - [FieldDefinition(2, Label = "Trakt List Name", HelpText = "Trakt List Name")] + [FieldDefinition(3, Label = "Trakt List Name", HelpText = "Required for Custom List")] public string Listname { get; set; } } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 90fc824d6..ff9a8c81d 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -125,6 +125,7 @@ +