[WIP] Additions to custom formats, such as rescanning old files. (#2949)

This commit is contained in:
Leonardo Galli 2018-09-10 21:25:10 +02:00 committed by GitHub
parent 1059e145c3
commit b4f456d5f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 1322 additions and 3766 deletions

25
.gitignore vendored
View File

@ -153,4 +153,27 @@ Thumbs.db
# Cake # Cake
/tools/Addins/* /tools/Addins/*
packages.config.md5sum 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

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ContentModelUserStore"> <component name="ContentModelUserStore">
<attachedFolders />
<explicitIncludes /> <explicitIncludes />
<explicitExcludes /> <explicitExcludes />
</component> </component>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/.idea.NzbDrone/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.NzbDrone/riderModule.iml" />
</modules>
</component>
</project>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="RIDER_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$/../../../Logo/1024.png" />
<content url="file://$MODULE_DIR$/../../../Logo/64.png" />
<content url="file://$MODULE_DIR$/../.." />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -47,7 +47,7 @@ namespace NzbDrone.Api.MovieFiles
private void SetQuality(MovieFileResource movieFileResource) private void SetQuality(MovieFileResource movieFileResource)
{ {
var movieFile = _mediaFileService.GetMovie(movieFileResource.Id); var movieFile = _mediaFileService.GetMovie(movieFileResource.Id);
movieFile.Quality = movieFileResource.Quality; movieFile.Quality = movieFileResource.Quality;
_mediaFileService.Update(movieFile); _mediaFileService.Update(movieFile);

View File

@ -3,8 +3,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using NzbDrone.Api.REST; using NzbDrone.Api.REST;
using NzbDrone.Api.Movies; using NzbDrone.Api.Movies;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
@ -14,7 +12,7 @@ namespace NzbDrone.Api.MovieFiles
{ {
public MovieFileResource() public MovieFileResource()
{ {
} }
//Todo: Sorters should be done completely on the client //Todo: Sorters should be done completely on the client
@ -75,7 +73,7 @@ namespace NzbDrone.Api.MovieFiles
return new MovieFile return new MovieFile
{ {
}; };
} }

View File

@ -147,6 +147,7 @@
<Compile Include="Qualities\CustomFormatModule.cs" /> <Compile Include="Qualities\CustomFormatModule.cs" />
<Compile Include="Qualities\CustomFormatResource.cs" /> <Compile Include="Qualities\CustomFormatResource.cs" />
<Compile Include="Qualities\FormatTagMatchResultResource.cs" /> <Compile Include="Qualities\FormatTagMatchResultResource.cs" />
<Compile Include="Qualities\FormatTagValidator.cs" />
<Compile Include="Queue\QueueActionModule.cs" /> <Compile Include="Queue\QueueActionModule.cs" />
<Compile Include="RemotePathMappings\RemotePathMappingModule.cs" /> <Compile Include="RemotePathMappings\RemotePathMappingModule.cs" />
<Compile Include="RemotePathMappings\RemotePathMappingResource.cs" /> <Compile Include="RemotePathMappings\RemotePathMappingResource.cs" />

View File

@ -1,3 +1,4 @@
using System.Collections.Generic;
using NzbDrone.Api.Movies; using NzbDrone.Api.Movies;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
@ -17,7 +18,8 @@ namespace NzbDrone.Api.Parse
private ParseResource Parse() private ParseResource Parse()
{ {
var title = Request.Query.Title.Value as string; var title = Request.Query.Title.Value as string;
var parsedMovieInfo = Parser.ParseMovieTitle(title, false); var parsedMovieInfo = _parsingService.ParseMovieInfo(title, new List<object>());
if (parsedMovieInfo == null) if (parsedMovieInfo == null)
{ {

View File

@ -3,6 +3,7 @@ using System.Linq;
using FluentValidation; using FluentValidation;
using Nancy; using Nancy;
using NzbDrone.Api.Extensions; using NzbDrone.Api.Extensions;
using NzbDrone.Api.Validation;
using NzbDrone.Core.CustomFormats; using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
@ -21,7 +22,7 @@ namespace NzbDrone.Api.Qualities
SharedValidator.RuleFor(c => c.Name).NotEmpty(); SharedValidator.RuleFor(c => c.Name).NotEmpty();
SharedValidator.RuleFor(c => c.Name) SharedValidator.RuleFor(c => c.Name)
.Must((v, c) => !_formatService.All().Any(f => f.Name == c && f.Id != v.Id)).WithMessage("Must be unique."); .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) => SharedValidator.RuleFor(c => c.FormatTags).Must((v, c) =>
{ {
var allFormats = _formatService.All(); var allFormats = _formatService.All();
@ -44,6 +45,8 @@ namespace NzbDrone.Api.Qualities
CreateResource = Create; CreateResource = Create;
DeleteResource = Delete;
Get["/test"] = x => Test(); Get["/test"] = x => Test();
Post["/test"] = x => TestWithNewModel(); Post["/test"] = x => TestWithNewModel();
@ -73,6 +76,11 @@ namespace NzbDrone.Api.Qualities
return _formatService.All().ToResource(); return _formatService.All().ToResource();
} }
private void Delete(int id)
{
_formatService.Delete(id);
}
private Response GetTemplates() private Response GetTemplates()
{ {
return CustomFormatService.Templates.SelectMany(t => return CustomFormatService.Templates.SelectMany(t =>
@ -107,8 +115,9 @@ namespace NzbDrone.Api.Qualities
var resource = ReadResourceFromRequest(); var resource = ReadResourceFromRequest();
var model = resource.ToModel(); var model = resource.ToModel();
model.Name = model.Name += " (New)";
var parsed = _parsingService.ParseMovieInfo((string) Request.Query.title, new List<object>{model}); var parsed = _parsingService.ParseMovieInfo(queryTitle, new List<object>{model});
if (parsed == null) if (parsed == null)
{ {
return null; return null;

View File

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

View File

@ -2,6 +2,7 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using FluentValidation; using FluentValidation;
using FluentValidation.Validators; using FluentValidation.Validators;
using NzbDrone.Api.Qualities;
namespace NzbDrone.Api.Validation namespace NzbDrone.Api.Validation
{ {
@ -41,5 +42,11 @@ namespace NzbDrone.Api.Validation
{ {
return ruleBuilder.SetValidator(new NetImportSyncIntervalValidator()); return ruleBuilder.SetValidator(new NetImportSyncIntervalValidator());
} }
public static IRuleBuilderOptions<T, IEnumerable<TProp>> AreValidFormatTags<T, TProp>(
this IRuleBuilder<T, IEnumerable<TProp>> ruleBuilder)
{
return ruleBuilder.SetValidator(new FormatTagValidator());
}
} }
} }

View File

@ -1,4 +1,5 @@
using System.Text.RegularExpressions; using System;
using System.Text.RegularExpressions;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.CustomFormats; using NzbDrone.Core.CustomFormats;
@ -24,13 +25,16 @@ namespace NzbDrone.Core.Test.CustomFormat
[TestCase("L_English", TagType.Language, Language.English)] [TestCase("L_English", TagType.Language, Language.English)]
[TestCase("L_germaN", TagType.Language, Language.German)] [TestCase("L_germaN", TagType.Language, Language.German)]
[TestCase("E_Director", TagType.Edition, "director")] [TestCase("E_Director", TagType.Edition, "director")]
[TestCase("E_R_Director('?s)?", TagType.Edition, "director('?s)?", TagModifier.Regex)] [TestCase("E_RX_Director('?s)?", TagType.Edition, "director('?s)?", TagModifier.Regex)]
[TestCase("E_RN_Director('?s)?", TagType.Edition, "director('?s)?", TagModifier.Regex, TagModifier.Not)] [TestCase("E_RXN_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_RXNRQ_Director('?s)?", TagType.Edition, "director('?s)?", TagModifier.Regex, TagModifier.Not, TagModifier.AbsolutelyRequired)]
[TestCase("C_Surround", TagType.Custom, "surround")] [TestCase("C_Surround", TagType.Custom, "surround")]
[TestCase("C_RE_Surround", TagType.Custom, "surround", TagModifier.AbsolutelyRequired)] [TestCase("C_RQ_Surround", TagType.Custom, "surround", TagModifier.AbsolutelyRequired)]
[TestCase("C_REN_Surround", TagType.Custom, "surround", TagModifier.AbsolutelyRequired, TagModifier.Not)] [TestCase("C_RQN_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_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) public void should_parse_tag_from_string(string raw, TagType type, object value, params TagModifier[] modifiers)
{ {
var parsed = new FormatTag(raw); var parsed = new FormatTag(raw);
@ -40,6 +44,10 @@ namespace NzbDrone.Core.Test.CustomFormat
modifier |= m; modifier |= m;
} }
parsed.TagType.Should().Be(type); parsed.TagType.Should().Be(type);
if (value is long[])
{
value = (((long[]) value)[0], ((long[]) value)[1]);
}
if ((parsed.Value as Regex) != null) if ((parsed.Value as Regex) != null)
{ {
(parsed.Value as Regex).ToString().Should().Be((value as string)); (parsed.Value as Regex).ToString().Should().Be((value as string));

View File

@ -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<convert_regex_required_tags>
{
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<CustomFormatTest149> QueryItems(IDirectDataMapper db)
{
var items = db.Query<CustomFormatTest149>("SELECT Name, FormatTags FROM CustomFormats");
return items.Select(i =>
{
i.DeserializedTags = JsonConvert.DeserializeObject<List<string>>(i.FormatTags);
return i;
}).ToList();
}
public class CustomFormatTest149
{
public string Name { get; set; }
public string FormatTags { get; set; }
public List<string> DeserializedTags { get; set; }
}
}
}

View File

@ -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<CustomFormatAllowedbyProfileSpecification>
{
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<Movie>.CreateNew()
.With(c => c.Profile = (LazyLoaded<Profile>)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<CustomFormats.CustomFormat> {_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<CustomFormats.CustomFormat> {_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<CustomFormats.CustomFormat> {_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<CustomFormats.CustomFormat> {_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<CustomFormats.CustomFormat> {};
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<CustomFormats.CustomFormat> {};
remoteMovie.Movie.Profile.Value.FormatItems = CustomFormatsFixture.GetSampleFormatItems(CustomFormats.CustomFormat.None.Name, _format1.Name, _format2.Name);
Subject.IsSatisfiedBy(remoteMovie, null).Accepted.Should().BeTrue();
}
}
}

View File

@ -15,12 +15,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
public class LanguageSpecificationFixture : CoreTest public class LanguageSpecificationFixture : CoreTest
{ {
private RemoteMovie _remoteEpisode; private RemoteMovie _remoteMovie;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
_remoteEpisode = new RemoteMovie _remoteMovie = new RemoteMovie
{ {
ParsedMovieInfo = new ParsedMovieInfo ParsedMovieInfo = new ParsedMovieInfo
{ {
@ -38,12 +38,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
private void WithEnglishRelease() private void WithEnglishRelease()
{ {
_remoteEpisode.ParsedMovieInfo.Languages = new List<Language> {Language.English}; _remoteMovie.ParsedMovieInfo.Languages = new List<Language> {Language.English};
} }
private void WithGermanRelease() private void WithGermanRelease()
{ {
_remoteEpisode.ParsedMovieInfo.Languages = new List<Language> {Language.German}; _remoteMovie.ParsedMovieInfo.Languages = new List<Language> {Language.German};
} }
[Test] [Test]
@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{ {
WithEnglishRelease(); WithEnglishRelease();
Mocker.Resolve<LanguageSpecification>().IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); Mocker.Resolve<LanguageSpecification>().IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
} }
[Test] [Test]
@ -59,7 +59,24 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{ {
WithGermanRelease(); WithGermanRelease();
Mocker.Resolve<LanguageSpecification>().IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse(); Mocker.Resolve<LanguageSpecification>().IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
}
[Test]
public void should_return_true_if_allowed_language_any()
{
_remoteMovie.Movie.Profile = new LazyLoaded<Profile>(new Profile
{
Language = Language.Any
});
WithGermanRelease();
Mocker.Resolve<LanguageSpecification>().IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
WithEnglishRelease();
Mocker.Resolve<LanguageSpecification>().IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
} }
} }
} }

View File

@ -3,10 +3,9 @@ using FluentAssertions;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine.Specifications.Search; using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.TorrentRss; using NzbDrone.Core.Indexers.TorrentRss;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Movies; using NzbDrone.Core.Movies;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;

View File

@ -18,14 +18,15 @@ using NzbDrone.Core.Download;
namespace NzbDrone.Core.Test.MediaFiles.MovieImport namespace NzbDrone.Core.Test.MediaFiles.MovieImport
{ {
/* [TestFixture] [TestFixture]
//TODO: Update all of this for movies. //TODO: Add tests to ensure helpers for augmenters are correctly passed.
public class ImportDecisionMakerFixture : CoreTest<ImportDecisionMaker> public class ImportDecisionMakerFixture : CoreTest<ImportDecisionMaker>
{ {
private List<string> _videoFiles; private List<string> _videoFiles;
private LocalMovie _localEpisode; private LocalMovie _localMovie;
private Movie _series; private Movie _movie;
private QualityModel _quality; private QualityModel _quality;
private ParsedMovieInfo _fileInfo;
private Mock<IImportDecisionEngineSpecification> _pass1; private Mock<IImportDecisionEngineSpecification> _pass1;
private Mock<IImportDecisionEngineSpecification> _pass2; private Mock<IImportDecisionEngineSpecification> _pass2;
@ -54,24 +55,35 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
_fail2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_fail2")); _fail2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_fail2"));
_fail3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_fail3")); _fail3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalMovie>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_fail3"));
_series = Builder<Movie>.CreateNew() _movie = Builder<Movie>.CreateNew()
.With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }) .With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() })
.Build(); .Build();
_quality = new QualityModel(Quality.DVD); _quality = new QualityModel(Quality.DVD);
_localEpisode = new LocalMovie _localMovie = new LocalMovie
{ {
Movie = _series, Movie = _movie,
Quality = _quality, 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<IParsingService>() Mocker.GetMock<IParsingService>()
.Setup(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<Movie>(), It.IsAny<ParsedMovieInfo>(), It.IsAny<bool>())) .Setup(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<ParsedMovieInfo>(), It.IsAny<Movie>(), It.IsAny<List<object>>(), It.IsAny<bool>()))
.Returns(_localEpisode); .Returns(_localMovie);
GivenVideoFiles(new List<string> { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi".AsOsAgnostic() }); Mocker.GetMock<IParsingService>()
.Setup(c => c.ParseMinimalPathMovieInfo(It.IsAny<string>()))
.Returns(_fileInfo);
GivenVideoFiles(new List<string> { @"C:\Test\Unsorted\The.Office.2018.DVDRip.XviD-OSiTV.avi".AsOsAgnostic() });
} }
private void GivenSpecifications(params Mock<IImportDecisionEngineSpecification>[] mocks) private void GivenSpecifications(params Mock<IImportDecisionEngineSpecification>[] mocks)
@ -96,12 +108,12 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
Subject.GetImportDecisions(_videoFiles, new Movie(), downloadClientItem, null, false); Subject.GetImportDecisions(_videoFiles, new Movie(), downloadClientItem, null, false);
_fail1.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once()); _fail1.Verify(c => c.IsSatisfiedBy(_localMovie, downloadClientItem), Times.Once());
_fail2.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once()); _fail2.Verify(c => c.IsSatisfiedBy(_localMovie, downloadClientItem), Times.Once());
_fail3.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once()); _fail3.Verify(c => c.IsSatisfiedBy(_localMovie, downloadClientItem), Times.Once());
_pass1.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once()); _pass1.Verify(c => c.IsSatisfiedBy(_localMovie, downloadClientItem), Times.Once());
_pass2.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once()); _pass2.Verify(c => c.IsSatisfiedBy(_localMovie, downloadClientItem), Times.Once());
_pass3.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once()); _pass3.Verify(c => c.IsSatisfiedBy(_localMovie, downloadClientItem), Times.Once());
} }
[Test] [Test]
@ -149,7 +161,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
GivenSpecifications(_pass1); GivenSpecifications(_pass1);
Mocker.GetMock<IParsingService>() Mocker.GetMock<IParsingService>()
.Setup(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<Movie>(), It.IsAny<ParsedMovieInfo>(), It.IsAny<bool>())) .Setup(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<ParsedMovieInfo>(), It.IsAny<Movie>(), It.IsAny<List<object>>(), It.IsAny<bool>()))
.Throws<TestException>(); .Throws<TestException>();
_videoFiles = new List<string> _videoFiles = new List<string>
@ -161,34 +173,53 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
GivenVideoFiles(_videoFiles); GivenVideoFiles(_videoFiles);
Subject.GetImportDecisions(_videoFiles, _series); Subject.GetImportDecisions(_videoFiles, _movie);
Mocker.GetMock<IParsingService>() Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<Movie>(), It.IsAny<ParsedMovieInfo>(), It.IsAny<bool>()), Times.Exactly(_videoFiles.Count)); .Verify(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<ParsedMovieInfo>(), It.IsAny<Movie>(), It.IsAny<List<object>>(), It.IsAny<bool>()), Times.Exactly(_videoFiles.Count));
ExceptionVerification.ExpectedErrors(3); ExceptionVerification.ExpectedErrors(3);
} }
[Test]
public void should_call_parsing_service_with_filename_as_simpletitle()
{
GivenSpecifications(_pass1, _pass2, _pass3);
Mocker.GetMock<IParsingService>()
.Setup(c => c.ParseMinimalPathMovieInfo(It.IsAny<string>()))
.Returns<ParsedMovieInfo>(null);
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<IParsingService>()
.Verify(
c => c.GetLocalMovie(It.IsAny<string>(),
It.Is<ParsedMovieInfo>(p => fileNames.Contains(p.SimpleReleaseTitle)), It.IsAny<Movie>(),
It.IsAny<List<object>>(), It.IsAny<bool>()), Times.Exactly(_videoFiles.Count));
}
[Test] [Test]
public void should_use_file_quality_if_folder_quality_is_null() public void should_use_file_quality_if_folder_quality_is_null()
{ {
GivenSpecifications(_pass1, _pass2, _pass3); GivenSpecifications(_pass1, _pass2, _pass3);
var expectedQuality = QualityParser.ParseQuality(_videoFiles.Single()); var result = Subject.GetImportDecisions(_videoFiles, _movie);
var result = Subject.GetImportDecisions(_videoFiles, _series); result.Single().LocalMovie.Quality.Should().Be(_fileInfo.Quality);
result.Single().LocalMovie.Quality.Should().Be(expectedQuality);
} }
[Test] [Test]
public void should_use_file_quality_if_file_quality_was_determined_by_name() public void should_use_file_quality_if_file_quality_was_determined_by_name()
{ {
GivenSpecifications(_pass1, _pass2, _pass3); 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] [Test]
@ -197,13 +228,13 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
GivenSpecifications(_pass1, _pass2, _pass3); GivenSpecifications(_pass1, _pass2, _pass3);
GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.mkv".AsOsAgnostic() }); GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.mkv".AsOsAgnostic() });
_localEpisode.Path = _videoFiles.Single(); _localMovie.Path = _videoFiles.Single();
_localEpisode.Quality.QualitySource = QualitySource.Extension; _localMovie.Quality.QualitySource = QualitySource.Extension;
_localEpisode.Quality.Quality = Quality.HDTV720p; _localMovie.Quality.Quality = Quality.HDTV720p;
var expectedQuality = new QualityModel(Quality.SDTV); 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); result.Single().LocalMovie.Quality.Should().Be(expectedQuality);
} }
@ -214,168 +245,22 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
GivenSpecifications(_pass1, _pass2, _pass3); GivenSpecifications(_pass1, _pass2, _pass3);
GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.mkv".AsOsAgnostic() }); GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.mkv".AsOsAgnostic() });
_localEpisode.Path = _videoFiles.Single(); _localMovie.Path = _videoFiles.Single();
_localEpisode.Quality.Quality = Quality.HDTV720p; _localMovie.Quality.Quality = Quality.HDTV720p;
var expectedQuality = new QualityModel(Quality.Bluray720p); 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); result.Single().LocalMovie.Quality.Should().Be(expectedQuality);
} }
[Test]
public void should_not_throw_if_episodes_are_not_found()
{
GivenSpecifications(_pass1);
Mocker.GetMock<IParsingService>()
.Setup(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<Movie>(), It.IsAny<ParsedMovieInfo>(), It.IsAny<bool>()))
.Returns(new LocalMovie() { Path = "test" });
_videoFiles = new List<string>
{
"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<IParsingService>()
.Verify(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<Movie>(), It.IsAny<ParsedMovieInfo>(), It.IsAny<bool>()), 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<IParsingService>()
.Verify(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<Movie>(), null, true), Times.Exactly(3));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<Movie>(), It.Is<ParsedMovieInfo>(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<IParsingService>()
.Verify(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<Movie>(), null, true), Times.Exactly(2));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<Movie>(), It.Is<ParsedMovieInfo>(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<IParsingService>()
.Verify(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<Movie>(), It.IsAny<ParsedMovieInfo>(), true), Times.Exactly(1));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<Movie>(), 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<IDetectSample>()
.Setup(s => s.IsSample(_series, It.IsAny<QualityModel>(), It.Is<string>(c => c.Contains("sample")), It.IsAny<long>(), It.IsAny<bool>()))
.Returns(true);
var folderInfo = Parser.Parser.ParseMovieTitle("Movie.Title.S01E01", false);
Subject.GetImportDecisions(_videoFiles, _series, null, folderInfo, true);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<Movie>(), It.IsAny<ParsedMovieInfo>(), true), Times.Exactly(2));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<Movie>(), 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<IParsingService>()
.Verify(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<Movie>(), null, true), Times.Exactly(1));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<Movie>(), It.Is<ParsedMovieInfo>(p => p != null), true), Times.Never());
}
[Test] [Test]
public void should_not_use_folder_quality_when_it_is_unknown() public void should_not_use_folder_quality_when_it_is_unknown()
{ {
GivenSpecifications(_pass1, _pass2, _pass3); GivenSpecifications(_pass1, _pass2, _pass3);
_series.Profile = new Profile _movie.Profile = new Profile
{ {
Items = Qualities.QualityFixture.GetDefaultQualities(Quality.DVD, Quality.Unknown) 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 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); 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() public void should_return_a_decision_when_exception_is_caught()
{ {
Mocker.GetMock<IParsingService>() Mocker.GetMock<IParsingService>()
.Setup(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<Movie>(), It.IsAny<ParsedMovieInfo>(), It.IsAny<bool>())) .Setup(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<ParsedMovieInfo>(), It.IsAny<Movie>(), It.IsAny<List<object>>(), It.IsAny<bool>()))
.Throws<TestException>(); .Throws<TestException>();
_videoFiles = new List<string> _videoFiles = new List<string>
@ -402,9 +287,9 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
GivenVideoFiles(_videoFiles); GivenVideoFiles(_videoFiles);
Subject.GetImportDecisions(_videoFiles, _series).Should().HaveCount(1); Subject.GetImportDecisions(_videoFiles, _movie).Should().HaveCount(1);
ExceptionVerification.ExpectedErrors(1); ExceptionVerification.ExpectedErrors(1);
} }
}*/ }
} }

