diff --git a/.gitignore b/.gitignore index aa234c2fe..a206d436a 100644 --- a/.gitignore +++ b/.gitignore @@ -153,4 +153,27 @@ Thumbs.db # Cake /tools/Addins/* -packages.config.md5sum \ No newline at end of file +packages.config.md5sum + + +# Common IntelliJ Platform excludes + +# User specific +**/.idea/**/workspace.xml +**/.idea/**/tasks.xml +**/.idea/shelf/* +**/.idea/dictionaries + +# Sensitive or high-churn files +**/.idea/**/dataSources/ +**/.idea/**/dataSources.ids +**/.idea/**/dataSources.xml +**/.idea/**/dataSources.local.xml +**/.idea/**/sqlDataSources.xml +**/.idea/**/dynamic.xml + +# Rider +# Rider auto-generates .iml files, and contentModel.xml +**/.idea/**/*.iml +**/.idea/**/contentModel.xml +**/.idea/**/modules.xml diff --git a/src/.idea/.idea.NzbDrone/.idea/contentModel.xml b/src/.idea/.idea.NzbDrone/.idea/contentModel.xml deleted file mode 100644 index 7bb9c32d7..000000000 --- a/src/.idea/.idea.NzbDrone/.idea/contentModel.xml +++ /dev/null @@ -1,3362 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/.idea/.idea.NzbDrone/.idea/indexLayout.xml b/src/.idea/.idea.NzbDrone/.idea/indexLayout.xml index f1feadf0e..27ba142e9 100644 --- a/src/.idea/.idea.NzbDrone/.idea/indexLayout.xml +++ b/src/.idea/.idea.NzbDrone/.idea/indexLayout.xml @@ -1,6 +1,7 @@ + diff --git a/src/.idea/.idea.NzbDrone/.idea/modules.xml b/src/.idea/.idea.NzbDrone/.idea/modules.xml deleted file mode 100644 index 364561fe7..000000000 --- a/src/.idea/.idea.NzbDrone/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/.idea/.idea.NzbDrone/riderModule.iml b/src/.idea/.idea.NzbDrone/riderModule.iml deleted file mode 100644 index c8b2ee068..000000000 --- a/src/.idea/.idea.NzbDrone/riderModule.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/src/NzbDrone.Api/MovieFiles/MovieFileModule.cs b/src/NzbDrone.Api/MovieFiles/MovieFileModule.cs index c65c449e2..049f61104 100644 --- a/src/NzbDrone.Api/MovieFiles/MovieFileModule.cs +++ b/src/NzbDrone.Api/MovieFiles/MovieFileModule.cs @@ -47,7 +47,7 @@ namespace NzbDrone.Api.MovieFiles private void SetQuality(MovieFileResource movieFileResource) - { + { var movieFile = _mediaFileService.GetMovie(movieFileResource.Id); movieFile.Quality = movieFileResource.Quality; _mediaFileService.Update(movieFile); diff --git a/src/NzbDrone.Api/MovieFiles/MovieFileResource.cs b/src/NzbDrone.Api/MovieFiles/MovieFileResource.cs index 6d8786fba..209323966 100644 --- a/src/NzbDrone.Api/MovieFiles/MovieFileResource.cs +++ b/src/NzbDrone.Api/MovieFiles/MovieFileResource.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Api.REST; using NzbDrone.Api.Movies; -using NzbDrone.Core.MediaCover; -using NzbDrone.Core.Movies; using NzbDrone.Core.Qualities; using NzbDrone.Core.MediaFiles; @@ -14,7 +12,7 @@ namespace NzbDrone.Api.MovieFiles { public MovieFileResource() { - + } //Todo: Sorters should be done completely on the client @@ -75,7 +73,7 @@ namespace NzbDrone.Api.MovieFiles return new MovieFile { - + }; } diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 15b4c8d21..dc06b9344 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -147,6 +147,7 @@ + diff --git a/src/NzbDrone.Api/Parse/ParseModule.cs b/src/NzbDrone.Api/Parse/ParseModule.cs index 064e1bbf1..20bdd8cfd 100644 --- a/src/NzbDrone.Api/Parse/ParseModule.cs +++ b/src/NzbDrone.Api/Parse/ParseModule.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using NzbDrone.Api.Movies; using NzbDrone.Core.Parser; @@ -17,7 +18,8 @@ namespace NzbDrone.Api.Parse private ParseResource Parse() { var title = Request.Query.Title.Value as string; - var parsedMovieInfo = Parser.ParseMovieTitle(title, false); + var parsedMovieInfo = _parsingService.ParseMovieInfo(title, new List()); + if (parsedMovieInfo == null) { diff --git a/src/NzbDrone.Api/Qualities/CustomFormatModule.cs b/src/NzbDrone.Api/Qualities/CustomFormatModule.cs index 33e7eaf7d..8a4cb7c24 100644 --- a/src/NzbDrone.Api/Qualities/CustomFormatModule.cs +++ b/src/NzbDrone.Api/Qualities/CustomFormatModule.cs @@ -3,6 +3,7 @@ using System.Linq; using FluentValidation; using Nancy; using NzbDrone.Api.Extensions; +using NzbDrone.Api.Validation; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Parser; @@ -21,7 +22,7 @@ namespace NzbDrone.Api.Qualities SharedValidator.RuleFor(c => c.Name).NotEmpty(); SharedValidator.RuleFor(c => c.Name) .Must((v, c) => !_formatService.All().Any(f => f.Name == c && f.Id != v.Id)).WithMessage("Must be unique."); - SharedValidator.RuleFor(c => c.FormatTags).Must((v, c) => c.All(s => FormatTag.QualityTagRegex.IsMatch(s))).WithMessage("Invalid format."); + SharedValidator.RuleFor(c => c.FormatTags).AreValidFormatTags(); SharedValidator.RuleFor(c => c.FormatTags).Must((v, c) => { var allFormats = _formatService.All(); @@ -44,6 +45,8 @@ namespace NzbDrone.Api.Qualities CreateResource = Create; + DeleteResource = Delete; + Get["/test"] = x => Test(); Post["/test"] = x => TestWithNewModel(); @@ -73,6 +76,11 @@ namespace NzbDrone.Api.Qualities return _formatService.All().ToResource(); } + private void Delete(int id) + { + _formatService.Delete(id); + } + private Response GetTemplates() { return CustomFormatService.Templates.SelectMany(t => @@ -107,8 +115,9 @@ namespace NzbDrone.Api.Qualities var resource = ReadResourceFromRequest(); var model = resource.ToModel(); + model.Name = model.Name += " (New)"; - var parsed = _parsingService.ParseMovieInfo((string) Request.Query.title, new List{model}); + var parsed = _parsingService.ParseMovieInfo(queryTitle, new List{model}); if (parsed == null) { return null; diff --git a/src/NzbDrone.Api/Qualities/FormatTagValidator.cs b/src/NzbDrone.Api/Qualities/FormatTagValidator.cs new file mode 100644 index 000000000..53efbea21 --- /dev/null +++ b/src/NzbDrone.Api/Qualities/FormatTagValidator.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation.Validators; +using NzbDrone.Core.CustomFormats; + +namespace NzbDrone.Api.Qualities +{ + public class FormatTagValidator : PropertyValidator + { + public FormatTagValidator() : base("{ValidationMessage}") + { + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) + { + context.SetMessage("Format Tags cannot be null!"); + return false; + } + + var tags = (IEnumerable) context.PropertyValue; + + var invalidTags = tags.Where(t => !FormatTag.QualityTagRegex.IsMatch(t)); + + if (invalidTags.Count() == 0) return true; + + var formatMessage = + $"Format Tags ({string.Join(", ", invalidTags)}) are in an invalid format! Check the Wiki to learn how they should look."; + context.SetMessage(formatMessage); + return false; + } + } + + public static class PropertyValidatorExtensions + { + public static void SetMessage(this PropertyValidatorContext context, string message, string argument = "ValidationMessage") + { + context.MessageFormatter.AppendArgument(argument, message); + } + } +} diff --git a/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs b/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs index 4684d3f12..c60be7fd0 100644 --- a/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs +++ b/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs @@ -2,6 +2,7 @@ using System.Text.RegularExpressions; using FluentValidation; using FluentValidation.Validators; +using NzbDrone.Api.Qualities; namespace NzbDrone.Api.Validation { @@ -41,5 +42,11 @@ namespace NzbDrone.Api.Validation { return ruleBuilder.SetValidator(new NetImportSyncIntervalValidator()); } + + public static IRuleBuilderOptions> AreValidFormatTags( + this IRuleBuilder> ruleBuilder) + { + return ruleBuilder.SetValidator(new FormatTagValidator()); + } } } diff --git a/src/NzbDrone.Core.Test/CustomFormat/QualityTagFixture.cs b/src/NzbDrone.Core.Test/CustomFormat/QualityTagFixture.cs index 6582ece91..7c35aeb87 100644 --- a/src/NzbDrone.Core.Test/CustomFormat/QualityTagFixture.cs +++ b/src/NzbDrone.Core.Test/CustomFormat/QualityTagFixture.cs @@ -1,4 +1,5 @@ -using System.Text.RegularExpressions; +using System; +using System.Text.RegularExpressions; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.CustomFormats; @@ -24,13 +25,16 @@ namespace NzbDrone.Core.Test.CustomFormat [TestCase("L_English", TagType.Language, Language.English)] [TestCase("L_germaN", TagType.Language, Language.German)] [TestCase("E_Director", TagType.Edition, "director")] - [TestCase("E_R_Director('?s)?", TagType.Edition, "director('?s)?", TagModifier.Regex)] - [TestCase("E_RN_Director('?s)?", TagType.Edition, "director('?s)?", TagModifier.Regex, TagModifier.Not)] - [TestCase("E_RNRE_Director('?s)?", TagType.Edition, "director('?s)?", TagModifier.Regex, TagModifier.Not, TagModifier.AbsolutelyRequired)] + [TestCase("E_RX_Director('?s)?", TagType.Edition, "director('?s)?", TagModifier.Regex)] + [TestCase("E_RXN_Director('?s)?", TagType.Edition, "director('?s)?", TagModifier.Regex, TagModifier.Not)] + [TestCase("E_RXNRQ_Director('?s)?", TagType.Edition, "director('?s)?", TagModifier.Regex, TagModifier.Not, TagModifier.AbsolutelyRequired)] [TestCase("C_Surround", TagType.Custom, "surround")] - [TestCase("C_RE_Surround", TagType.Custom, "surround", TagModifier.AbsolutelyRequired)] - [TestCase("C_REN_Surround", TagType.Custom, "surround", TagModifier.AbsolutelyRequired, TagModifier.Not)] - [TestCase("C_RENR_Surround|(5|7)(\\.1)?", TagType.Custom, "surround|(5|7)(\\.1)?", TagModifier.AbsolutelyRequired, TagModifier.Not, TagModifier.Regex)] + [TestCase("C_RQ_Surround", TagType.Custom, "surround", TagModifier.AbsolutelyRequired)] + [TestCase("C_RQN_Surround", TagType.Custom, "surround", TagModifier.AbsolutelyRequired, TagModifier.Not)] + [TestCase("C_RQNRX_Surround|(5|7)(\\.1)?", TagType.Custom, "surround|(5|7)(\\.1)?", TagModifier.AbsolutelyRequired, TagModifier.Not, TagModifier.Regex)] + [TestCase("G_10<>20", TagType.Size, new[] { 10737418240L, 21474836480L})] + [TestCase("G_15.55<>20", TagType.Size, new[] { 16696685363L, 21474836480L})] + [TestCase("G_15.55<>25.1908754", TagType.Size, new[] { 16696685363L, 27048496500L})] public void should_parse_tag_from_string(string raw, TagType type, object value, params TagModifier[] modifiers) { var parsed = new FormatTag(raw); @@ -40,6 +44,10 @@ namespace NzbDrone.Core.Test.CustomFormat modifier |= m; } parsed.TagType.Should().Be(type); + if (value is long[]) + { + value = (((long[]) value)[0], ((long[]) value)[1]); + } if ((parsed.Value as Regex) != null) { (parsed.Value as Regex).ToString().Should().Be((value as string)); diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/149_regex_required_tagsFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/149_regex_required_tagsFixture.cs new file mode 100644 index 000000000..998f3dcb9 --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Migration/149_regex_required_tagsFixture.cs @@ -0,0 +1,100 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using FluentAssertions; +using Newtonsoft.Json; +using NUnit.Framework; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Migration +{ + [TestFixture] + public class regex_required_tagsFixture : MigrationTest + { + + public void AddCustomFormat(convert_regex_required_tags c, string name, params string[] formatTags) + { + var customFormat = new {Name = name, FormatTags = formatTags.ToList().ToJson()}; + + c.Insert.IntoTable("CustomFormats").Row(customFormat); + } + + [TestCase("C_RE_HDR", "C_RQ_HDR")] + [TestCase("C_R_HDR", "C_RX_HDR")] + [TestCase("C_RER_HDR", "C_RXRQ_HDR")] + [TestCase("C_RENR_HDR", "C_NRXRQ_HDR")] + [TestCase("C_NRER_HDR", "C_NRXRQ_HDR")] + [TestCase("C_RE_RERN", "C_RQ_RERN")] + [TestCase("E_NRER_Director", "E_NRXRQ_Director")] + [TestCase("G_N_1000<>1000", "G_N_1000<>1000")] + public void should_correctly_convert_format_tag(string original, string converted) + { + var db = WithMigrationTestDb(c => { AddCustomFormat(c, "TestFormat", original); }); + + var items = QueryItems(db); + + var convertedTags = items.First().DeserializedTags; + + convertedTags.Should().HaveCount(1); + convertedTags.First().ShouldBeEquivalentTo(converted); + } + + [Test] + public void should_correctly_convert_multiple() + { + var db = WithMigrationTestDb(c => { AddCustomFormat(c, "TestFormat", "C_RE_HDR", "C_R_HDR", "E_NRER_Director"); }); + + var items = QueryItems(db); + + var convertedTags = items.First().DeserializedTags; + + convertedTags.Should().HaveCount(3); + convertedTags.Should().BeEquivalentTo( "C_RQ_HDR", "C_RX_HDR", "E_NRXRQ_Director"); + } + + [Test] + public void should_correctly_convert_multiple_formats() + { + var db = WithMigrationTestDb(c => + { + AddCustomFormat(c, "TestFormat", "C_RE_HDR", "C_R_HDR", "E_NRER_Director"); + AddCustomFormat(c, "TestFormat2", "E_NRER_Director"); + }); + + var items = QueryItems(db); + + var convertedTags = items.First().DeserializedTags; + + convertedTags.Should().HaveCount(3); + convertedTags.Should().BeEquivalentTo( "C_RQ_HDR", "C_RX_HDR", "E_NRXRQ_Director"); + + var convertedTags2 = items.Last().DeserializedTags; + + convertedTags2.Should().HaveCount(1); + convertedTags2.Should().BeEquivalentTo("E_NRXRQ_Director"); + } + + private List QueryItems(IDirectDataMapper db) + { + var items = db.Query("SELECT Name, FormatTags FROM CustomFormats"); + + return items.Select(i => + { + i.DeserializedTags = JsonConvert.DeserializeObject>(i.FormatTags); + return i; + }).ToList(); + } + + public class CustomFormatTest149 + { + public string Name { get; set; } + public string FormatTags { get; set; } + public List DeserializedTags { get; set; } + } + } +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/CustomFormatAllowedByProfileSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/CustomFormatAllowedByProfileSpecificationFixture.cs new file mode 100644 index 000000000..72a8aa582 --- /dev/null +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/CustomFormatAllowedByProfileSpecificationFixture.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using FizzWare.NBuilder; +using FluentAssertions; +using Marr.Data; +using NUnit.Framework; +using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Movies; +using NzbDrone.Core.Test.CustomFormat; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.DecisionEngineTests +{ + [TestFixture] + + public class CustomFormatAllowedByProfileSpecificationFixture : CoreTest + { + private RemoteMovie remoteMovie; + + private CustomFormats.CustomFormat _format1; + private CustomFormats.CustomFormat _format2; + + [SetUp] + public void Setup() + { + _format1 = new CustomFormats.CustomFormat("Awesome Format"); + _format1.Id = 1; + + _format2 = new CustomFormats.CustomFormat("Cool Format"); + _format2.Id = 2; + + + var fakeSeries = Builder.CreateNew() + .With(c => c.Profile = (LazyLoaded)new Profile { Cutoff = Quality.Bluray1080p }) + .Build(); + + remoteMovie = new RemoteMovie + { + Movie = fakeSeries, + ParsedMovieInfo = new ParsedMovieInfo { Quality = new QualityModel(Quality.DVD, new Revision(version: 2)) }, + }; + + CustomFormatsFixture.GivenCustomFormats(CustomFormats.CustomFormat.None, _format1, _format2); + } + + [Test] + public void should_allow_if_format_is_defined_in_profile() + { + remoteMovie.ParsedMovieInfo.Quality.CustomFormats = new List {_format1}; + remoteMovie.Movie.Profile.Value.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name); + + Subject.IsSatisfiedBy(remoteMovie, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_deny_if_format_is_defined_in_profile() + { + remoteMovie.ParsedMovieInfo.Quality.CustomFormats = new List {_format2}; + remoteMovie.Movie.Profile.Value.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name); + + Subject.IsSatisfiedBy(remoteMovie, null).Accepted.Should().BeFalse(); + } + + [Test] + public void should_deny_if_one_format_is_defined_in_profile() + { + remoteMovie.ParsedMovieInfo.Quality.CustomFormats = new List {_format2, _format1}; + remoteMovie.Movie.Profile.Value.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name); + + Subject.IsSatisfiedBy(remoteMovie, null).Accepted.Should().BeFalse(); + } + + [Test] + public void should_allow_if_all_format_is_defined_in_profile() + { + remoteMovie.ParsedMovieInfo.Quality.CustomFormats = new List {_format2, _format1}; + remoteMovie.Movie.Profile.Value.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name, _format2.Name); + + Subject.IsSatisfiedBy(remoteMovie, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_deny_if_no_format_was_parsed_and_none_not_in_profile() + { + remoteMovie.ParsedMovieInfo.Quality.CustomFormats = new List {}; + remoteMovie.Movie.Profile.Value.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name, _format2.Name); + + Subject.IsSatisfiedBy(remoteMovie, null).Accepted.Should().BeFalse(); + } + + [Test] + public void should_allow_if_no_format_was_parsed_and_none_in_profile() + { + remoteMovie.ParsedMovieInfo.Quality.CustomFormats = new List {}; + remoteMovie.Movie.Profile.Value.FormatItems = CustomFormatsFixture.GetSampleFormatItems(CustomFormats.CustomFormat.None.Name, _format1.Name, _format2.Name); + + Subject.IsSatisfiedBy(remoteMovie, null).Accepted.Should().BeTrue(); + } + } +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/LanguageSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/LanguageSpecificationFixture.cs index 5a3d69897..9309fa955 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/LanguageSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/LanguageSpecificationFixture.cs @@ -15,12 +15,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public class LanguageSpecificationFixture : CoreTest { - private RemoteMovie _remoteEpisode; + private RemoteMovie _remoteMovie; [SetUp] public void Setup() { - _remoteEpisode = new RemoteMovie + _remoteMovie = new RemoteMovie { ParsedMovieInfo = new ParsedMovieInfo { @@ -38,12 +38,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private void WithEnglishRelease() { - _remoteEpisode.ParsedMovieInfo.Languages = new List {Language.English}; + _remoteMovie.ParsedMovieInfo.Languages = new List {Language.English}; } private void WithGermanRelease() { - _remoteEpisode.ParsedMovieInfo.Languages = new List {Language.German}; + _remoteMovie.ParsedMovieInfo.Languages = new List {Language.German}; } [Test] @@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { WithEnglishRelease(); - Mocker.Resolve().IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); + Mocker.Resolve().IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue(); } [Test] @@ -59,7 +59,24 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { WithGermanRelease(); - Mocker.Resolve().IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse(); + Mocker.Resolve().IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse(); + } + + [Test] + public void should_return_true_if_allowed_language_any() + { + _remoteMovie.Movie.Profile = new LazyLoaded(new Profile + { + Language = Language.Any + }); + + WithGermanRelease(); + + Mocker.Resolve().IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue(); + + WithEnglishRelease(); + + Mocker.Resolve().IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue(); } } } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/Search/TorrentSeedingSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/Search/TorrentSeedingSpecificationFixture.cs index c6531b372..24172a9c7 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/Search/TorrentSeedingSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/Search/TorrentSeedingSpecificationFixture.cs @@ -3,10 +3,9 @@ using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Core.Datastore; -using NzbDrone.Core.DecisionEngine.Specifications.Search; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers.TorrentRss; -using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Movies; using NzbDrone.Test.Common; diff --git a/src/NzbDrone.Core.Test/MediaFiles/MovieImport/ImportDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MovieImport/ImportDecisionMakerFixture.cs index 380ed8274..b94ed7ea4 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MovieImport/ImportDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MovieImport/ImportDecisionMakerFixture.cs @@ -18,14 +18,15 @@ using NzbDrone.Core.Download; namespace NzbDrone.Core.Test.MediaFiles.MovieImport { - /* [TestFixture] - //TODO: Update all of this for movies. + [TestFixture] + //TODO: Add tests to ensure helpers for augmenters are correctly passed. public class ImportDecisionMakerFixture : CoreTest { private List _videoFiles; - private LocalMovie _localEpisode; - private Movie _series; + private LocalMovie _localMovie; + private Movie _movie; private QualityModel _quality; + private ParsedMovieInfo _fileInfo; private Mock _pass1; private Mock _pass2; @@ -54,24 +55,35 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport _fail2.Setup(c => c.IsSatisfiedBy(It.IsAny(), It.IsAny())).Returns(Decision.Reject("_fail2")); _fail3.Setup(c => c.IsSatisfiedBy(It.IsAny(), It.IsAny())).Returns(Decision.Reject("_fail3")); - _series = Builder.CreateNew() + _movie = Builder.CreateNew() .With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); _quality = new QualityModel(Quality.DVD); - _localEpisode = new LocalMovie - { - Movie = _series, + _localMovie = new LocalMovie + { + Movie = _movie, Quality = _quality, - Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi" + Path = @"C:\Test\Unsorted\The.Office.2018.DVDRip.XviD-OSiTV.avi" }; + _fileInfo = new ParsedMovieInfo + { + MovieTitle = "The Office", + Year = 2018, + Quality = _quality + }; + + Mocker.GetMock() + .Setup(c => c.GetLocalMovie(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) + .Returns(_localMovie); + Mocker.GetMock() - .Setup(c => c.GetLocalMovie(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(_localEpisode); + .Setup(c => c.ParseMinimalPathMovieInfo(It.IsAny())) + .Returns(_fileInfo); - GivenVideoFiles(new List { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi".AsOsAgnostic() }); + GivenVideoFiles(new List { @"C:\Test\Unsorted\The.Office.2018.DVDRip.XviD-OSiTV.avi".AsOsAgnostic() }); } private void GivenSpecifications(params Mock[] mocks) @@ -96,12 +108,12 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport Subject.GetImportDecisions(_videoFiles, new Movie(), downloadClientItem, null, false); - _fail1.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once()); - _fail2.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once()); - _fail3.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once()); - _pass1.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once()); - _pass2.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once()); - _pass3.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once()); + _fail1.Verify(c => c.IsSatisfiedBy(_localMovie, downloadClientItem), Times.Once()); + _fail2.Verify(c => c.IsSatisfiedBy(_localMovie, downloadClientItem), Times.Once()); + _fail3.Verify(c => c.IsSatisfiedBy(_localMovie, downloadClientItem), Times.Once()); + _pass1.Verify(c => c.IsSatisfiedBy(_localMovie, downloadClientItem), Times.Once()); + _pass2.Verify(c => c.IsSatisfiedBy(_localMovie, downloadClientItem), Times.Once()); + _pass3.Verify(c => c.IsSatisfiedBy(_localMovie, downloadClientItem), Times.Once()); } [Test] @@ -149,7 +161,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport GivenSpecifications(_pass1); Mocker.GetMock() - .Setup(c => c.GetLocalMovie(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(c => c.GetLocalMovie(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) .Throws(); _videoFiles = new List @@ -161,34 +173,53 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport GivenVideoFiles(_videoFiles); - Subject.GetImportDecisions(_videoFiles, _series); + Subject.GetImportDecisions(_videoFiles, _movie); Mocker.GetMock() - .Verify(c => c.GetLocalMovie(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(_videoFiles.Count)); + .Verify(c => c.GetLocalMovie(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny()), Times.Exactly(_videoFiles.Count)); ExceptionVerification.ExpectedErrors(3); } [Test] - public void should_use_file_quality_if_folder_quality_is_null() + public void should_call_parsing_service_with_filename_as_simpletitle() { GivenSpecifications(_pass1, _pass2, _pass3); - var expectedQuality = QualityParser.ParseQuality(_videoFiles.Single()); - var result = Subject.GetImportDecisions(_videoFiles, _series); + Mocker.GetMock() + .Setup(c => c.ParseMinimalPathMovieInfo(It.IsAny())) + .Returns(null); - result.Single().LocalMovie.Quality.Should().Be(expectedQuality); + var folderInfo = new ParsedMovieInfo {SimpleReleaseTitle = "A Movie Folder 2018", Quality = _quality}; + + var result = Subject.GetImportDecisions(_videoFiles, _movie, null, folderInfo, true); + + var fileNames = _videoFiles.Select(System.IO.Path.GetFileName); + + Mocker.GetMock() + .Verify( + c => c.GetLocalMovie(It.IsAny(), + It.Is(p => fileNames.Contains(p.SimpleReleaseTitle)), It.IsAny(), + It.IsAny>(), It.IsAny()), Times.Exactly(_videoFiles.Count)); + } + + [Test] + public void should_use_file_quality_if_folder_quality_is_null() + { + GivenSpecifications(_pass1, _pass2, _pass3); + var result = Subject.GetImportDecisions(_videoFiles, _movie); + + result.Single().LocalMovie.Quality.Should().Be(_fileInfo.Quality); } [Test] public void should_use_file_quality_if_file_quality_was_determined_by_name() { GivenSpecifications(_pass1, _pass2, _pass3); - var expectedQuality = QualityParser.ParseQuality(_videoFiles.Single()); - var result = Subject.GetImportDecisions(_videoFiles, _series, null, new ParsedMovieInfo{Quality = new QualityModel(Quality.SDTV)}, true); + var result = Subject.GetImportDecisions(_videoFiles, _movie, null, new ParsedMovieInfo{Quality = new QualityModel(Quality.SDTV)}, true); - result.Single().LocalMovie.Quality.Should().Be(expectedQuality); + result.Single().LocalMovie.Quality.Should().Be(_fileInfo.Quality); } [Test] @@ -197,13 +228,13 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport GivenSpecifications(_pass1, _pass2, _pass3); GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.mkv".AsOsAgnostic() }); - _localEpisode.Path = _videoFiles.Single(); - _localEpisode.Quality.QualitySource = QualitySource.Extension; - _localEpisode.Quality.Quality = Quality.HDTV720p; + _localMovie.Path = _videoFiles.Single(); + _localMovie.Quality.QualitySource = QualitySource.Extension; + _localMovie.Quality.Quality = Quality.HDTV720p; var expectedQuality = new QualityModel(Quality.SDTV); - var result = Subject.GetImportDecisions(_videoFiles, _series, null, new ParsedMovieInfo { Quality = expectedQuality }, true); + var result = Subject.GetImportDecisions(_videoFiles, _movie, null, new ParsedMovieInfo { Quality = expectedQuality }, true); result.Single().LocalMovie.Quality.Should().Be(expectedQuality); } @@ -214,168 +245,22 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport GivenSpecifications(_pass1, _pass2, _pass3); GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.mkv".AsOsAgnostic() }); - _localEpisode.Path = _videoFiles.Single(); - _localEpisode.Quality.Quality = Quality.HDTV720p; + _localMovie.Path = _videoFiles.Single(); + _localMovie.Quality.Quality = Quality.HDTV720p; var expectedQuality = new QualityModel(Quality.Bluray720p); - var result = Subject.GetImportDecisions(_videoFiles, _series, null, new ParsedMovieInfo { Quality = expectedQuality }, true); + var result = Subject.GetImportDecisions(_videoFiles, _movie, null, new ParsedMovieInfo { Quality = expectedQuality }, true); result.Single().LocalMovie.Quality.Should().Be(expectedQuality); } - [Test] - public void should_not_throw_if_episodes_are_not_found() - { - GivenSpecifications(_pass1); - - Mocker.GetMock() - .Setup(c => c.GetLocalMovie(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(new LocalMovie() { Path = "test" }); - - _videoFiles = new List - { - "The.Office.S03E115.DVDRip.XviD-OSiTV", - "The.Office.S03E115.DVDRip.XviD-OSiTV", - "The.Office.S03E115.DVDRip.XviD-OSiTV" - }; - - GivenVideoFiles(_videoFiles); - - var decisions = Subject.GetImportDecisions(_videoFiles, _series); - - Mocker.GetMock() - .Verify(c => c.GetLocalMovie(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(_videoFiles.Count)); - - decisions.Should().HaveCount(3); - decisions.First().Rejections.Should().NotBeEmpty(); - } - - [Test] - public void should_not_use_folder_for_full_season() - { - var videoFiles = new[] - { - @"C:\Test\Unsorted\Movie.Title.S01\S01E01.mkv".AsOsAgnostic(), - @"C:\Test\Unsorted\Movie.Title.S01\S01E02.mkv".AsOsAgnostic(), - @"C:\Test\Unsorted\Movie.Title.S01\S01E03.mkv".AsOsAgnostic() - }; - - GivenSpecifications(_pass1); - GivenVideoFiles(videoFiles); - - var folderInfo = Parser.Parser.ParseMovieTitle("Movie.Title.S01", false); - - Subject.GetImportDecisions(_videoFiles, _series, null, folderInfo, true); - - Mocker.GetMock() - .Verify(c => c.GetLocalMovie(It.IsAny(), It.IsAny(), null, true), Times.Exactly(3)); - - Mocker.GetMock() - .Verify(c => c.GetLocalMovie(It.IsAny(), It.IsAny(), It.Is(p => p != null), true), Times.Never()); - } - - [Test] - public void should_not_use_folder_when_it_contains_more_than_one_valid_video_file() - { - var videoFiles = new[] - { - @"C:\Test\Unsorted\Movie.Title.S01E01\S01E01.mkv".AsOsAgnostic(), - @"C:\Test\Unsorted\Movie.Title.S01E01\1x01.mkv".AsOsAgnostic() - }; - - GivenSpecifications(_pass1); - GivenVideoFiles(videoFiles); - - var folderInfo = Parser.Parser.ParseMovieTitle("Movie.Title.S01E01", false); - - Subject.GetImportDecisions(_videoFiles, _series, null, folderInfo, true); - - Mocker.GetMock() - .Verify(c => c.GetLocalMovie(It.IsAny(), It.IsAny(), null, true), Times.Exactly(2)); - - Mocker.GetMock() - .Verify(c => c.GetLocalMovie(It.IsAny(), It.IsAny(), It.Is(p => p != null), true), Times.Never()); - } - - [Test] - public void should_use_folder_when_only_one_video_file() - { - var videoFiles = new[] - { - @"C:\Test\Unsorted\Movie.Title.S01E01\S01E01.mkv".AsOsAgnostic() - }; - - GivenSpecifications(_pass1); - GivenVideoFiles(videoFiles); - - var folderInfo = Parser.Parser.ParseMovieTitle("Movie.Title.S01E01", false); - - Subject.GetImportDecisions(_videoFiles, _series, null, folderInfo, true); - - Mocker.GetMock() - .Verify(c => c.GetLocalMovie(It.IsAny(), It.IsAny(), It.IsAny(), true), Times.Exactly(1)); - - Mocker.GetMock() - .Verify(c => c.GetLocalMovie(It.IsAny(), It.IsAny(), null, true), Times.Never()); - } - - [Test] - public void should_use_folder_when_only_one_video_file_and_a_sample() - { - var videoFiles = new[] - { - @"C:\Test\Unsorted\Movie.Title.S01E01\S01E01.mkv".AsOsAgnostic(), - @"C:\Test\Unsorted\Movie.Title.S01E01\S01E01.sample.mkv".AsOsAgnostic() - }; - - GivenSpecifications(_pass1); - GivenVideoFiles(videoFiles.ToList()); - - Mocker.GetMock() - .Setup(s => s.IsSample(_series, It.IsAny(), It.Is(c => c.Contains("sample")), It.IsAny(), It.IsAny())) - .Returns(true); - - var folderInfo = Parser.Parser.ParseMovieTitle("Movie.Title.S01E01", false); - - Subject.GetImportDecisions(_videoFiles, _series, null, folderInfo, true); - - Mocker.GetMock() - .Verify(c => c.GetLocalMovie(It.IsAny(), It.IsAny(), It.IsAny(), true), Times.Exactly(2)); - - Mocker.GetMock() - .Verify(c => c.GetLocalMovie(It.IsAny(), It.IsAny(), null, true), Times.Never()); - } - - [Test] - [Ignore("Movie")] - public void should_not_use_folder_name_if_file_name_is_scene_name() - { - var videoFiles = new[] - { - @"C:\Test\Unsorted\Movie.Title.S01E01.720p.HDTV-LOL\Movie.Title.S01E01.720p.HDTV-LOL.mkv".AsOsAgnostic() - }; - - GivenSpecifications(_pass1); - GivenVideoFiles(videoFiles); - - var folderInfo = Parser.Parser.ParseMovieTitle("Movie.Title.S01E01.720p.HDTV-LOL", false); - - Subject.GetImportDecisions(_videoFiles, _series, null, folderInfo, true); - - Mocker.GetMock() - .Verify(c => c.GetLocalMovie(It.IsAny(), It.IsAny(), null, true), Times.Exactly(1)); - - Mocker.GetMock() - .Verify(c => c.GetLocalMovie(It.IsAny(), It.IsAny(), It.Is(p => p != null), true), Times.Never()); - } - [Test] public void should_not_use_folder_quality_when_it_is_unknown() { GivenSpecifications(_pass1, _pass2, _pass3); - _series.Profile = new Profile + _movie.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities(Quality.DVD, Quality.Unknown) }; @@ -383,7 +268,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport var folderQuality = new QualityModel(Quality.Unknown); - var result = Subject.GetImportDecisions(_videoFiles, _series, null, new ParsedMovieInfo { Quality = folderQuality}, true); + var result = Subject.GetImportDecisions(_videoFiles, _movie, null, new ParsedMovieInfo { Quality = folderQuality}, true); result.Single().LocalMovie.Quality.Should().Be(_quality); } @@ -392,7 +277,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport public void should_return_a_decision_when_exception_is_caught() { Mocker.GetMock() - .Setup(c => c.GetLocalMovie(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(c => c.GetLocalMovie(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) .Throws(); _videoFiles = new List @@ -402,9 +287,9 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport GivenVideoFiles(_videoFiles); - Subject.GetImportDecisions(_videoFiles, _series).Should().HaveCount(1); + Subject.GetImportDecisions(_videoFiles, _movie).Should().HaveCount(1); ExceptionVerification.ExpectedErrors(1); } - }*/ + } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/UpdateMovieFileQualityServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/UpdateMovieFileQualityServiceFixture.cs new file mode 100644 index 000000000..88cb9a64a --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/UpdateMovieFileQualityServiceFixture.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.History; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Commands; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.MediaFiles +{ + [TestFixture] + public class UpdateMovieFileQualityServiceFixture : CoreTest + { + private MovieFile _movieFile; + private QualityModel _oldQuality; + private QualityModel _newQuality; + + private ParsedMovieInfo _newInfo; + + [SetUp] + public void Setup() + { + _movieFile = Builder.CreateNew().With(m => m.MovieId = 0).Build(); + + _oldQuality = new QualityModel(Quality.Bluray720p); + + _movieFile.Quality = _oldQuality; + + _newQuality = _oldQuality.JsonClone(); + var format = new CustomFormats.CustomFormat("Awesome Format"); + format.Id = 1; + _newQuality.CustomFormats = new List{format}; + + _newInfo = new ParsedMovieInfo + { + Quality = _newQuality + }; + + Mocker.GetMock().Setup(s => s.GetMovies(It.IsAny>())) + .Returns(new List{_movieFile}); + + Mocker.GetMock().Setup(s => s.FindByMovieId(It.IsAny())) + .Returns(new List()); + } + + private void ExecuteCommand() + { + Subject.Execute(new UpdateMovieFileQualityCommand(new List{0})); + } + + [Test] + public void should_not_update_if_unable_to_parse() + { + ExecuteCommand(); + + Mocker.GetMock().Verify(s => s.Update(It.IsAny()), Times.Never()); + } + + [Test] + public void should_update_with_new_formats() + { + Mocker.GetMock().Setup(s => s.ParseMovieInfo(It.IsAny(), It.IsAny>())) + .Returns(_newInfo); + + ExecuteCommand(); + + Mocker.GetMock().Verify(s => s.Update(It.Is(f => f.Quality.CustomFormats == _newQuality.CustomFormats)), Times.Once()); + } + + [Test] + public void should_use_imported_history_title() + { + var imported = Builder.CreateNew() + .With(h => h.EventType = HistoryEventType.DownloadFolderImported) + .With(h => h.SourceTitle = "My Movie 2018.mkv").Build(); + Mocker.GetMock().Setup(s => s.FindByMovieId(It.IsAny())) + .Returns(new List {imported}); + + ExecuteCommand(); + + Mocker.GetMock().Verify(s => s.ParseMovieInfo("My Movie 2018.mkv", It.IsAny>())); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 3a0a9d73e..b295ba044 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -95,6 +95,9 @@ + + ..\packages\System.ValueTuple.4.5.0\lib\portable-net40+sl4+win8+wp8\System.ValueTuple.dll + @@ -136,12 +139,14 @@ + + @@ -283,6 +288,7 @@ + @@ -297,6 +303,7 @@ + diff --git a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/AugmentersTests/AugmentWithParsedMovieInfo.cs b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/AugmentersTests/AugmentWithParsedMovieInfo.cs new file mode 100644 index 000000000..a9fb92c12 --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/AugmentersTests/AugmentWithParsedMovieInfo.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Augmenters; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests.AugmentersTests +{ + [TestFixture] + public class AugmentWithParsedMovieInfoFixture : AugmentMovieInfoFixture + { + [Test] + public void should_add_edition_if_null() + { + var folderInfo = new ParsedMovieInfo + { + Edition = "Directors Cut" + }; + + var result = Subject.AugmentMovieInfo(MovieInfo, folderInfo); + + result.Edition.Should().Be(folderInfo.Edition); + } + + [Test] + public void should_preferr_longer_edition() + { + var folderInfo = new ParsedMovieInfo + { + Edition = "Super duper cut" + }; + + MovieInfo.Edition = "Rogue"; + + var result = Subject.AugmentMovieInfo(MovieInfo, folderInfo); + + result.Edition.Should().Be(folderInfo.Edition); + + MovieInfo.Edition = "Super duper awesome cut"; + + result = Subject.AugmentMovieInfo(MovieInfo, folderInfo); + + result.Edition.Should().Be(MovieInfo.Edition); + } + + [Test] + public void should_combine_languages() + { + var folderInfo = new ParsedMovieInfo + { + Languages = new List {Language.French} + }; + + MovieInfo.Languages = new List{Language.English}; + + var result = Subject.AugmentMovieInfo(MovieInfo, folderInfo); + + result.Languages.Should().BeEquivalentTo(Language.English, Language.French); + } + + [Test] + public void should_combine_formats() + { + var folderInfo = new ParsedMovieInfo + { + Quality = new QualityModel(Quality.Bluray1080p) + }; + + var format1 = new CustomFormats.CustomFormat("Awesome Format"); + format1.Id = 1; + + var format2 = new CustomFormats.CustomFormat("Cool Format"); + format2.Id = 2; + + folderInfo.Quality.CustomFormats = new List { format1 }; + + MovieInfo.Quality.CustomFormats = new List { format2 }; + + var result = Subject.AugmentMovieInfo(MovieInfo, folderInfo); + + result.Quality.CustomFormats.Count.Should().Be(2); + result.Quality.CustomFormats.Should().BeEquivalentTo(format2, format1); + + folderInfo.Quality.CustomFormats = new List { format1, format2 }; + + result = Subject.AugmentMovieInfo(MovieInfo, folderInfo); + + result.Quality.CustomFormats.Count.Should().Be(2); + result.Quality.CustomFormats.Should().BeEquivalentTo(format2, format1); + } + + [Test] + public void should_use_folder_release_group() + { + var folderInfo = new ParsedMovieInfo + { + ReleaseGroup = "AwesomeGroup" + }; + + MovieInfo.ReleaseGroup = ""; + + var result = Subject.AugmentMovieInfo(MovieInfo, folderInfo); + + result.ReleaseGroup.Should().BeEquivalentTo(folderInfo.ReleaseGroup); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index a6bc3d7c8..700c9b4f7 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -13,14 +13,14 @@ namespace NzbDrone.Core.Test.ParserTests public class QualityParserFixture : CoreTest { - /* + [SetUp] public void Setup() { - QualityDefinitionServiceFixture.SetupDefaultDefinitions(); + //QualityDefinitionServiceFixture.SetupDefaultDefinitions(); } - public static object[] SelfQualityParserCases = QualityDefinition.DefaultQualityDefinitions.ToArray(); + //public static object[] SelfQualityParserCases = QualityDefinition.DefaultQualityDefinitions.ToArray(); public static object[] OtherSourceQualityParserCases = { @@ -288,7 +288,7 @@ namespace NzbDrone.Core.Test.ParserTests ParseAndVerifyQuality(title, Source.UNKNOWN, proper, Resolution.Unknown); } - [Test, TestCaseSource("SelfQualityParserCases")] + /*[Test, TestCaseSource("SelfQualityParserCases")] public void parsing_our_own_quality_enum_name(QualityDefinition definition) { var fileName = string.Format("My series S01E01 [{0}]", definition.Title); @@ -300,7 +300,7 @@ namespace NzbDrone.Core.Test.ParserTests if (resolution != null) result.Resolution.Should().Be(resolution); if (modifier != null) result.Modifier.Should().Be(modifier); - } + }*/ [Test, TestCaseSource("OtherSourceQualityParserCases")] public void should_parse_quality_from_other_source(string qualityString, Source source, Resolution resolution, Modifier modifier = Modifier.NONE) @@ -352,6 +352,6 @@ namespace NzbDrone.Core.Test.ParserTests var version = proper ? 2 : 1; result.Revision.Version.Should().Be(version); - }*/ + } } } diff --git a/src/NzbDrone.Core.Test/packages.config b/src/NzbDrone.Core.Test/packages.config index 0efd582ac..029d08f0a 100644 --- a/src/NzbDrone.Core.Test/packages.config +++ b/src/NzbDrone.Core.Test/packages.config @@ -13,5 +13,6 @@ + \ No newline at end of file diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormat.cs b/src/NzbDrone.Core/CustomFormats/CustomFormat.cs index 5d6091448..1cd176400 100644 --- a/src/NzbDrone.Core/CustomFormats/CustomFormat.cs +++ b/src/NzbDrone.Core/CustomFormats/CustomFormat.cs @@ -49,4 +49,20 @@ namespace NzbDrone.Core.CustomFormats return (Id != null ? Id.GetHashCode() : 0); } } + + public static class CustomFormatExtensions + { + public static string ToExtendedString(this IEnumerable formats) + { + return string.Join(", ", formats.Select(f => f.ToString())); + } + + public static List WithNone(this IEnumerable formats) + { + var list = formats.ToList(); + if (list.Any()) return list; + + return new List{CustomFormat.None}; + } + } } diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormatService.cs b/src/NzbDrone.Core/CustomFormats/CustomFormatService.cs index 26213b006..849545a04 100644 --- a/src/NzbDrone.Core/CustomFormats/CustomFormatService.cs +++ b/src/NzbDrone.Core/CustomFormats/CustomFormatService.cs @@ -1,11 +1,15 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.Remoting.Messaging; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Composition; +using NzbDrone.Core.Blacklisting; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.History; using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Profiles; @@ -17,6 +21,7 @@ namespace NzbDrone.Core.CustomFormats CustomFormat Insert(CustomFormat customFormat); List All(); CustomFormat GetById(int id); + void Delete(int id); } @@ -24,6 +29,10 @@ namespace NzbDrone.Core.CustomFormats { private readonly ICustomFormatRepository _formatRepository; private IProfileService _profileService; + private readonly IMediaFileService _mediaFileService; + private readonly IBlacklistService _blacklistService; + private readonly IHistoryService _historyService; + private readonly IPendingReleaseService _pendingReleaseService; public IProfileService ProfileService { @@ -45,12 +54,18 @@ namespace NzbDrone.Core.CustomFormats public static Dictionary AllCustomFormats; public CustomFormatService(ICustomFormatRepository formatRepository, ICacheManager cacheManager, - IContainer container, + IContainer container, IHistoryService historyService,/*IMediaFileService mediaFileService, IBlacklistService blacklistService, + IHistoryService historyService, IPendingReleaseService pendingReleaseService,*/ Logger logger) { _formatRepository = formatRepository; _container = container; _cache = cacheManager.GetCache>(typeof(CustomFormat), "formats"); + /*_mediaFileService = mediaFileService; + _blacklistService = blacklistService; + _historyService = historyService; + _pendingReleaseService = pendingReleaseService;*/ + _historyService = historyService; _logger = logger; } @@ -69,7 +84,7 @@ namespace NzbDrone.Core.CustomFormats } catch (Exception e) { - _logger.Error("Failure while trying to add the new custom format to all profiles.", e); + _logger.Error(e, "Failure while trying to add the new custom format to all profiles. Deleting again!"); _formatRepository.Delete(ret); throw; } @@ -77,6 +92,76 @@ namespace NzbDrone.Core.CustomFormats return ret; } + public void Delete(int id) + { + _cache.Clear(); + try + { + //First history: + var historyRepo = _container.Resolve(); + DeleteInRepo(historyRepo, h => h.Quality.CustomFormats, (h, f) => + { + h.Quality.CustomFormats = f; + return h; + }, id); + + //Then Blacklist: + var blacklistRepo = _container.Resolve(); + DeleteInRepo(blacklistRepo, h => h.Quality.CustomFormats, (h, f) => + { + h.Quality.CustomFormats = f; + return h; + }, id); + + //Then MovieFiles: + var moviefileRepo = _container.Resolve(); + DeleteInRepo(moviefileRepo, h => h.Quality.CustomFormats, (h, f) => + { + h.Quality.CustomFormats = f; + return h; + }, id); + + //Then Profiles + ProfileService.DeleteCustomFormat(id); + } + catch (Exception e) + { + _logger.Error(e, "Failed to delete format with id {} from other repositories! Format will not be deleted!", id); + throw; + } + + //Finally delete the format for real! + _formatRepository.Delete(id); + + _cache.Clear(); + } + + private void DeleteInRepo(IBasicRepository repository, Func> queryFunc, + Func, TModel> updateFunc, int customFormatId) where TModel : ModelBase, new() + { + var pagingSpec = new PagingSpec + { + Page = 0, + PageSize = 2000 + }; + while (true) + { + var allItems = repository.GetPaged(pagingSpec); + var toUpdate = allItems.Records.Where(r => queryFunc(r).Exists(c => c.Id == customFormatId)).Select(r => + { + return updateFunc(r, queryFunc(r).Where(c => c.Id != customFormatId).ToList()); + }); + repository.UpdateMany(toUpdate.ToList()); + + if (pagingSpec.Page * pagingSpec.PageSize >= allItems.TotalRecords) + { + break; + } + + pagingSpec.Page += 1; + } + } + private Dictionary AllDictionary() { return _cache.Get("all", () => diff --git a/src/NzbDrone.Core/CustomFormats/FormatTag.cs b/src/NzbDrone.Core/CustomFormats/FormatTag.cs index fa8d50cca..e1a6a8ab6 100644 --- a/src/NzbDrone.Core/CustomFormats/FormatTag.cs +++ b/src/NzbDrone.Core/CustomFormats/FormatTag.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using NzbDrone.Common.Extensions; @@ -14,7 +15,9 @@ namespace NzbDrone.Core.CustomFormats public TagModifier TagModifier { get; set; } public object Value { get; set; } - public static Regex QualityTagRegex = new Regex(@"^(?R|S|M|E|L|C|I)(_((?R)|(?RE)|(?N)){1,3})?_(?.*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public static Regex QualityTagRegex = new Regex(@"^(?R|S|M|E|L|C|I|G)(_((?RX)|(?RQ)|(?N)){1,3})?_(?.*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static Regex SizeTagRegex = new Regex(@"(?\d+(\.\d+)?)\s*<>\s*(?\d+(\.\d+)?)", RegexOptions.Compiled | RegexOptions.IgnoreCase); public FormatTag(string raw) { @@ -29,6 +32,7 @@ namespace NzbDrone.Core.CustomFormats ParseRawMatch(match); } + // This function is needed for json deserialization to work. private FormatTag() { @@ -74,6 +78,10 @@ namespace NzbDrone.Core.CustomFormats return movieInfo.Quality.Modifier == (Modifier) Value; case TagType.Source: return movieInfo.Quality.Source == (Source) Value; + case TagType.Size: + var size = (movieInfo.ExtraInfo.GetValueOrDefault("Size", 0.0) as long?) ?? 0; + var tuple = Value as (long, long)? ?? (0, 0); + return size > tuple.Item1 && size < tuple.Item2; case TagType.Indexer: return (movieInfo.ExtraInfo.GetValueOrDefault("IndexerFlags") as IndexerFlags?)?.HasFlag((IndexerFlags) Value) == true; default: @@ -191,6 +199,13 @@ namespace NzbDrone.Core.CustomFormats break; } + break; + case "g": + TagType = TagType.Size; + var matches = SizeTagRegex.Match(value); + var min = double.Parse(matches.Groups["min"].Value, CultureInfo.InvariantCulture); + var max = double.Parse(matches.Groups["max"].Value, CultureInfo.InvariantCulture); + Value = (min.Gigabytes(), max.Gigabytes()); break; case "c": default: @@ -218,6 +233,7 @@ namespace NzbDrone.Core.CustomFormats Language = 16, Custom = 32, Indexer = 64, + Size = 128, } [Flags] diff --git a/src/NzbDrone.Core/Datastore/Migration/149_convert_regex_required_tags.cs b/src/NzbDrone.Core/Datastore/Migration/149_convert_regex_required_tags.cs new file mode 100644 index 000000000..be3350b87 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/149_convert_regex_required_tags.cs @@ -0,0 +1,124 @@ +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text.RegularExpressions; +using FluentMigrator; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(149)] + public class convert_regex_required_tags : NzbDroneMigrationBase + { + public static Regex OriginalRegex = new Regex(@"^(?R|S|M|E|L|C|I|G)(_((?R)|(?RE)|(?N)){1,3})?_(?.*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + protected override void MainDbUpgrade() + { + Execute.WithConnection(ConvertExistingFormatTags); + } + + private void ConvertExistingFormatTags(IDbConnection conn, IDbTransaction tran) + { + var updater = new CustomFormatUpdater149(conn, tran); + + updater.ReplaceInTags(OriginalRegex, match => + { + var modifiers = ""; + if (match.Groups["m_n"].Success) modifiers += "N"; + if (match.Groups["m_r"].Success) modifiers += "RX"; + if (match.Groups["m_re"].Success) modifiers += "RQ"; + return $"{match.Groups["type"].Value}_{modifiers}_{match.Groups["value"].Value}"; + }); + + updater.Commit(); + } + } + + public class CustomFormat149 + { + public int Id { get; set; } + public string Name { get; set; } + public List FormatTags { get; set; } + } + + public class CustomFormatUpdater149 + { + private readonly IDbConnection _connection; + private readonly IDbTransaction _transaction; + + private List _customFormats; + private HashSet _changedFormats = new HashSet(); + + public CustomFormatUpdater149(IDbConnection conn, IDbTransaction tran) + { + _connection = conn; + _transaction = tran; + + _customFormats = GetFormats(); + } + + public void Commit() + { + foreach (var profile in _changedFormats) + { + using (var updateProfileCmd = _connection.CreateCommand()) + { + updateProfileCmd.Transaction = _transaction; + updateProfileCmd.CommandText = "UPDATE CustomFormats SET Name = ?, FormatTags = ? WHERE Id = ?"; + updateProfileCmd.AddParameter(profile.Name); + updateProfileCmd.AddParameter(profile.FormatTags.ToJson()); + updateProfileCmd.AddParameter(profile.Id); + + updateProfileCmd.ExecuteNonQuery(); + } + } + + _changedFormats.Clear(); + } + + public void ReplaceInTags(Regex search, string replacement) + { + foreach (var format in _customFormats) + { + format.FormatTags.ForEach(t => { search.Replace(t, replacement); }); + _changedFormats.Add(format); + } + } + + public void ReplaceInTags(Regex search, MatchEvaluator evaluator) + { + foreach (var format in _customFormats) + { + format.FormatTags = format.FormatTags.Select(t => search.Replace(t, evaluator)).ToList(); + _changedFormats.Add(format); + } + } + + private List GetFormats() + { + var profiles = new List(); + + using (var getProfilesCmd = _connection.CreateCommand()) + { + getProfilesCmd.Transaction = _transaction; + getProfilesCmd.CommandText = @"SELECT Id, Name, FormatTags FROM CustomFormats"; + + using (var profileReader = getProfilesCmd.ExecuteReader()) + { + while (profileReader.Read()) + { + profiles.Add(new CustomFormat149 + { + Id = profileReader.GetInt32(0), + Name = profileReader.GetString(1), + FormatTags = Json.Deserialize>(profileReader.GetString(2)) + }); + } + } + } + + return profiles; + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs index 1ec20781b..ae97292db 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs @@ -69,11 +69,7 @@ namespace NzbDrone.Core.DecisionEngine private int CompareCustomFormats(DownloadDecision x, DownloadDecision y) { - var left = x.RemoteMovie.ParsedMovieInfo.Quality.CustomFormats.ToArray().ToList(); - if (left.Count == 0) - { - left.Add(CustomFormat.None); - } + var left = x.RemoteMovie.ParsedMovieInfo.Quality.CustomFormats.WithNone(); var right = y.RemoteMovie.ParsedMovieInfo.Quality.CustomFormats; var leftIndicies = QualityModelComparer.GetIndicies(left, x.RemoteMovie.Movie.Profile.Value); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/CustomFormatAllowedByProfileSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/CustomFormatAllowedByProfileSpecification.cs new file mode 100644 index 000000000..ef5f626cb --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/CustomFormatAllowedByProfileSpecification.cs @@ -0,0 +1,35 @@ +using System.Linq; +using NLog; +using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.DecisionEngine.Specifications +{ + public class CustomFormatAllowedbyProfileSpecification : IDecisionEngineSpecification + { + private readonly Logger _logger; + + public CustomFormatAllowedbyProfileSpecification(Logger logger) + { + _logger = logger; + } + + public RejectionType Type => RejectionType.Permanent; + + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + var formats = subject.ParsedMovieInfo.Quality.CustomFormats.WithNone(); + _logger.Debug("Checking if report meets custom format requirements. {0}", formats.ToExtendedString()); + var notAllowedFormats = subject.Movie.Profile.Value.FormatItems.Where(v => v.Allowed == false).Select(f => f.Format).ToList(); + var notWantedFormats = notAllowedFormats.Intersect(formats); + if (notWantedFormats.Any()) + { + _logger.Debug("Custom Formats {0} rejected by Movie's profile", notWantedFormats.ToExtendedString()); + return Decision.Reject("Custom Formats {0} not wanted in profile", notWantedFormats.ToExtendedString()); + } + + return Decision.Accept(); + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs index e87cc4331..3cf1d296c 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs @@ -20,6 +20,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { var wantedLanguage = subject.Movie.Profile.Value.Language; + if (wantedLanguage == Language.Any) + { + _logger.Debug("Profile allows any language, accepting release."); + return Decision.Accept(); + } + _logger.Debug("Checking if report meets language requirements. {0}", subject.ParsedMovieInfo.Languages.ToExtendedString()); if (!subject.ParsedMovieInfo.Languages.Contains(wantedLanguage)) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RequiredIndexerFlagsSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RequiredIndexerFlagsSpecification.cs index d47cf6050..cef33c0a7 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RequiredIndexerFlagsSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RequiredIndexerFlagsSpecification.cs @@ -1,13 +1,11 @@ using System; -using System.Collections.Generic; using System.Linq; using NLog; -using NzbDrone.Core.Datastore; using NzbDrone.Core.Indexers; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; -namespace NzbDrone.Core.DecisionEngine.Specifications.Search +namespace NzbDrone.Core.DecisionEngine.Specifications { public class RequiredIndexerFlagsSpecification : IDecisionEngineSpecification { diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/TorrentSeedingSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/TorrentSeedingSpecification.cs index be9fecd48..81b72f3b5 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/TorrentSeedingSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/TorrentSeedingSpecification.cs @@ -1,11 +1,10 @@ using System; using NLog; -using NzbDrone.Core.Datastore; using NzbDrone.Core.Indexers; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; -namespace NzbDrone.Core.DecisionEngine.Specifications.Search +namespace NzbDrone.Core.DecisionEngine.Specifications { public class TorrentSeedingSpecification : IDecisionEngineSpecification { diff --git a/src/NzbDrone.Core/History/HistoryRepository.cs b/src/NzbDrone.Core/History/HistoryRepository.cs index 6f433c528..1ff9653e1 100644 --- a/src/NzbDrone.Core/History/HistoryRepository.cs +++ b/src/NzbDrone.Core/History/HistoryRepository.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.History History MostRecentForDownloadId(string downloadId); List FindByDownloadId(string downloadId); List FindDownloadHistory(int idMovieId, QualityModel quality); + List FindByMovieId(int movieId); void DeleteForMovie(int movieId); History MostRecentForMovie(int movieId); } @@ -57,6 +58,11 @@ namespace NzbDrone.Core.History ).ToList(); } + public List FindByMovieId(int movieId) + { + return Query.Where(h => h.MovieId == movieId); + } + public void DeleteForMovie(int movieId) { Delete(c => c.MovieId == movieId); diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index e1045455c..492d0de7b 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -25,6 +25,8 @@ namespace NzbDrone.Core.History History Get(int historyId); List Find(string downloadId, HistoryEventType eventType); List FindByDownloadId(string downloadId); + List FindByMovieId(int movieId); + void UpdateMany(List toUpdate); } public class HistoryService : IHistoryService, @@ -73,6 +75,11 @@ namespace NzbDrone.Core.History return _historyRepository.FindByDownloadId(downloadId); } + public List FindByMovieId(int movieId) + { + return _historyRepository.FindByMovieId(movieId); + } + public QualityModel GetBestQualityInHistory(Profile profile, int movieId) { var comparer = new QualityModelComparer(profile); @@ -81,6 +88,11 @@ namespace NzbDrone.Core.History .FirstOrDefault(); } + public void UpdateMany(List toUpdate) + { + _historyRepository.UpdateMany(toUpdate); + } + public void Handle(MovieGrabbedEvent message) { var history = new History diff --git a/src/NzbDrone.Core/MediaFiles/Commands/UpdateMovieFileQualityCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/UpdateMovieFileQualityCommand.cs new file mode 100644 index 000000000..7d5cc43fd --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Commands/UpdateMovieFileQualityCommand.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.MediaFiles.Commands +{ + public class UpdateMovieFileQualityCommand : Command + { + public IEnumerable MovieFileIds { get; set; } + + public override bool SendUpdatesToClient => true; + + public UpdateMovieFileQualityCommand(IEnumerable movieFileIds) + { + MovieFileIds = movieFileIds; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/MovieImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/MovieImport/ImportDecisionMaker.cs index 43f023951..9b6899253 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieImport/ImportDecisionMaker.cs @@ -112,32 +112,35 @@ namespace NzbDrone.Core.MediaFiles.MovieImport try { - var minimalInfo = shouldUseFolderName - ? folderInfo.JsonClone() - : _parsingService.ParseMinimalPathMovieInfo(file); + ParsedMovieInfo modifiedFolderInfo = null; + if (folderInfo != null) + { + modifiedFolderInfo = folderInfo.JsonClone(); + // We want the filename to be used for parsing quality, etc. even if we didn't get any movie info from there. + modifiedFolderInfo.SimpleReleaseTitle = Path.GetFileName(file); + } + + var minimalInfo = _parsingService.ParseMinimalPathMovieInfo(file) ?? modifiedFolderInfo; LocalMovie localMovie = null; - //var localMovie = _parsingService.GetLocalMovie(file, movie, shouldUseFolderName ? folderInfo : null, sceneSource); if (minimalInfo != null) { //TODO: make it so media info doesn't ruin the import process of a new movie - var mediaInfo = (_config.EnableMediaInfo || !movie.Path.IsParentPath(file)) ? _videoFileInfoReader.GetMediaInfo(file) : null; + var mediaInfo = (_config.EnableMediaInfo || !movie.Path?.IsParentPath(file) == true) ? _videoFileInfoReader.GetMediaInfo(file) : null; var size = _diskProvider.GetFileSize(file); var historyItems = _historyService.FindByDownloadId(downloadClientItem?.DownloadId ?? ""); - var firstHistoryItem = historyItems.OrderByDescending(h => h.Date).FirstOrDefault(); + var firstHistoryItem = historyItems?.OrderByDescending(h => h.Date)?.FirstOrDefault(); var sizeMovie = new LocalMovie(); sizeMovie.Size = size; - localMovie = _parsingService.GetLocalMovie(file, minimalInfo, movie, new List{mediaInfo, firstHistoryItem, sizeMovie}, sceneSource); + localMovie = _parsingService.GetLocalMovie(file, minimalInfo, movie, new List{mediaInfo, firstHistoryItem, sizeMovie, folderInfo}, sceneSource); localMovie.Quality = GetQuality(folderInfo, localMovie.Quality, movie); localMovie.Size = size; _logger.Debug("Size: {0}", localMovie.Size); decision = GetDecision(localMovie, downloadClientItem); - } - else { localMovie = new LocalMovie(); @@ -201,38 +204,10 @@ namespace NzbDrone.Core.MediaFiles.MovieImport return null; } + //TODO: Remove this method, since it is no longer needed. private bool ShouldUseFolderName(List videoFiles, Movie movie, ParsedMovieInfo folderInfo) { - if (folderInfo == null) - { - return false; - } - - //if (folderInfo.FullSeason) - //{ - // return false; - //} - - return videoFiles.Count(file => - { - var size = _diskProvider.GetFileSize(file); - var fileQuality = QualityParser.ParseQuality(file); - //var sample = null;//_detectSample.IsSample(movie, GetQuality(folderInfo, fileQuality, movie), file, size, folderInfo.IsPossibleSpecialEpisode); //Todo to this - - return true; - - //if (sample) - { - return false; - } - - if (SceneChecker.IsSceneTitle(Path.GetFileName(file))) - { - return false; - } - - return true; - }) == 1; + return false; } private QualityModel GetQuality(ParsedMovieInfo folderInfo, QualityModel fileQuality, Movie movie) diff --git a/src/NzbDrone.Core/MediaFiles/UpdateMovieFileQualityService.cs b/src/NzbDrone.Core/MediaFiles/UpdateMovieFileQualityService.cs new file mode 100644 index 000000000..ece777053 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/UpdateMovieFileQualityService.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Core.History; +using NzbDrone.Core.MediaFiles.Commands; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles +{ + public interface IUpdateMovieFileQualityService + { + + } + + public class UpdateMovieFileQualityService : IUpdateMovieFileQualityService, IExecute + { + private readonly IMediaFileService _mediaFileService; + private readonly IHistoryService _historyService; + private readonly IParsingService _parsingService; + private readonly IEventAggregator _eventAggregator; + private readonly Logger _logger; + + public UpdateMovieFileQualityService(IMediaFileService mediaFileService, + IHistoryService historyService, + IParsingService parsingService, + IEventAggregator eventAggregator, + Logger logger) + { + _mediaFileService = mediaFileService; + _historyService = historyService; + _parsingService = parsingService; + _eventAggregator = eventAggregator; + _logger = logger; + } + + //TODO add some good tests for this! + public void Execute(UpdateMovieFileQualityCommand command) + { + var movieFiles = _mediaFileService.GetMovies(command.MovieFileIds); + + var count = 1; + + foreach (var movieFile in movieFiles) + { + _logger.ProgressInfo("Updating quality for {0}/{1} files.", count, movieFiles.Count); + + var history = _historyService.FindByMovieId(movieFile.MovieId).OrderByDescending(h => h.Date); + var latestImported = history.FirstOrDefault(h => h.EventType == HistoryEventType.DownloadFolderImported); + var latestGrabbed = history.FirstOrDefault(h => h.EventType == HistoryEventType.Grabbed); + var sizeMovie = new LocalMovie(); + sizeMovie.Size = movieFile.Size; + + var helpers = new List{sizeMovie}; + + if (movieFile.MediaInfo != null) + { + helpers.Add(movieFile.MediaInfo); + } + + if (latestGrabbed != null) + { + helpers.Add(latestGrabbed); + } + + var parsedMovieInfo = _parsingService.ParseMovieInfo(latestImported?.SourceTitle ?? movieFile.RelativePath, helpers); + + //Only update Custom formats for now. + if (parsedMovieInfo != null) + { + movieFile.Quality.CustomFormats = parsedMovieInfo.Quality.CustomFormats; + _mediaFileService.Update(movieFile); + _eventAggregator.PublishEvent(new MovieFileUpdatedEvent(movieFile)); + } + else + { + _logger.Debug("Could not update custom formats for {0}, since it's title could not be parsed!", movieFile); + } + + count++; + } + + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index f8ec29f74..a7f78458a 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -58,7 +58,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook public Movie GetMovieInfo(int TmdbId, Profile profile = null, bool hasPreDBEntry = false) { - var langCode = profile != null ? IsoLanguages.Get(profile.Language).TwoLetterCode : "en"; + var langCode = profile != null ? IsoLanguages.Get(profile.Language)?.TwoLetterCode ?? "en" : "en"; var request = _movieBuilder.Create() .SetSegment("route", "movie") @@ -140,7 +140,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook movie.ImdbId = resource.imdb_id; movie.Title = resource.title; movie.TitleSlug = Parser.Parser.ToUrlSlug(resource.title); - movie.CleanTitle = Parser.Parser.CleanSeriesTitle(resource.title); + movie.CleanTitle = resource.title.CleanSeriesTitle(); movie.SortTitle = Parser.Parser.NormalizeTitle(resource.title); movie.Overview = resource.overview; movie.Website = resource.homepage; diff --git a/src/NzbDrone.Core/Movies/QueryExtensions.cs b/src/NzbDrone.Core/Movies/QueryExtensions.cs index db961f532..b0c5d3604 100644 --- a/src/NzbDrone.Core/Movies/QueryExtensions.cs +++ b/src/NzbDrone.Core/Movies/QueryExtensions.cs @@ -1,16 +1,8 @@ -using System; +using System.Collections.Generic; using System.Linq; -using System.Collections.Generic; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Datastore.Extensions; using Marr.Data.QGen; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Parser.RomanNumerals; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Movies; -using CoreParser = NzbDrone.Core.Parser.Parser; -namespace NzbDrone.Core + +namespace NzbDrone.Core.Movies { public static class QueryExtensions { diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index e9b1011c7..5466109d8 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -101,6 +101,9 @@ + + ..\packages\System.ValueTuple.4.5.0\lib\portable-net40+sl4+win8+wp8\System.ValueTuple.dll + @@ -141,11 +144,14 @@ + + + @@ -157,6 +163,7 @@ + @@ -973,6 +980,7 @@ + diff --git a/src/NzbDrone.Core/Parser/Augmenters/AugmentWithParsedMovieInfo.cs b/src/NzbDrone.Core/Parser/Augmenters/AugmentWithParsedMovieInfo.cs new file mode 100644 index 000000000..35986aa33 --- /dev/null +++ b/src/NzbDrone.Core/Parser/Augmenters/AugmentWithParsedMovieInfo.cs @@ -0,0 +1,51 @@ +using System; +using System.Linq; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Parser.Augmenters +{ + public class AugmentWithParsedMovieInfo : IAugmentParsedMovieInfo + { + public Type HelperType + { + get + { + return typeof(ParsedMovieInfo); + } + } + + public ParsedMovieInfo AugmentMovieInfo(ParsedMovieInfo movieInfo, object helper) + { + if (helper is ParsedMovieInfo otherInfo) + { + // Create union of all languages + if (otherInfo.Languages != null) + { + movieInfo.Languages = movieInfo.Languages.Union(otherInfo.Languages).Distinct().ToList(); + } + + if ((otherInfo.Edition?.Length ?? 0) > (movieInfo.Edition?.Length ?? 0)) + { + movieInfo.Edition = otherInfo.Edition; + } + + if (otherInfo.Quality != null) + { + movieInfo.Quality.CustomFormats = movieInfo.Quality.CustomFormats.Union(otherInfo.Quality.CustomFormats) + .Distinct().ToList(); + } + + if (otherInfo.ReleaseGroup.IsNotNullOrWhiteSpace() && movieInfo.ReleaseGroup.IsNullOrWhiteSpace()) + { + movieInfo.ReleaseGroup = otherInfo.ReleaseGroup; + } + } + + return movieInfo; + } + } +} diff --git a/src/NzbDrone.Core/Parser/IsoLanguages.cs b/src/NzbDrone.Core/Parser/IsoLanguages.cs index 017d5ab06..289288f1c 100644 --- a/src/NzbDrone.Core/Parser/IsoLanguages.cs +++ b/src/NzbDrone.Core/Parser/IsoLanguages.cs @@ -29,7 +29,8 @@ namespace NzbDrone.Core.Parser new IsoLanguage("el", "ell", Language.Greek), new IsoLanguage("ko", "kor", Language.Korean), new IsoLanguage("hu", "hun", Language.Hungarian), - new IsoLanguage("he", "heb", Language.Hebrew) + new IsoLanguage("he", "heb", Language.Hebrew), + new IsoLanguage("an", "any", Language.Any) }; public static IsoLanguage Find(string isoCode) diff --git a/src/NzbDrone.Core/Parser/Language.cs b/src/NzbDrone.Core/Parser/Language.cs index e4cd1e76f..37e47ff9d 100644 --- a/src/NzbDrone.Core/Parser/Language.cs +++ b/src/NzbDrone.Core/Parser/Language.cs @@ -28,9 +28,10 @@ namespace NzbDrone.Core.Parser Greek = 20, Korean = 21, Hungarian = 22, - Hebrew = 23 + Hebrew = 23, + Any = -1, } - + public static class LanguageExtensions { public static string ToExtendedString(this IEnumerable languages) diff --git a/src/NzbDrone.Core/Profiles/ProfileService.cs b/src/NzbDrone.Core/Profiles/ProfileService.cs index 0bf99e07a..abc92b749 100644 --- a/src/NzbDrone.Core/Profiles/ProfileService.cs +++ b/src/NzbDrone.Core/Profiles/ProfileService.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.Profiles Profile Add(Profile profile); void Update(Profile profile); void AddCustomFormat(CustomFormat format); + void DeleteCustomFormat(int formatId); void Delete(int id); List All(); Profile Get(int id); @@ -65,6 +66,21 @@ namespace NzbDrone.Core.Profiles } } + public void DeleteCustomFormat(int formatId) + { + var all = All(); + foreach (var profile in all) + { + profile.FormatItems = profile.FormatItems.Where(c => c.Format.Id != formatId).ToList(); + if (profile.FormatCutoff.Id == formatId) + { + profile.FormatCutoff = CustomFormat.None; + } + + Update(profile); + } + } + public void Delete(int id) { if (_movieService.GetAllMovies().Any(c => c.ProfileId == id) || _netImportFactory.All().Any(c => c.ProfileId == id)) diff --git a/src/NzbDrone.Core/Qualities/QualityModel.cs b/src/NzbDrone.Core/Qualities/QualityModel.cs index 2ad4cf1ce..cab450b20 100644 --- a/src/NzbDrone.Core/Qualities/QualityModel.cs +++ b/src/NzbDrone.Core/Qualities/QualityModel.cs @@ -42,8 +42,7 @@ namespace NzbDrone.Core.Qualities public override string ToString() { - var formats = CustomFormats.Count > 0 ? CustomFormats : new List {CustomFormat.None}; - return string.Format("{0} {1} ({2})", Quality, Revision, string.Join(", ", formats)); + return string.Format("{0} {1} ({2})", Quality, Revision, CustomFormats.WithNone().ToExtendedString()); } public override int GetHashCode() diff --git a/src/NzbDrone.Core/Qualities/QualityModelComparer.cs b/src/NzbDrone.Core/Qualities/QualityModelComparer.cs index 27467921e..ca29f6f89 100644 --- a/src/NzbDrone.Core/Qualities/QualityModelComparer.cs +++ b/src/NzbDrone.Core/Qualities/QualityModelComparer.cs @@ -41,9 +41,7 @@ namespace NzbDrone.Core.Qualities public static List GetIndicies(List formats, Profile profile) { - return formats.Count > 0 - ? formats.Select(f => profile.FormatItems.FindIndex(v => Equals(v.Format, f))).ToList() - : new List {profile.FormatItems.FindIndex(v => Equals(v.Format, CustomFormat.None))}; + return formats.WithNone().Select(f => profile.FormatItems.FindIndex(v => Equals(v.Format, f))).ToList(); } public int Compare(CustomFormat left, CustomFormat right) @@ -56,10 +54,7 @@ namespace NzbDrone.Core.Qualities public int Compare(List left, CustomFormat right) { - if (left.Count == 0) - { - left.Add(CustomFormat.None); - } + left = left.WithNone(); var leftIndicies = GetIndicies(left, _profile); var rightIndex = _profile.FormatItems.FindIndex(v => Equals(v.Format, right)); diff --git a/src/NzbDrone.Core/packages.config b/src/NzbDrone.Core/packages.config index 530314fd0..4f7874aa4 100644 --- a/src/NzbDrone.Core/packages.config +++ b/src/NzbDrone.Core/packages.config @@ -9,6 +9,7 @@ + \ No newline at end of file diff --git a/src/UI/ManualImport/Quality/SelectQualityLayout.js b/src/UI/ManualImport/Quality/SelectQualityLayout.js index beba005e9..98ce696fe 100644 --- a/src/UI/ManualImport/Quality/SelectQualityLayout.js +++ b/src/UI/ManualImport/Quality/SelectQualityLayout.js @@ -31,8 +31,11 @@ module.exports = Marionette.Layout.extend({ var qualities = _.map(this.profileSchemaCollection.first().get('items'), function (quality) { return quality.quality; }); + var formats = _.map(this.profileSchemaCollection.first().get('formatItems'), function (format) { + return format.format; + }); - this.selectQualityView = new SelectQualityView({ qualities: qualities }); + this.selectQualityView = new SelectQualityView({ qualities: qualities, formats : formats }); this.quality.show(this.selectQualityView); }, @@ -40,4 +43,4 @@ module.exports = Marionette.Layout.extend({ this.trigger('manualimport:selected:quality', { quality: this.selectQualityView.selectedQuality() }); vent.trigger(vent.Commands.CloseModal2Command); } -}); \ No newline at end of file +}); diff --git a/src/UI/ManualImport/Quality/SelectQualityView.js b/src/UI/ManualImport/Quality/SelectQualityView.js index 8a39fab82..2e864fdd4 100644 --- a/src/UI/ManualImport/Quality/SelectQualityView.js +++ b/src/UI/ManualImport/Quality/SelectQualityView.js @@ -1,22 +1,40 @@ var _ = require('underscore'); var Marionette = require('marionette'); +var Backbone = require('backbone'); +require('../../Mixins/TagInput'); module.exports = Marionette.ItemView.extend({ template : 'ManualImport/Quality/SelectQualityViewTemplate', ui : { select : '.x-select-quality', - proper : 'x-proper' + proper : 'x-proper', + formats: '.x-tags', }, initialize : function(options) { this.qualities = options.qualities; + this.formats = options.formats; + this.current = options.current || {}; this.templateHelpers = { - qualities: this.qualities + qualities: this.qualities, + formats: JSON.stringify(_.map(this.formats, function(f) { + return { value : f.id, name : f.name }; + })), }; }, + onRender : function() { + if (this.current.formats != undefined) { + this.ui.formats.val(this.current.formats.map(function(m) {return m.id;}).join(",")); + } + if (this.current.quality != undefined) { + this.ui.select.val(this.current.quality.id); + } + this.ui.formats.tagInput(); + }, + selectedQuality : function () { var selected = parseInt(this.ui.select.val(), 10); var proper = this.ui.proper.prop('checked'); @@ -25,13 +43,21 @@ module.exports = Marionette.ItemView.extend({ return q.id === selected; }); + var formatIds = this.ui.formats.val().split(','); + + var formats = _.map(_.filter(this.formats, function(f) { + return formatIds.includes(f.id + ""); + }), function(f) { + return { name : f.name, id : f.id}; + }); return { quality : quality, revision : { version : proper ? 2 : 1, real : 0 - } + }, + customFormats : formats }; } -}); \ No newline at end of file +}); diff --git a/src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs b/src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs index a04342280..853f3a42b 100644 --- a/src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs +++ b/src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs @@ -30,4 +30,12 @@ + +
+ + +
+ +
+
diff --git a/src/UI/Mixins/TagInput.js b/src/UI/Mixins/TagInput.js index 47fcd9296..9090d8a4c 100644 --- a/src/UI/Mixins/TagInput.js +++ b/src/UI/Mixins/TagInput.js @@ -113,7 +113,7 @@ $.fn.tagInput = function(options) { var input = $(this); var tagInput = null; - + if (input[0].hasAttribute('tag-source')) { var listItems = JSON.parse(input.attr('tag-source')); @@ -124,6 +124,7 @@ $.fn.tagInput = function(options) { allowDuplicates: false, itemValue: 'value', itemText: 'name', + tagClass : input.attr('tag-class-name') || "label label-info", typeaheadjs: { displayKey: 'name', source: substringMatcher(listItems, function (t) { return t.name; }) @@ -195,4 +196,4 @@ $.fn.tagInput = function(options) { }); -}; \ No newline at end of file +}; diff --git a/src/UI/Movies/Editor/MovieEditorFooterView.js b/src/UI/Movies/Editor/MovieEditorFooterView.js index 9be7fccde..44ca244e0 100644 --- a/src/UI/Movies/Editor/MovieEditorFooterView.js +++ b/src/UI/Movies/Editor/MovieEditorFooterView.js @@ -25,7 +25,8 @@ module.exports = Marionette.ItemView.extend({ events : { 'click .x-save' : '_updateAndSave', 'change .x-root-folder' : '_rootFolderChanged', - 'click .x-organize-files' : '_organizeFiles' + 'click .x-organize-files' : '_organizeFiles', + 'click .x-update-quality' : '_updateQuality' }, templateHelpers : function() { diff --git a/src/UI/Movies/Editor/MovieEditorLayout.js b/src/UI/Movies/Editor/MovieEditorLayout.js index 08d9f4bb7..ec88df748 100644 --- a/src/UI/Movies/Editor/MovieEditorLayout.js +++ b/src/UI/Movies/Editor/MovieEditorLayout.js @@ -16,6 +16,7 @@ var GridPager = require('../../Shared/Grid/Pager'); require('../../Mixins/backbone.signalr.mixin'); var DeleteSelectedView = require('./Delete/DeleteSelectedView'); var Config = require('../../Config'); +var CommandController = require('../../Commands/CommandController'); window.shownOnce = false; module.exports = Marionette.Layout.extend({ @@ -116,6 +117,12 @@ module.exports = Marionette.Layout.extend({ successMessage : 'Library was updated!', errorMessage : 'Library update failed!' }, + { + title : 'Update Custom Formats', + icon : 'icon-radarr-refresh', + className : 'btn-danger', + callback : this._updateQuality + }, { title : 'Delete selected', icon : 'icon-radarr-delete-white', @@ -296,6 +303,20 @@ module.exports = Marionette.Layout.extend({ }); this.movieCollection.setPageSize(pageSize, {fetch: false}); this.movieCollection.getPage(currentPage, {fetch: false}); - } + }, + + _updateQuality : function() { + var selected = FullMovieCollection.where({ selected : true}); + var files = selected.filter(function(model) { + return model.get("movieFile") !== undefined; + }).map(function(model){ + return model.get("movieFile").id; + }); + + CommandController.Execute('updateMovieFileQuality', { + name : 'updateMovieFileQuality', + movieFileIds : files + }); + } }); diff --git a/src/UI/Movies/Files/Media/Edit/EditFileTemplate.hbs b/src/UI/Movies/Files/Media/Edit/EditFileTemplate.hbs index 360a210fa..794fafe4b 100644 --- a/src/UI/Movies/Files/Media/Edit/EditFileTemplate.hbs +++ b/src/UI/Movies/Files/Media/Edit/EditFileTemplate.hbs @@ -4,25 +4,11 @@

{{relativePath}}

+
- +
The Format Tag is required and does not match the release. Ergo the release will not be considered this format. diff --git a/src/UI/Settings/CustomFormats/CustomFormatsLayoutTemplate.hbs b/src/UI/Settings/CustomFormats/CustomFormatsLayoutTemplate.hbs index 558a530ed..bad24bffa 100644 --- a/src/UI/Settings/CustomFormats/CustomFormatsLayoutTemplate.hbs +++ b/src/UI/Settings/CustomFormats/CustomFormatsLayoutTemplate.hbs @@ -1,14 +1,23 @@ -
-
- × - You can use custom formats to service all your automation needs! Read the Wiki Page for more info. - If you don't have the need for full customization, you can find a lot of predefined examples here. - These should be able to cover most automation needs. +
+
+
+ × + You can use custom formats to service all your automation needs! Read the Wiki Page for more info. + If you don't have the need for full customization, you can find a lot of predefined examples here. + These should be able to cover most automation needs. +
-
-
+
-
+
+
+
+
+
+ Custom Formats are very advanced. Please make sure you understand them fully before proceeding! +
+ + diff --git a/src/UI/Settings/CustomFormats/DeleteCustomFormatView.js b/src/UI/Settings/CustomFormats/DeleteCustomFormatView.js new file mode 100644 index 000000000..f9894ebd7 --- /dev/null +++ b/src/UI/Settings/CustomFormats/DeleteCustomFormatView.js @@ -0,0 +1,28 @@ +var vent = require('vent'); +var Marionette = require('marionette'); + +module.exports = Marionette.ItemView.extend({ + template : 'Settings/CustomFormats/DeleteCustomFormatView', + + ui: { + indicator : '.x-indicator', + delete : '.x-confirm-delete', + cancel : '.x-cancel-confirm' + }, + + events : { + 'click .x-confirm-delete' : '_removeProfile' + }, + + _removeProfile : function() { + this.ui.indicator.show(); + this.ui.delete.attr("disabled", "disabled"); + this.ui.cancel.attr("disabled", "disabled"); + + var self = this; + this.model.destroy({ wait : true }).done(function() { + self.ui.indicator.hide(); + vent.trigger(vent.Commands.CloseModalCommand); + }); + } +}); diff --git a/src/UI/Settings/CustomFormats/DeleteCustomFormatViewTemplate.hbs b/src/UI/Settings/CustomFormats/DeleteCustomFormatViewTemplate.hbs new file mode 100644 index 000000000..e59e17f0f --- /dev/null +++ b/src/UI/Settings/CustomFormats/DeleteCustomFormatViewTemplate.hbs @@ -0,0 +1,21 @@ + diff --git a/src/UI/Settings/CustomFormats/Edit/CustomFormatEditView.js b/src/UI/Settings/CustomFormats/Edit/CustomFormatEditView.js index 6efde413b..70acee683 100644 --- a/src/UI/Settings/CustomFormats/Edit/CustomFormatEditView.js +++ b/src/UI/Settings/CustomFormats/Edit/CustomFormatEditView.js @@ -2,7 +2,7 @@ var $ = require('jquery'); var vent = require('vent'); var Marionette = require('marionette'); -//var DeleteView = require('../Delete/IndexerDeleteView'); +var DeleteView = require('../DeleteCustomFormatView'); var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); var AsValidatedView = require('../../../Mixins/AsValidatedView'); var AsEditModalView = require('../../../Mixins/AsEditModalView'); @@ -29,7 +29,7 @@ var view = Marionette.Layout.extend({ testArea : '#x-test-region' }, - //_deleteView : DeleteView, + _deleteView : DeleteView, initialize : function(options) { this.targetCollection = options.targetCollection; diff --git a/src/UI/Settings/CustomFormats/Edit/CustomFormatEditViewTemplate.hbs b/src/UI/Settings/CustomFormats/Edit/CustomFormatEditViewTemplate.hbs index 533930cef..634317a01 100644 --- a/src/UI/Settings/CustomFormats/Edit/CustomFormatEditViewTemplate.hbs +++ b/src/UI/Settings/CustomFormats/Edit/CustomFormatEditViewTemplate.hbs @@ -31,7 +31,7 @@
-
+
diff --git a/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs b/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs index 6b8eea0b5..fb980581e 100644 --- a/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs +++ b/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs @@ -15,7 +15,7 @@ @@ -58,7 +58,7 @@
-
+
diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionItemViewTemplate.hbs b/src/UI/Settings/Quality/Definition/QualityDefinitionItemViewTemplate.hbs index 4eb2cf168..51c1ae472 100644 --- a/src/UI/Settings/Quality/Definition/QualityDefinitionItemViewTemplate.hbs +++ b/src/UI/Settings/Quality/Definition/QualityDefinitionItemViewTemplate.hbs @@ -1,10 +1,8 @@
- {{#if parentQualityDefinition }} - Custom Format - {{ else }} - {{quality.name}} - {{/if}} + + {{quality.name}} + @@ -35,28 +33,8 @@
- {{#if parentQualityDefinition }} - - Parent: - - - - - {{else}} - - - - - {{/if}}
-{{#if parentQualityDefinition}} - -{{/if}} diff --git a/src/UI/Settings/settings.less b/src/UI/Settings/settings.less index ff5855395..ecb51de5b 100644 --- a/src/UI/Settings/settings.less +++ b/src/UI/Settings/settings.less @@ -154,3 +154,8 @@ li.save-and-add:hover { } + +.x-custom-formats-tab { + color: @brand-warning !important; +} +