View File

@ -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<UpdateMovieFileQualityService>
{
private MovieFile _movieFile;
private QualityModel _oldQuality;
private QualityModel _newQuality;
private ParsedMovieInfo _newInfo;
[SetUp]
public void Setup()
{
_movieFile = Builder<MovieFile>.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<CustomFormats.CustomFormat>{format};
_newInfo = new ParsedMovieInfo
{
Quality = _newQuality
};
Mocker.GetMock<IMediaFileService>().Setup(s => s.GetMovies(It.IsAny<IEnumerable<int>>()))
.Returns(new List<MovieFile>{_movieFile});
Mocker.GetMock<IHistoryService>().Setup(s => s.FindByMovieId(It.IsAny<int>()))
.Returns(new List<History.History>());
}
private void ExecuteCommand()
{
Subject.Execute(new UpdateMovieFileQualityCommand(new List<int>{0}));
}
[Test]
public void should_not_update_if_unable_to_parse()
{
ExecuteCommand();
Mocker.GetMock<IMediaFileService>().Verify(s => s.Update(It.IsAny<MovieFile>()), Times.Never());
}
[Test]
public void should_update_with_new_formats()
{
Mocker.GetMock<IParsingService>().Setup(s => s.ParseMovieInfo(It.IsAny<string>(), It.IsAny<List<object>>()))
.Returns(_newInfo);
ExecuteCommand();
Mocker.GetMock<IMediaFileService>().Verify(s => s.Update(It.Is<MovieFile>(f => f.Quality.CustomFormats == _newQuality.CustomFormats)), Times.Once());
}
[Test]
public void should_use_imported_history_title()
{
var imported = Builder<History.History>.CreateNew()
.With(h => h.EventType = HistoryEventType.DownloadFolderImported)
.With(h => h.SourceTitle = "My Movie 2018.mkv").Build();
Mocker.GetMock<IHistoryService>().Setup(s => s.FindByMovieId(It.IsAny<int>()))
.Returns(new List<History.History> {imported});
ExecuteCommand();
Mocker.GetMock<IParsingService>().Verify(s => s.ParseMovieInfo("My Movie 2018.mkv", It.IsAny<List<object>>()));
}
}
}

View File

@ -95,6 +95,9 @@
<Reference Include="System.Data" /> <Reference Include="System.Data" />
<Reference Include="System.Drawing" /> <Reference Include="System.Drawing" />
<Reference Include="System.Numerics" /> <Reference Include="System.Numerics" />
<Reference Include="System.ValueTuple, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51">
<HintPath>..\packages\System.ValueTuple.4.5.0\lib\portable-net40+sl4+win8+wp8\System.ValueTuple.dll</HintPath>
</Reference>
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
<Reference Include="Microsoft.CSharp" /> <Reference Include="Microsoft.CSharp" />
@ -136,12 +139,14 @@
<Compile Include="Datastore\Migration\075_force_lib_updateFixture.cs" /> <Compile Include="Datastore\Migration\075_force_lib_updateFixture.cs" />
<Compile Include="Datastore\Migration\090_update_kickass_urlFixture.cs" /> <Compile Include="Datastore\Migration\090_update_kickass_urlFixture.cs" />
<Compile Include="Datastore\Migration\147_custom_formatsFixture.cs" /> <Compile Include="Datastore\Migration\147_custom_formatsFixture.cs" />
<Compile Include="Datastore\Migration\149_regex_required_tagsFixture.cs" />
<Compile Include="Datastore\ObjectDatabaseFixture.cs" /> <Compile Include="Datastore\ObjectDatabaseFixture.cs" />
<Compile Include="Datastore\PagingSpecExtensionsTests\PagingOffsetFixture.cs" /> <Compile Include="Datastore\PagingSpecExtensionsTests\PagingOffsetFixture.cs" />
<Compile Include="Datastore\PagingSpecExtensionsTests\ToSortDirectionFixture.cs" /> <Compile Include="Datastore\PagingSpecExtensionsTests\ToSortDirectionFixture.cs" />
<Compile Include="Datastore\ReflectionStrategyFixture\Benchmarks.cs" /> <Compile Include="Datastore\ReflectionStrategyFixture\Benchmarks.cs" />
<Compile Include="Datastore\SqliteSchemaDumperTests\SqliteSchemaDumperFixture.cs" /> <Compile Include="Datastore\SqliteSchemaDumperTests\SqliteSchemaDumperFixture.cs" />
<Compile Include="DecisionEngineTests\AcceptableSizeSpecificationFixture.cs" /> <Compile Include="DecisionEngineTests\AcceptableSizeSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\CustomFormatAllowedByProfileSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\MaximumSizeSpecificationFixture.cs" /> <Compile Include="DecisionEngineTests\MaximumSizeSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\ProtocolSpecificationFixture.cs" /> <Compile Include="DecisionEngineTests\ProtocolSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\CutoffSpecificationFixture.cs" /> <Compile Include="DecisionEngineTests\CutoffSpecificationFixture.cs" />
@ -283,6 +288,7 @@
<Compile Include="MediaFiles\MovieImport\Specifications\UpgradeSpecificationFixture.cs" /> <Compile Include="MediaFiles\MovieImport\Specifications\UpgradeSpecificationFixture.cs" />
<Compile Include="MediaFiles\ImportApprovedEpisodesFixture.cs" /> <Compile Include="MediaFiles\ImportApprovedEpisodesFixture.cs" />
<Compile Include="MediaFiles\MediaFileRepositoryFixture.cs" /> <Compile Include="MediaFiles\MediaFileRepositoryFixture.cs" />
<Compile Include="MediaFiles\UpdateMovieFileQualityServiceFixture.cs" />
<Compile Include="Messaging\Commands\CommandQueueManagerFixture.cs" /> <Compile Include="Messaging\Commands\CommandQueueManagerFixture.cs" />
<Compile Include="MetadataSource\SkyHook\SkyHookProxySearchFixture.cs" /> <Compile Include="MetadataSource\SkyHook\SkyHookProxySearchFixture.cs" />
<Compile Include="MetadataSource\SearchMovieComparerFixture.cs" /> <Compile Include="MetadataSource\SearchMovieComparerFixture.cs" />
@ -297,6 +303,7 @@
<Compile Include="ParserTests\ParsingServiceTests\AugmentersTests\AugmentWithFileSizeFixture.cs" /> <Compile Include="ParserTests\ParsingServiceTests\AugmentersTests\AugmentWithFileSizeFixture.cs" />
<Compile Include="ParserTests\ParsingServiceTests\AugmentersTests\AugmentWithHistoryFixture.cs" /> <Compile Include="ParserTests\ParsingServiceTests\AugmentersTests\AugmentWithHistoryFixture.cs" />
<Compile Include="ParserTests\ParsingServiceTests\AugmentersTests\AugmentWithMediaInfoFixture.cs" /> <Compile Include="ParserTests\ParsingServiceTests\AugmentersTests\AugmentWithMediaInfoFixture.cs" />
<Compile Include="ParserTests\ParsingServiceTests\AugmentersTests\AugmentWithParsedMovieInfo.cs" />
<Compile Include="ParserTests\ParsingServiceTests\AugmentersTests\AugmentWithReleaseInfoFixture.cs" /> <Compile Include="ParserTests\ParsingServiceTests\AugmentersTests\AugmentWithReleaseInfoFixture.cs" />
<Compile Include="ParserTests\ParsingServiceTests\ParseQualityDefinitionFixture.cs" /> <Compile Include="ParserTests\ParsingServiceTests\ParseQualityDefinitionFixture.cs" />
<Compile Include="ParserTests\RomanNumeralTests\RomanNumeralConversionFixture.cs" /> <Compile Include="ParserTests\RomanNumeralTests\RomanNumeralConversionFixture.cs" />

View File

@ -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<AugmentWithParsedMovieInfo>
{
[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> {Language.French}
};
MovieInfo.Languages = new List<Language>{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<CustomFormats.CustomFormat> { format1 };
MovieInfo.Quality.CustomFormats = new List<CustomFormats.CustomFormat> { 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<CustomFormats.CustomFormat> { 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);
}
}
}

View File

@ -13,14 +13,14 @@ namespace NzbDrone.Core.Test.ParserTests
public class QualityParserFixture : CoreTest public class QualityParserFixture : CoreTest
{ {
/*
[SetUp] [SetUp]
public void 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 = public static object[] OtherSourceQualityParserCases =
{ {
@ -288,7 +288,7 @@ namespace NzbDrone.Core.Test.ParserTests
ParseAndVerifyQuality(title, Source.UNKNOWN, proper, Resolution.Unknown); ParseAndVerifyQuality(title, Source.UNKNOWN, proper, Resolution.Unknown);
} }
[Test, TestCaseSource("SelfQualityParserCases")] /*[Test, TestCaseSource("SelfQualityParserCases")]
public void parsing_our_own_quality_enum_name(QualityDefinition definition) public void parsing_our_own_quality_enum_name(QualityDefinition definition)
{ {
var fileName = string.Format("My series S01E01 [{0}]", definition.Title); 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 (resolution != null) result.Resolution.Should().Be(resolution);
if (modifier != null) result.Modifier.Should().Be(modifier); if (modifier != null) result.Modifier.Should().Be(modifier);
} }*/
[Test, TestCaseSource("OtherSourceQualityParserCases")] [Test, TestCaseSource("OtherSourceQualityParserCases")]
public void should_parse_quality_from_other_source(string qualityString, Source source, Resolution resolution, Modifier modifier = Modifier.NONE) 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; var version = proper ? 2 : 1;
result.Revision.Version.Should().Be(version); result.Revision.Version.Should().Be(version);
}*/ }
} }
} }

View File

@ -13,5 +13,6 @@
<package id="NLog" version="4.5.0-rc06" targetFramework="net40" /> <package id="NLog" version="4.5.0-rc06" targetFramework="net40" />
<package id="NUnit" version="3.5.0" targetFramework="net40" /> <package id="NUnit" version="3.5.0" targetFramework="net40" />
<package id="Prowlin" version="0.9.4456.26422" targetFramework="net40" /> <package id="Prowlin" version="0.9.4456.26422" targetFramework="net40" />
<package id="System.ValueTuple" version="4.5.0" targetFramework="net40" />
<package id="Unity" version="2.1.505.2" targetFramework="net40" /> <package id="Unity" version="2.1.505.2" targetFramework="net40" />
</packages> </packages>

View File

@ -49,4 +49,20 @@ namespace NzbDrone.Core.CustomFormats
return (Id != null ? Id.GetHashCode() : 0); return (Id != null ? Id.GetHashCode() : 0);
} }
} }
public static class CustomFormatExtensions
{
public static string ToExtendedString(this IEnumerable<CustomFormat> formats)
{
return string.Join(", ", formats.Select(f => f.ToString()));
}
public static List<CustomFormat> WithNone(this IEnumerable<CustomFormat> formats)
{
var list = formats.ToList();
if (list.Any()) return list;
return new List<CustomFormat>{CustomFormat.None};
}
}
} }

View File

@ -1,11 +1,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.Remoting.Messaging;
using NLog; using NLog;
using NzbDrone.Common.Cache; using NzbDrone.Common.Cache;
using NzbDrone.Common.Composition; 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.Lifecycle;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Profiles; using NzbDrone.Core.Profiles;
@ -17,6 +21,7 @@ namespace NzbDrone.Core.CustomFormats
CustomFormat Insert(CustomFormat customFormat); CustomFormat Insert(CustomFormat customFormat);
List<CustomFormat> All(); List<CustomFormat> All();
CustomFormat GetById(int id); CustomFormat GetById(int id);
void Delete(int id);
} }
@ -24,6 +29,10 @@ namespace NzbDrone.Core.CustomFormats
{ {
private readonly ICustomFormatRepository _formatRepository; private readonly ICustomFormatRepository _formatRepository;
private IProfileService _profileService; private IProfileService _profileService;
private readonly IMediaFileService _mediaFileService;
private readonly IBlacklistService _blacklistService;
private readonly IHistoryService _historyService;
private readonly IPendingReleaseService _pendingReleaseService;
public IProfileService ProfileService public IProfileService ProfileService
{ {
@ -45,12 +54,18 @@ namespace NzbDrone.Core.CustomFormats
public static Dictionary<int, CustomFormat> AllCustomFormats; public static Dictionary<int, CustomFormat> AllCustomFormats;
public CustomFormatService(ICustomFormatRepository formatRepository, ICacheManager cacheManager, public CustomFormatService(ICustomFormatRepository formatRepository, ICacheManager cacheManager,
IContainer container, IContainer container, IHistoryService historyService,/*IMediaFileService mediaFileService, IBlacklistService blacklistService,
IHistoryService historyService, IPendingReleaseService pendingReleaseService,*/
Logger logger) Logger logger)
{ {
_formatRepository = formatRepository; _formatRepository = formatRepository;
_container = container; _container = container;
_cache = cacheManager.GetCache<Dictionary<int, CustomFormat>>(typeof(CustomFormat), "formats"); _cache = cacheManager.GetCache<Dictionary<int, CustomFormat>>(typeof(CustomFormat), "formats");
/*_mediaFileService = mediaFileService;
_blacklistService = blacklistService;
_historyService = historyService;
_pendingReleaseService = pendingReleaseService;*/
_historyService = historyService;
_logger = logger; _logger = logger;
} }
@ -69,7 +84,7 @@ namespace NzbDrone.Core.CustomFormats
} }
catch (Exception e) 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); _formatRepository.Delete(ret);
throw; throw;
} }
@ -77,6 +92,76 @@ namespace NzbDrone.Core.CustomFormats
return ret; return ret;
} }
public void Delete(int id)
{
_cache.Clear();
try
{
//First history:
var historyRepo = _container.Resolve<IHistoryRepository>();
DeleteInRepo(historyRepo, h => h.Quality.CustomFormats, (h, f) =>
{
h.Quality.CustomFormats = f;
return h;
}, id);
//Then Blacklist:
var blacklistRepo = _container.Resolve<IBlacklistRepository>();
DeleteInRepo(blacklistRepo, h => h.Quality.CustomFormats, (h, f) =>
{
h.Quality.CustomFormats = f;
return h;
}, id);
//Then MovieFiles:
var moviefileRepo = _container.Resolve<IMediaFileRepository>();
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<TModel>(IBasicRepository<TModel> repository, Func<TModel, List<CustomFormat>> queryFunc,
Func<TModel, List<CustomFormat>, TModel> updateFunc, int customFormatId) where TModel : ModelBase, new()
{
var pagingSpec = new PagingSpec<TModel>
{
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<int, CustomFormat> AllDictionary() private Dictionary<int, CustomFormat> AllDictionary()
{ {
return _cache.Get("all", () => return _cache.Get("all", () =>

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
@ -14,7 +15,9 @@ namespace NzbDrone.Core.CustomFormats
public TagModifier TagModifier { get; set; } public TagModifier TagModifier { get; set; }
public object Value { get; set; } public object Value { get; set; }
public static Regex QualityTagRegex = new Regex(@"^(?<type>R|S|M|E|L|C|I)(_((?<m_r>R)|(?<m_re>RE)|(?<m_n>N)){1,3})?_(?<value>.*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); public static Regex QualityTagRegex = new Regex(@"^(?<type>R|S|M|E|L|C|I|G)(_((?<m_r>RX)|(?<m_re>RQ)|(?<m_n>N)){1,3})?_(?<value>.*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static Regex SizeTagRegex = new Regex(@"(?<min>\d+(\.\d+)?)\s*<>\s*(?<max>\d+(\.\d+)?)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public FormatTag(string raw) public FormatTag(string raw)
{ {
@ -29,6 +32,7 @@ namespace NzbDrone.Core.CustomFormats
ParseRawMatch(match); ParseRawMatch(match);
} }
// This function is needed for json deserialization to work.
private FormatTag() private FormatTag()
{ {
@ -74,6 +78,10 @@ namespace NzbDrone.Core.CustomFormats
return movieInfo.Quality.Modifier == (Modifier) Value; return movieInfo.Quality.Modifier == (Modifier) Value;
case TagType.Source: case TagType.Source:
return movieInfo.Quality.Source == (Source) Value; 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: case TagType.Indexer:
return (movieInfo.ExtraInfo.GetValueOrDefault("IndexerFlags") as IndexerFlags?)?.HasFlag((IndexerFlags) Value) == true; return (movieInfo.ExtraInfo.GetValueOrDefault("IndexerFlags") as IndexerFlags?)?.HasFlag((IndexerFlags) Value) == true;
default: default:
@ -191,6 +199,13 @@ namespace NzbDrone.Core.CustomFormats
break; 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; break;
case "c": case "c":
default: default:
@ -218,6 +233,7 @@ namespace NzbDrone.Core.CustomFormats
Language = 16, Language = 16,
Custom = 32, Custom = 32,
Indexer = 64, Indexer = 64,
Size = 128,
} }
[Flags] [Flags]

View File

@ -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(@"^(?<type>R|S|M|E|L|C|I|G)(_((?<m_r>R)|(?<m_re>RE)|(?<m_n>N)){1,3})?_(?<value>.*)$", 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<string> FormatTags { get; set; }
}
public class CustomFormatUpdater149
{
private readonly IDbConnection _connection;
private readonly IDbTransaction _transaction;
private List<CustomFormat149> _customFormats;
private HashSet<CustomFormat149> _changedFormats = new HashSet<CustomFormat149>();
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<CustomFormat149> GetFormats()
{
var profiles = new List<CustomFormat149>();
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<List<string>>(profileReader.GetString(2))
});
}
}
}
return profiles;
}
}
}

View File

@ -69,11 +69,7 @@ namespace NzbDrone.Core.DecisionEngine
private int CompareCustomFormats(DownloadDecision x, DownloadDecision y) private int CompareCustomFormats(DownloadDecision x, DownloadDecision y)
{ {
var left = x.RemoteMovie.ParsedMovieInfo.Quality.CustomFormats.ToArray().ToList(); var left = x.RemoteMovie.ParsedMovieInfo.Quality.CustomFormats.WithNone();
if (left.Count == 0)
{
left.Add(CustomFormat.None);
}
var right = y.RemoteMovie.ParsedMovieInfo.Quality.CustomFormats; var right = y.RemoteMovie.ParsedMovieInfo.Quality.CustomFormats;
var leftIndicies = QualityModelComparer.GetIndicies(left, x.RemoteMovie.Movie.Profile.Value); var leftIndicies = QualityModelComparer.GetIndicies(left, x.RemoteMovie.Movie.Profile.Value);

View File

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

View File

@ -20,6 +20,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
{ {
var wantedLanguage = subject.Movie.Profile.Value.Language; 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()); _logger.Debug("Checking if report meets language requirements. {0}", subject.ParsedMovieInfo.Languages.ToExtendedString());
if (!subject.ParsedMovieInfo.Languages.Contains(wantedLanguage)) if (!subject.ParsedMovieInfo.Languages.Contains(wantedLanguage))

View File

@ -1,13 +1,11 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.DecisionEngine.Specifications.Search namespace NzbDrone.Core.DecisionEngine.Specifications
{ {
public class RequiredIndexerFlagsSpecification : IDecisionEngineSpecification public class RequiredIndexerFlagsSpecification : IDecisionEngineSpecification
{ {

View File

@ -1,11 +1,10 @@
using System; using System;
using NLog; using NLog;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.DecisionEngine.Specifications.Search namespace NzbDrone.Core.DecisionEngine.Specifications
{ {
public class TorrentSeedingSpecification : IDecisionEngineSpecification public class TorrentSeedingSpecification : IDecisionEngineSpecification
{ {

View File

@ -15,6 +15,7 @@ namespace NzbDrone.Core.History
History MostRecentForDownloadId(string downloadId); History MostRecentForDownloadId(string downloadId);
List<History> FindByDownloadId(string downloadId); List<History> FindByDownloadId(string downloadId);
List<History> FindDownloadHistory(int idMovieId, QualityModel quality); List<History> FindDownloadHistory(int idMovieId, QualityModel quality);
List<History> FindByMovieId(int movieId);
void DeleteForMovie(int movieId); void DeleteForMovie(int movieId);
History MostRecentForMovie(int movieId); History MostRecentForMovie(int movieId);
} }
@ -57,6 +58,11 @@ namespace NzbDrone.Core.History
).ToList(); ).ToList();
} }
public List<History> FindByMovieId(int movieId)
{
return Query.Where(h => h.MovieId == movieId);
}
public void DeleteForMovie(int movieId) public void DeleteForMovie(int movieId)
{ {
Delete(c => c.MovieId == movieId); Delete(c => c.MovieId == movieId);

View File

@ -25,6 +25,8 @@ namespace NzbDrone.Core.History
History Get(int historyId); History Get(int historyId);
List<History> Find(string downloadId, HistoryEventType eventType); List<History> Find(string downloadId, HistoryEventType eventType);
List<History> FindByDownloadId(string downloadId); List<History> FindByDownloadId(string downloadId);
List<History> FindByMovieId(int movieId);
void UpdateMany(List<History> toUpdate);
} }
public class HistoryService : IHistoryService, public class HistoryService : IHistoryService,
@ -73,6 +75,11 @@ namespace NzbDrone.Core.History
return _historyRepository.FindByDownloadId(downloadId); return _historyRepository.FindByDownloadId(downloadId);
} }
public List<History> FindByMovieId(int movieId)
{
return _historyRepository.FindByMovieId(movieId);
}
public QualityModel GetBestQualityInHistory(Profile profile, int movieId) public QualityModel GetBestQualityInHistory(Profile profile, int movieId)
{ {
var comparer = new QualityModelComparer(profile); var comparer = new QualityModelComparer(profile);
@ -81,6 +88,11 @@ namespace NzbDrone.Core.History
.FirstOrDefault(); .FirstOrDefault();
} }
public void UpdateMany(List<History> toUpdate)
{
_historyRepository.UpdateMany(toUpdate);
}
public void Handle(MovieGrabbedEvent message) public void Handle(MovieGrabbedEvent message)
{ {
var history = new History var history = new History

View File

@ -0,0 +1,17 @@
using System.Collections.Generic;
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.MediaFiles.Commands
{
public class UpdateMovieFileQualityCommand : Command
{
public IEnumerable<int> MovieFileIds { get; set; }
public override bool SendUpdatesToClient => true;
public UpdateMovieFileQualityCommand(IEnumerable<int> movieFileIds)
{
MovieFileIds = movieFileIds;
}
}
}

View File

@ -112,32 +112,35 @@ namespace NzbDrone.Core.MediaFiles.MovieImport
try try
{ {
var minimalInfo = shouldUseFolderName ParsedMovieInfo modifiedFolderInfo = null;
? folderInfo.JsonClone() if (folderInfo != null)
: _parsingService.ParseMinimalPathMovieInfo(file); {
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; LocalMovie localMovie = null;
//var localMovie = _parsingService.GetLocalMovie(file, movie, shouldUseFolderName ? folderInfo : null, sceneSource);
if (minimalInfo != null) if (minimalInfo != null)
{ {
//TODO: make it so media info doesn't ruin the import process of a new movie //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 size = _diskProvider.GetFileSize(file);
var historyItems = _historyService.FindByDownloadId(downloadClientItem?.DownloadId ?? ""); 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(); var sizeMovie = new LocalMovie();
sizeMovie.Size = size; sizeMovie.Size = size;
localMovie = _parsingService.GetLocalMovie(file, minimalInfo, movie, new List<object>{mediaInfo, firstHistoryItem, sizeMovie}, sceneSource); localMovie = _parsingService.GetLocalMovie(file, minimalInfo, movie, new List<object>{mediaInfo, firstHistoryItem, sizeMovie, folderInfo}, sceneSource);
localMovie.Quality = GetQuality(folderInfo, localMovie.Quality, movie); localMovie.Quality = GetQuality(folderInfo, localMovie.Quality, movie);
localMovie.Size = size; localMovie.Size = size;
_logger.Debug("Size: {0}", localMovie.Size); _logger.Debug("Size: {0}", localMovie.Size);
decision = GetDecision(localMovie, downloadClientItem); decision = GetDecision(localMovie, downloadClientItem);
} }
else else
{ {
localMovie = new LocalMovie(); localMovie = new LocalMovie();
@ -201,38 +204,10 @@ namespace NzbDrone.Core.MediaFiles.MovieImport
return null; return null;
} }
//TODO: Remove this method, since it is no longer needed.
private bool ShouldUseFolderName(List<string> videoFiles, Movie movie, ParsedMovieInfo folderInfo) private bool ShouldUseFolderName(List<string> videoFiles, Movie movie, ParsedMovieInfo folderInfo)
{ {
if (folderInfo == null) return false;
{
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;
} }
private QualityModel GetQuality(ParsedMovieInfo folderInfo, QualityModel fileQuality, Movie movie) private QualityModel GetQuality(ParsedMovieInfo folderInfo, QualityModel fileQuality, Movie movie)

View File

@ -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<UpdateMovieFileQualityCommand>
{
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<object>{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++;
}
}
}
}

View File

@ -58,7 +58,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
public Movie GetMovieInfo(int TmdbId, Profile profile = null, bool hasPreDBEntry = false) 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() var request = _movieBuilder.Create()
.SetSegment("route", "movie") .SetSegment("route", "movie")
@ -140,7 +140,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
movie.ImdbId = resource.imdb_id; movie.ImdbId = resource.imdb_id;
movie.Title = resource.title; movie.Title = resource.title;
movie.TitleSlug = Parser.Parser.ToUrlSlug(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.SortTitle = Parser.Parser.NormalizeTitle(resource.title);
movie.Overview = resource.overview; movie.Overview = resource.overview;
movie.Website = resource.homepage; movie.Website = resource.homepage;

View File

@ -1,16 +1,8 @@
using System; using System.Collections.Generic;
using System.Linq; 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 Marr.Data.QGen;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Parser.RomanNumerals; namespace NzbDrone.Core.Movies
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Movies;
using CoreParser = NzbDrone.Core.Parser.Parser;
namespace NzbDrone.Core
{ {
public static class QueryExtensions public static class QueryExtensions
{ {

View File

@ -101,6 +101,9 @@
<Reference Include="System.Numerics" /> <Reference Include="System.Numerics" />
<Reference Include="System.ServiceModel" /> <Reference Include="System.ServiceModel" />
<Reference Include="System.Runtime.Serialization" /> <Reference Include="System.Runtime.Serialization" />
<Reference Include="System.ValueTuple, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51">
<HintPath>..\packages\System.ValueTuple.4.5.0\lib\portable-net40+sl4+win8+wp8\System.ValueTuple.dll</HintPath>
</Reference>
<Reference Include="System.Web" /> <Reference Include="System.Web" />
<Reference Include="System.Web.Extensions" /> <Reference Include="System.Web.Extensions" />
<Reference Include="System.Windows.Forms" /> <Reference Include="System.Windows.Forms" />
@ -141,11 +144,14 @@
<Compile Include="Datastore\Migration\141_fix_duplicate_alt_titles.cs" /> <Compile Include="Datastore\Migration\141_fix_duplicate_alt_titles.cs" />
<Compile Include="Datastore\Migration\145_banner_to_fanart.cs" /> <Compile Include="Datastore\Migration\145_banner_to_fanart.cs" />
<Compile Include="Datastore\Migration\144_add_cookies_to_indexer_status.cs" /> <Compile Include="Datastore\Migration\144_add_cookies_to_indexer_status.cs" />
<Compile Include="Datastore\Migration\149_convert_regex_required_tags.cs" />
<Compile Include="DecisionEngine\Specifications\CustomFormatAllowedByProfileSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\MaximumSizeSpecification.cs" /> <Compile Include="DecisionEngine\Specifications\MaximumSizeSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\RequiredIndexerFlagsSpecification.cs" /> <Compile Include="DecisionEngine\Specifications\RequiredIndexerFlagsSpecification.cs" />
<Compile Include="Extras\Metadata\Consumers\Xbmc\XbmcNfoDetector.cs" /> <Compile Include="Extras\Metadata\Consumers\Xbmc\XbmcNfoDetector.cs" />
<Compile Include="Extras\Others\OtherExtraFileRenamer.cs" /> <Compile Include="Extras\Others\OtherExtraFileRenamer.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedAlternativeTitles.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedAlternativeTitles.cs" />
<Compile Include="MediaFiles\Commands\UpdateMovieFileQualityCommand.cs" />
<Compile Include="MediaFiles\MovieImport\Specifications\GrabbedReleaseQualitySpecification.cs" /> <Compile Include="MediaFiles\MovieImport\Specifications\GrabbedReleaseQualitySpecification.cs" />
<Compile Include="MediaFiles\MovieImport\Specifications\SameFileSpecification.cs" /> <Compile Include="MediaFiles\MovieImport\Specifications\SameFileSpecification.cs" />
<Compile Include="MediaFiles\Events\MovieFileUpdatedEvent.cs" /> <Compile Include="MediaFiles\Events\MovieFileUpdatedEvent.cs" />
@ -157,6 +163,7 @@
<Compile Include="Datastore\Migration\133_add_minimumavailability.cs" /> <Compile Include="Datastore\Migration\133_add_minimumavailability.cs" />
<Compile Include="IndexerSearch\CutoffUnmetMoviesSearchCommand.cs" /> <Compile Include="IndexerSearch\CutoffUnmetMoviesSearchCommand.cs" />
<Compile Include="Indexers\HDBits\HDBitsInfo.cs" /> <Compile Include="Indexers\HDBits\HDBitsInfo.cs" />
<Compile Include="MediaFiles\UpdateMovieFileQualityService.cs" />
<Compile Include="Movies\AlternativeTitles\AlternativeTitle.cs" /> <Compile Include="Movies\AlternativeTitles\AlternativeTitle.cs" />
<Compile Include="Movies\AlternativeTitles\AlternativeTitleRepository.cs" /> <Compile Include="Movies\AlternativeTitles\AlternativeTitleRepository.cs" />
<Compile Include="Movies\AlternativeTitles\AlternativeTitleService.cs" /> <Compile Include="Movies\AlternativeTitles\AlternativeTitleService.cs" />
@ -973,6 +980,7 @@
<Compile Include="Parser\Augmenters\AugmentWithFileSize.cs" /> <Compile Include="Parser\Augmenters\AugmentWithFileSize.cs" />
<Compile Include="Parser\Augmenters\AugmentWithHistory.cs" /> <Compile Include="Parser\Augmenters\AugmentWithHistory.cs" />
<Compile Include="Parser\Augmenters\AugmentWithMediaInfo.cs" /> <Compile Include="Parser\Augmenters\AugmentWithMediaInfo.cs" />
<Compile Include="Parser\Augmenters\AugmentWithParsedMovieInfo.cs" />
<Compile Include="Parser\Augmenters\AugmentWithReleaseInfo.cs" /> <Compile Include="Parser\Augmenters\AugmentWithReleaseInfo.cs" />
<Compile Include="Parser\Augmenters\IAugmentParsedMovieInfo.cs" /> <Compile Include="Parser\Augmenters\IAugmentParsedMovieInfo.cs" />
<Compile Include="Parser\IsoLanguage.cs" /> <Compile Include="Parser\IsoLanguage.cs" />

View File

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

View File

@ -29,7 +29,8 @@ namespace NzbDrone.Core.Parser
new IsoLanguage("el", "ell", Language.Greek), new IsoLanguage("el", "ell", Language.Greek),
new IsoLanguage("ko", "kor", Language.Korean), new IsoLanguage("ko", "kor", Language.Korean),
new IsoLanguage("hu", "hun", Language.Hungarian), 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) public static IsoLanguage Find(string isoCode)

View File

@ -28,9 +28,10 @@ namespace NzbDrone.Core.Parser
Greek = 20, Greek = 20,
Korean = 21, Korean = 21,
Hungarian = 22, Hungarian = 22,
Hebrew = 23 Hebrew = 23,
Any = -1,
} }
public static class LanguageExtensions public static class LanguageExtensions
{ {
public static string ToExtendedString(this IEnumerable<Language> languages) public static string ToExtendedString(this IEnumerable<Language> languages)

View File

@ -16,6 +16,7 @@ namespace NzbDrone.Core.Profiles
Profile Add(Profile profile); Profile Add(Profile profile);
void Update(Profile profile); void Update(Profile profile);
void AddCustomFormat(CustomFormat format); void AddCustomFormat(CustomFormat format);
void DeleteCustomFormat(int formatId);
void Delete(int id); void Delete(int id);
List<Profile> All(); List<Profile> All();
Profile Get(int id); 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) public void Delete(int id)
{ {
if (_movieService.GetAllMovies().Any(c => c.ProfileId == id) || _netImportFactory.All().Any(c => c.ProfileId == id)) if (_movieService.GetAllMovies().Any(c => c.ProfileId == id) || _netImportFactory.All().Any(c => c.ProfileId == id))

View File

@ -42,8 +42,7 @@ namespace NzbDrone.Core.Qualities
public override string ToString() public override string ToString()
{ {
var formats = CustomFormats.Count > 0 ? CustomFormats : new List<CustomFormat> {CustomFormat.None}; return string.Format("{0} {1} ({2})", Quality, Revision, CustomFormats.WithNone().ToExtendedString());
return string.Format("{0} {1} ({2})", Quality, Revision, string.Join(", ", formats));
} }
public override int GetHashCode() public override int GetHashCode()

View File

@ -41,9 +41,7 @@ namespace NzbDrone.Core.Qualities
public static List<int> GetIndicies(List<CustomFormat> formats, Profile profile) public static List<int> GetIndicies(List<CustomFormat> formats, Profile profile)
{ {
return formats.Count > 0 return formats.WithNone().Select(f => profile.FormatItems.FindIndex(v => Equals(v.Format, f))).ToList();
? formats.Select(f => profile.FormatItems.FindIndex(v => Equals(v.Format, f))).ToList()
: new List<int> {profile.FormatItems.FindIndex(v => Equals(v.Format, CustomFormat.None))};
} }
public int Compare(CustomFormat left, CustomFormat right) public int Compare(CustomFormat left, CustomFormat right)
@ -56,10 +54,7 @@ namespace NzbDrone.Core.Qualities
public int Compare(List<CustomFormat> left, CustomFormat right) public int Compare(List<CustomFormat> left, CustomFormat right)
{ {
if (left.Count == 0) left = left.WithNone();
{
left.Add(CustomFormat.None);
}
var leftIndicies = GetIndicies(left, _profile); var leftIndicies = GetIndicies(left, _profile);
var rightIndex = _profile.FormatItems.FindIndex(v => Equals(v.Format, right)); var rightIndex = _profile.FormatItems.FindIndex(v => Equals(v.Format, right));

View File

@ -9,6 +9,7 @@
<package id="OAuth" version="1.0.3" targetFramework="net40" /> <package id="OAuth" version="1.0.3" targetFramework="net40" />
<package id="Prowlin" version="0.9.4456.26422" targetFramework="net40" /> <package id="Prowlin" version="0.9.4456.26422" targetFramework="net40" />
<package id="RestSharp" version="105.2.3" targetFramework="net40" /> <package id="RestSharp" version="105.2.3" targetFramework="net40" />
<package id="System.ValueTuple" version="4.5.0" targetFramework="net40" />
<package id="TinyTwitter" version="1.1.1" targetFramework="net40" /> <package id="TinyTwitter" version="1.1.1" targetFramework="net40" />
<package id="xmlrpcnet" version="2.5.0" targetFramework="net40" /> <package id="xmlrpcnet" version="2.5.0" targetFramework="net40" />
</packages> </packages>

View File

@ -31,8 +31,11 @@ module.exports = Marionette.Layout.extend({
var qualities = _.map(this.profileSchemaCollection.first().get('items'), function (quality) { var qualities = _.map(this.profileSchemaCollection.first().get('items'), function (quality) {
return quality.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); this.quality.show(this.selectQualityView);
}, },
@ -40,4 +43,4 @@ module.exports = Marionette.Layout.extend({
this.trigger('manualimport:selected:quality', { quality: this.selectQualityView.selectedQuality() }); this.trigger('manualimport:selected:quality', { quality: this.selectQualityView.selectedQuality() });
vent.trigger(vent.Commands.CloseModal2Command); vent.trigger(vent.Commands.CloseModal2Command);
} }
}); });

View File

@ -1,22 +1,40 @@
var _ = require('underscore'); var _ = require('underscore');
var Marionette = require('marionette'); var Marionette = require('marionette');
var Backbone = require('backbone');
require('../../Mixins/TagInput');
module.exports = Marionette.ItemView.extend({ module.exports = Marionette.ItemView.extend({
template : 'ManualImport/Quality/SelectQualityViewTemplate', template : 'ManualImport/Quality/SelectQualityViewTemplate',
ui : { ui : {
select : '.x-select-quality', select : '.x-select-quality',
proper : 'x-proper' proper : 'x-proper',
formats: '.x-tags',
}, },
initialize : function(options) { initialize : function(options) {
this.qualities = options.qualities; this.qualities = options.qualities;
this.formats = options.formats;
this.current = options.current || {};
this.templateHelpers = { 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 () { selectedQuality : function () {
var selected = parseInt(this.ui.select.val(), 10); var selected = parseInt(this.ui.select.val(), 10);
var proper = this.ui.proper.prop('checked'); var proper = this.ui.proper.prop('checked');
@ -25,13 +43,21 @@ module.exports = Marionette.ItemView.extend({
return q.id === selected; 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 { return {
quality : quality, quality : quality,
revision : { revision : {
version : proper ? 2 : 1, version : proper ? 2 : 1,
real : 0 real : 0
} },
customFormats : formats
}; };
} }
}); });

View File

@ -30,4 +30,12 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-group advanced-setting">
<label class="col-sm-4 control-label">Custom Formats</label>
<div class="col-sm-4">
<input type="text" class="form-control x-tags" tag-source="{{formats}}" tag-class-name="label label-success">
</div>
</div>
</div> </div>

View File

@ -113,7 +113,7 @@ $.fn.tagInput = function(options) {
var input = $(this); var input = $(this);
var tagInput = null; var tagInput = null;
if (input[0].hasAttribute('tag-source')) { if (input[0].hasAttribute('tag-source')) {
var listItems = JSON.parse(input.attr('tag-source')); var listItems = JSON.parse(input.attr('tag-source'));
@ -124,6 +124,7 @@ $.fn.tagInput = function(options) {
allowDuplicates: false, allowDuplicates: false,
itemValue: 'value', itemValue: 'value',
itemText: 'name', itemText: 'name',
tagClass : input.attr('tag-class-name') || "label label-info",
typeaheadjs: { typeaheadjs: {
displayKey: 'name', displayKey: 'name',
source: substringMatcher(listItems, function (t) { return t.name; }) source: substringMatcher(listItems, function (t) { return t.name; })
@ -195,4 +196,4 @@ $.fn.tagInput = function(options) {
}); });
}; };

View File

@ -25,7 +25,8 @@ module.exports = Marionette.ItemView.extend({
events : { events : {
'click .x-save' : '_updateAndSave', 'click .x-save' : '_updateAndSave',
'change .x-root-folder' : '_rootFolderChanged', 'change .x-root-folder' : '_rootFolderChanged',
'click .x-organize-files' : '_organizeFiles' 'click .x-organize-files' : '_organizeFiles',
'click .x-update-quality' : '_updateQuality'
}, },
templateHelpers : function() { templateHelpers : function() {

View File

@ -16,6 +16,7 @@ var GridPager = require('../../Shared/Grid/Pager');
require('../../Mixins/backbone.signalr.mixin'); require('../../Mixins/backbone.signalr.mixin');
var DeleteSelectedView = require('./Delete/DeleteSelectedView'); var DeleteSelectedView = require('./Delete/DeleteSelectedView');
var Config = require('../../Config'); var Config = require('../../Config');
var CommandController = require('../../Commands/CommandController');
window.shownOnce = false; window.shownOnce = false;
module.exports = Marionette.Layout.extend({ module.exports = Marionette.Layout.extend({
@ -116,6 +117,12 @@ module.exports = Marionette.Layout.extend({
successMessage : 'Library was updated!', successMessage : 'Library was updated!',
errorMessage : 'Library update failed!' errorMessage : 'Library update failed!'
}, },
{
title : 'Update Custom Formats',
icon : 'icon-radarr-refresh',
className : 'btn-danger',
callback : this._updateQuality
},
{ {
title : 'Delete selected', title : 'Delete selected',
icon : 'icon-radarr-delete-white', icon : 'icon-radarr-delete-white',
@ -296,6 +303,20 @@ module.exports = Marionette.Layout.extend({
}); });
this.movieCollection.setPageSize(pageSize, {fetch: false}); this.movieCollection.setPageSize(pageSize, {fetch: false});
this.movieCollection.getPage(currentPage, {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
});
}
}); });

View File

@ -4,25 +4,11 @@
<h3>{{relativePath}}</h3> <h3>{{relativePath}}</h3>
</div> </div>
<div class="modal-body edit-movie-modal"> <div class="modal-body edit-movie-modal">
<div class="row"> <div id="select-quality">
<div class="col-sm-12">
<div class="form-horizontal">
<div class="form-group">
<label class="col-sm-4 control-label">Quality</label>
<div class="col-sm-4"> </div>
<select class="form-control x-quality" id="inputProfile" name="qualityId">
{{#each qualities}}
<option value="{{quality.id}}">{{quality.name}}</option>
{{/each}}
</select>
</div>
</div>
</div>
</div>
</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<span class="indicator x-indicator"><i class="icon-radarr-spinner fa-spin"></i></span> <span class="indicator x-indicator"><i class="icon-radarr-spinner fa-spin"></i></span>

View File

@ -1,13 +1,13 @@
var vent = require('vent'); var vent = require('vent');
var Marionette = require('marionette'); var Marionette = require('marionette');
var Qualities = require('../../../../Quality/QualityDefinitionCollection');
var AsModelBoundView = require('../../../../Mixins/AsModelBoundView'); var AsModelBoundView = require('../../../../Mixins/AsModelBoundView');
var AsValidatedView = require('../../../../Mixins/AsValidatedView'); var AsValidatedView = require('../../../../Mixins/AsValidatedView');
var AsEditModalView = require('../../../../Mixins/AsEditModalView'); var AsEditModalView = require('../../../../Mixins/AsEditModalView');
require('../../../../Mixins/TagInput'); var LoadingView = require('../../../../Shared/LoadingView');
require('../../../../Mixins/FileBrowser'); var ProfileSchemaCollection = require('../../../../Settings/Profile/ProfileSchemaCollection');
var SelectQualityView = require('../../../../ManualImport/Quality/SelectQualityView');
var view = Marionette.ItemView.extend({ var view = Marionette.Layout.extend({
template : 'Movies/Files/Media/Edit/EditFileTemplate', template : 'Movies/Files/Media/Edit/EditFileTemplate',
ui : { ui : {
@ -16,34 +16,44 @@ var view = Marionette.ItemView.extend({
tags : '.x-tags' tags : '.x-tags'
}, },
regions : {
selectQuality : '#select-quality'
},
events : { events : {
}, },
initialize : function() { initialize : function() {
this.qualities = new Qualities(); this.profileSchemaCollection = new ProfileSchemaCollection();
var self = this; this.profileSchemaCollection.fetch();
this.listenTo(this.qualities, 'all', this._qualitiesUpdated);
this.qualities.fetch();
}, this.listenTo(this.profileSchemaCollection, 'sync', this._showQuality);
},
onRender : function() { onRender : function() {
this.ui.quality.val(this.model.get("quality").quality.id); this.selectQuality.show(new LoadingView());
}, },
_showQuality : function () {
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;
});
var quality = this.model.get("quality");
this.selectQualityView = new SelectQualityView({ qualities: qualities, formats : formats, current : {
formats : quality.customFormats, quality : quality.quality
}
});
this.selectQuality.show(this.selectQualityView);
},
_onBeforeSave : function() { _onBeforeSave : function() {
var qualityId = this.ui.quality.val(); this.model.set({ quality : this.selectQualityView.selectedQuality() });
var quality = this.qualities.find(function(m){return m.get("quality").id === parseInt(qualityId);}).get("quality");
var mQuality = this.model.get("quality");
mQuality.quality = quality;
this.model.set({ quality : mQuality });
},
_qualitiesUpdated : function() {
this.templateHelpers = {};
this.templateHelpers.qualities = this.qualities.toJSON();
this.render();
}, },
_onAfterSave : function() { _onAfterSave : function() {

View File

@ -55,7 +55,7 @@
</div> </div>
<div class="row quality-legend-row"> <div class="row quality-legend-row">
<div class="col-md-2"> <div class="col-md-2">
<label class="label label-danger" >L_RE_ENGLISH</label> <label class="label label-danger" >L_RQ_ENGLISH</label>
</div> </div>
<div class="col-md-10"> <div class="col-md-10">
The Format Tag is required and does not match the release. Ergo the release will not be considered this format. The Format Tag is required and does not match the release. Ergo the release will not be considered this format.

View File

@ -1,14 +1,23 @@
<div class="row"> <div class="advanced-setting">
<div class="alert alert-warning alert-dismissable"> <div class="row">
<a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a> <div class="alert alert-warning alert-dismissable">
You can use custom formats to service all your automation needs! Read the <a href="https://github.com/Radarr/Radarr/wiki/Custom-Formats">Wiki Page</a> for more info. <a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>
If you don't have the need for full customization, you can find a lot of predefined examples <a href="https://github.com/Radarr/Radarr/wiki/Custom-Formats#examples">here</a>. You can use custom formats to service all your automation needs! Read the <a href="https://github.com/Radarr/Radarr/wiki/Custom-Formats">Wiki Page</a> for more info.
These should be able to cover most automation needs. If you don't have the need for full customization, you can find a lot of predefined examples <a href="https://github.com/Radarr/Radarr/wiki/Custom-Formats#examples">here</a>.
These should be able to cover most automation needs.
</div>
</div>
<div id="x-custom-formats-region"></div>
<div id="x-custom-formats-test">
</div>
</div>
<div class="basic-setting">
<div class="alert alert-danger alert-dismissable">
Custom Formats are very advanced. Please make sure you understand them fully before proceeding!
</div> </div>
</div> </div>
<div id="x-custom-formats-region"></div>
<div id="x-custom-formats-test">
</div>

View File

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

View File

@ -0,0 +1,21 @@
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>Delete: {{name}}</h3>
</div>
<div class="modal-body">
<p>Are you sure you want to delete '{{name}}'?</p>
<div class="alert alert-danger">
Custom Formats will be removed from all files, history items, backlisted releases and profiles.
If a profile has this format set as cutoff, the cutoff will be reset to 'None'!
<br>
<br>
<b>Important:</b> This operation may take multiple minutes to complete! Please make sure you keep this tab open and active!
</div>
</div>
<div class="modal-footer">
<span class="indicator x-indicator"><i class="icon-radarr-spinner fa-spin"></i></span>
<button class="btn x-cancel-confirm" data-dismiss="modal">Cancel</button>
<button class="btn btn-danger x-confirm-delete">Delete</button>
</div>
</div>

View File

@ -2,7 +2,7 @@
var $ = require('jquery'); var $ = require('jquery');
var vent = require('vent'); var vent = require('vent');
var Marionette = require('marionette'); var Marionette = require('marionette');
//var DeleteView = require('../Delete/IndexerDeleteView'); var DeleteView = require('../DeleteCustomFormatView');
var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); var AsModelBoundView = require('../../../Mixins/AsModelBoundView');
var AsValidatedView = require('../../../Mixins/AsValidatedView'); var AsValidatedView = require('../../../Mixins/AsValidatedView');
var AsEditModalView = require('../../../Mixins/AsEditModalView'); var AsEditModalView = require('../../../Mixins/AsEditModalView');
@ -29,7 +29,7 @@ var view = Marionette.Layout.extend({
testArea : '#x-test-region' testArea : '#x-test-region'
}, },
//_deleteView : DeleteView, _deleteView : DeleteView,
initialize : function(options) { initialize : function(options) {
this.targetCollection = options.targetCollection; this.targetCollection = options.targetCollection;

View File

@ -31,7 +31,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
{{#if id}} {{#if id}}
<button class="btn btn-danger pull-left x-delete disabled" title="You cannot delete custom formats for now!">Delete</button> <button class="btn btn-danger pull-left x-delete">Delete</button>
{{else}} {{else}}
<button class="btn pull-left x-back">Back</button> <button class="btn pull-left x-back">Back</button>
{{/if}} {{/if}}

View File

@ -23,7 +23,7 @@
<i class="icon-radarr-form-info" title="Qualities higher in the list are more preferred. Only checked qualities will be wanted."/> <i class="icon-radarr-form-info" title="Qualities higher in the list are more preferred. Only checked qualities will be wanted."/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group advanced-setting">
<label class="col-sm-3 control-label">Custom Formats</label> <label class="col-sm-3 control-label">Custom Formats</label>
<div class="col-sm-5"> <div class="col-sm-5">

View File

@ -15,7 +15,7 @@
<select class="form-control" name="language"> <select class="form-control" name="language">
{{#each languages}} {{#each languages}}
{{#unless_eq nameLower compare="unknown"}} {{#unless_eq nameLower compare="unknown"}}
<option value="{{nameLower}}">{{name}}</option> <option value="{{nameLower}}" {{#if_eq nameLower compare="any"}}class="advanced-setting"{{/if_eq}}>{{name}}</option>
{{/unless_eq}} {{/unless_eq}}
{{/each}} {{/each}}
</select> </select>
@ -58,7 +58,7 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group advanced-setting">
<label class="col-sm-3 control-label">Custom Format Cutoff</label> <label class="col-sm-3 control-label">Custom Format Cutoff</label>
<div class="col-sm-5"> <div class="col-sm-5">

View File

@ -1,10 +1,8 @@
<div class="row quality-definition-row"> <div class="row quality-definition-row">
<span class="col-md-2 col-sm-3"> <span class="col-md-2 col-sm-3">
{{#if parentQualityDefinition }}
<span class="label label-warning">Custom Format</span> {{quality.name}}
{{ else }}
{{quality.name}}
{{/if}}
</span> </span>
<span class="col-md-2 col-sm-3"> <span class="col-md-2 col-sm-3">
<input type="text" class="form-control" name="title"> <input type="text" class="form-control" name="title">
@ -35,28 +33,8 @@
</div> </div>
</div> </div>
</span> </span>
{{#if parentQualityDefinition }}
<span class="col-md-1">
Parent:
</span>
<span class="col-md-3 col-sm-4">
<select class="form-control x-parent" name="parentQuality">
{{#each qualities}}
<option value="{{id}}">{{title}}</option>
{{/each}}
</select>
</span>
{{else}}
<span class="col-md-3 col-sm-4 advanced-setting">
<button class="btn btn-success x-create-format">Create Custom Format</button>
</span>
{{/if}}
</div> </div>
{{#if parentQualityDefinition}}
{{/if}}

View File

@ -154,3 +154,8 @@ li.save-and-add:hover {
} }
.x-custom-formats-tab {
color: @brand-warning !important;
}