[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
/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"?>
<project version="4">
<component name="ContentModelUserStore">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</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)
{
{
var movieFile = _mediaFileService.GetMovie(movieFileResource.Id);
movieFile.Quality = movieFileResource.Quality;
_mediaFileService.Update(movieFile);

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ using System.Linq;
using FluentValidation;
using Nancy;
using NzbDrone.Api.Extensions;
using NzbDrone.Api.Validation;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Parser;
@ -21,7 +22,7 @@ namespace NzbDrone.Api.Qualities
SharedValidator.RuleFor(c => c.Name).NotEmpty();
SharedValidator.RuleFor(c => c.Name)
.Must((v, c) => !_formatService.All().Any(f => f.Name == c && f.Id != v.Id)).WithMessage("Must be unique.");
SharedValidator.RuleFor(c => c.FormatTags).Must((v, c) => c.All(s => FormatTag.QualityTagRegex.IsMatch(s))).WithMessage("Invalid format.");
SharedValidator.RuleFor(c => c.FormatTags).AreValidFormatTags();
SharedValidator.RuleFor(c => c.FormatTags).Must((v, c) =>
{
var allFormats = _formatService.All();
@ -44,6 +45,8 @@ namespace NzbDrone.Api.Qualities
CreateResource = Create;
DeleteResource = Delete;
Get["/test"] = x => Test();
Post["/test"] = x => TestWithNewModel();
@ -73,6 +76,11 @@ namespace NzbDrone.Api.Qualities
return _formatService.All().ToResource();
}
private void Delete(int id)
{
_formatService.Delete(id);
}
private Response GetTemplates()
{
return CustomFormatService.Templates.SelectMany(t =>
@ -107,8 +115,9 @@ namespace NzbDrone.Api.Qualities
var resource = ReadResourceFromRequest();
var model = resource.ToModel();
model.Name = model.Name += " (New)";
var parsed = _parsingService.ParseMovieInfo((string) Request.Query.title, new List<object>{model});
var parsed = _parsingService.ParseMovieInfo(queryTitle, new List<object>{model});
if (parsed == 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 FluentValidation;
using FluentValidation.Validators;
using NzbDrone.Api.Qualities;
namespace NzbDrone.Api.Validation
{
@ -41,5 +42,11 @@ namespace NzbDrone.Api.Validation
{
return ruleBuilder.SetValidator(new NetImportSyncIntervalValidator());
}
public static IRuleBuilderOptions<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 NUnit.Framework;
using NzbDrone.Core.CustomFormats;
@ -24,13 +25,16 @@ namespace NzbDrone.Core.Test.CustomFormat
[TestCase("L_English", TagType.Language, Language.English)]
[TestCase("L_germaN", TagType.Language, Language.German)]
[TestCase("E_Director", TagType.Edition, "director")]
[TestCase("E_R_Director('?s)?", TagType.Edition, "director('?s)?", TagModifier.Regex)]
[TestCase("E_RN_Director('?s)?", TagType.Edition, "director('?s)?", TagModifier.Regex, TagModifier.Not)]
[TestCase("E_RNRE_Director('?s)?", TagType.Edition, "director('?s)?", TagModifier.Regex, TagModifier.Not, TagModifier.AbsolutelyRequired)]
[TestCase("E_RX_Director('?s)?", TagType.Edition, "director('?s)?", TagModifier.Regex)]
[TestCase("E_RXN_Director('?s)?", TagType.Edition, "director('?s)?", TagModifier.Regex, TagModifier.Not)]
[TestCase("E_RXNRQ_Director('?s)?", TagType.Edition, "director('?s)?", TagModifier.Regex, TagModifier.Not, TagModifier.AbsolutelyRequired)]
[TestCase("C_Surround", TagType.Custom, "surround")]
[TestCase("C_RE_Surround", TagType.Custom, "surround", TagModifier.AbsolutelyRequired)]
[TestCase("C_REN_Surround", TagType.Custom, "surround", TagModifier.AbsolutelyRequired, TagModifier.Not)]
[TestCase("C_RENR_Surround|(5|7)(\\.1)?", TagType.Custom, "surround|(5|7)(\\.1)?", TagModifier.AbsolutelyRequired, TagModifier.Not, TagModifier.Regex)]
[TestCase("C_RQ_Surround", TagType.Custom, "surround", TagModifier.AbsolutelyRequired)]
[TestCase("C_RQN_Surround", TagType.Custom, "surround", TagModifier.AbsolutelyRequired, TagModifier.Not)]
[TestCase("C_RQNRX_Surround|(5|7)(\\.1)?", TagType.Custom, "surround|(5|7)(\\.1)?", TagModifier.AbsolutelyRequired, TagModifier.Not, TagModifier.Regex)]
[TestCase("G_10<>20", TagType.Size, new[] { 10737418240L, 21474836480L})]
[TestCase("G_15.55<>20", TagType.Size, new[] { 16696685363L, 21474836480L})]
[TestCase("G_15.55<>25.1908754", TagType.Size, new[] { 16696685363L, 27048496500L})]
public void should_parse_tag_from_string(string raw, TagType type, object value, params TagModifier[] modifiers)
{
var parsed = new FormatTag(raw);
@ -40,6 +44,10 @@ namespace NzbDrone.Core.Test.CustomFormat
modifier |= m;
}
parsed.TagType.Should().Be(type);
if (value is long[])
{
value = (((long[]) value)[0], ((long[]) value)[1]);
}
if ((parsed.Value as Regex) != null)
{
(parsed.Value as Regex).ToString().Should().Be((value as string));

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
{
private RemoteMovie _remoteEpisode;
private RemoteMovie _remoteMovie;
[SetUp]
public void Setup()
{
_remoteEpisode = new RemoteMovie
_remoteMovie = new RemoteMovie
{
ParsedMovieInfo = new ParsedMovieInfo
{
@ -38,12 +38,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
private void WithEnglishRelease()
{
_remoteEpisode.ParsedMovieInfo.Languages = new List<Language> {Language.English};
_remoteMovie.ParsedMovieInfo.Languages = new List<Language> {Language.English};
}
private void WithGermanRelease()
{
_remoteEpisode.ParsedMovieInfo.Languages = new List<Language> {Language.German};
_remoteMovie.ParsedMovieInfo.Languages = new List<Language> {Language.German};
}
[Test]
@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
WithEnglishRelease();
Mocker.Resolve<LanguageSpecification>().IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
Mocker.Resolve<LanguageSpecification>().IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
}
[Test]
@ -59,7 +59,24 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
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 NUnit.Framework;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine.Specifications.Search;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.TorrentRss;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Movies;
using NzbDrone.Test.Common;

View File

@ -18,14 +18,15 @@ using NzbDrone.Core.Download;
namespace NzbDrone.Core.Test.MediaFiles.MovieImport
{
/* [TestFixture]
//TODO: Update all of this for movies.
[TestFixture]
//TODO: Add tests to ensure helpers for augmenters are correctly passed.
public class ImportDecisionMakerFixture : CoreTest<ImportDecisionMaker>
{
private List<string> _videoFiles;
private LocalMovie _localEpisode;
private Movie _series;
private LocalMovie _localMovie;
private Movie _movie;
private QualityModel _quality;
private ParsedMovieInfo _fileInfo;
private Mock<IImportDecisionEngineSpecification> _pass1;
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"));
_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() })
.Build();
_quality = new QualityModel(Quality.DVD);
_localEpisode = new LocalMovie
{
Movie = _series,
_localMovie = new LocalMovie
{
Movie = _movie,
Quality = _quality,
Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi"
Path = @"C:\Test\Unsorted\The.Office.2018.DVDRip.XviD-OSiTV.avi"
};
_fileInfo = new ParsedMovieInfo
{
MovieTitle = "The Office",
Year = 2018,
Quality = _quality
};
Mocker.GetMock<IParsingService>()
.Setup(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<Movie>(), It.IsAny<ParsedMovieInfo>(), It.IsAny<bool>()))
.Returns(_localEpisode);
.Setup(c => c.GetLocalMovie(It.IsAny<string>(), It.IsAny<ParsedMovieInfo>(), It.IsAny<Movie>(), It.IsAny<List<object>>(), It.IsAny<bool>()))
.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)
@ -96,12 +108,12 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
Subject.GetImportDecisions(_videoFiles, new Movie(), downloadClientItem, null, false);
_fail1.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once());
_fail2.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once());
_fail3.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once());
_pass1.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once());
_pass2.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once());
_pass3.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once());
_fail1.Verify(c => c.IsSatisfiedBy(_localMovie, downloadClientItem), Times.Once());
_fail2.Verify(c => c.IsSatisfiedBy(_localMovie, downloadClientItem), Times.Once());
_fail3.Verify(c => c.IsSatisfiedBy(_localMovie, downloadClientItem), Times.Once());
_pass1.Verify(c => c.IsSatisfiedBy(_localMovie, downloadClientItem), Times.Once());
_pass2.Verify(c => c.IsSatisfiedBy(_localMovie, downloadClientItem), Times.Once());
_pass3.Verify(c => c.IsSatisfiedBy(_localMovie, downloadClientItem), Times.Once());
}
[Test]
@ -149,7 +161,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
GivenSpecifications(_pass1);
Mocker.GetMock<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>();
_videoFiles = new List<string>
@ -161,34 +173,53 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
GivenVideoFiles(_videoFiles);
Subject.GetImportDecisions(_videoFiles, _series);
Subject.GetImportDecisions(_videoFiles, _movie);
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);
}
[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]
public void should_use_file_quality_if_folder_quality_is_null()
{
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(expectedQuality);
result.Single().LocalMovie.Quality.Should().Be(_fileInfo.Quality);
}
[Test]
public void should_use_file_quality_if_file_quality_was_determined_by_name()
{
GivenSpecifications(_pass1, _pass2, _pass3);
var expectedQuality = QualityParser.ParseQuality(_videoFiles.Single());
var result = Subject.GetImportDecisions(_videoFiles, _series, null, new ParsedMovieInfo{Quality = new QualityModel(Quality.SDTV)}, true);
var result = Subject.GetImportDecisions(_videoFiles, _movie, null, new ParsedMovieInfo{Quality = new QualityModel(Quality.SDTV)}, true);
result.Single().LocalMovie.Quality.Should().Be(expectedQuality);
result.Single().LocalMovie.Quality.Should().Be(_fileInfo.Quality);
}
[Test]
@ -197,13 +228,13 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
GivenSpecifications(_pass1, _pass2, _pass3);
GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.mkv".AsOsAgnostic() });
_localEpisode.Path = _videoFiles.Single();
_localEpisode.Quality.QualitySource = QualitySource.Extension;
_localEpisode.Quality.Quality = Quality.HDTV720p;
_localMovie.Path = _videoFiles.Single();
_localMovie.Quality.QualitySource = QualitySource.Extension;
_localMovie.Quality.Quality = Quality.HDTV720p;
var expectedQuality = new QualityModel(Quality.SDTV);
var result = Subject.GetImportDecisions(_videoFiles, _series, null, new ParsedMovieInfo { Quality = expectedQuality }, true);
var result = Subject.GetImportDecisions(_videoFiles, _movie, null, new ParsedMovieInfo { Quality = expectedQuality }, true);
result.Single().LocalMovie.Quality.Should().Be(expectedQuality);
}
@ -214,168 +245,22 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
GivenSpecifications(_pass1, _pass2, _pass3);
GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.mkv".AsOsAgnostic() });
_localEpisode.Path = _videoFiles.Single();
_localEpisode.Quality.Quality = Quality.HDTV720p;
_localMovie.Path = _videoFiles.Single();
_localMovie.Quality.Quality = Quality.HDTV720p;
var expectedQuality = new QualityModel(Quality.Bluray720p);
var result = Subject.GetImportDecisions(_videoFiles, _series, null, new ParsedMovieInfo { Quality = expectedQuality }, true);
var result = Subject.GetImportDecisions(_videoFiles, _movie, null, new ParsedMovieInfo { Quality = expectedQuality }, true);
result.Single().LocalMovie.Quality.Should().Be(expectedQuality);
}
[Test]
public void should_not_throw_if_episodes_are_not_found()
{
GivenSpecifications(_pass1);
Mocker.GetMock<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]
public void should_not_use_folder_quality_when_it_is_unknown()
{
GivenSpecifications(_pass1, _pass2, _pass3);
_series.Profile = new Profile
_movie.Profile = new Profile
{
Items = Qualities.QualityFixture.GetDefaultQualities(Quality.DVD, Quality.Unknown)
};
@ -383,7 +268,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
var folderQuality = new QualityModel(Quality.Unknown);
var result = Subject.GetImportDecisions(_videoFiles, _series, null, new ParsedMovieInfo { Quality = folderQuality}, true);
var result = Subject.GetImportDecisions(_videoFiles, _movie, null, new ParsedMovieInfo { Quality = folderQuality}, true);
result.Single().LocalMovie.Quality.Should().Be(_quality);
}
@ -392,7 +277,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
public void should_return_a_decision_when_exception_is_caught()
{
Mocker.GetMock<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>();
_videoFiles = new List<string>
@ -402,9 +287,9 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
GivenVideoFiles(_videoFiles);
Subject.GetImportDecisions(_videoFiles, _series).Should().HaveCount(1);
Subject.GetImportDecisions(_videoFiles, _movie).Should().HaveCount(1);
ExceptionVerification.ExpectedErrors(1);
}
}*/
}
}

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.Drawing" />
<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.Linq" />
<Reference Include="Microsoft.CSharp" />
@ -136,12 +139,14 @@
<Compile Include="Datastore\Migration\075_force_lib_updateFixture.cs" />
<Compile Include="Datastore\Migration\090_update_kickass_urlFixture.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\PagingSpecExtensionsTests\PagingOffsetFixture.cs" />
<Compile Include="Datastore\PagingSpecExtensionsTests\ToSortDirectionFixture.cs" />
<Compile Include="Datastore\ReflectionStrategyFixture\Benchmarks.cs" />
<Compile Include="Datastore\SqliteSchemaDumperTests\SqliteSchemaDumperFixture.cs" />
<Compile Include="DecisionEngineTests\AcceptableSizeSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\CustomFormatAllowedByProfileSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\MaximumSizeSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\ProtocolSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\CutoffSpecificationFixture.cs" />
@ -283,6 +288,7 @@
<Compile Include="MediaFiles\MovieImport\Specifications\UpgradeSpecificationFixture.cs" />
<Compile Include="MediaFiles\ImportApprovedEpisodesFixture.cs" />
<Compile Include="MediaFiles\MediaFileRepositoryFixture.cs" />
<Compile Include="MediaFiles\UpdateMovieFileQualityServiceFixture.cs" />
<Compile Include="Messaging\Commands\CommandQueueManagerFixture.cs" />
<Compile Include="MetadataSource\SkyHook\SkyHookProxySearchFixture.cs" />
<Compile Include="MetadataSource\SearchMovieComparerFixture.cs" />
@ -297,6 +303,7 @@
<Compile Include="ParserTests\ParsingServiceTests\AugmentersTests\AugmentWithFileSizeFixture.cs" />
<Compile Include="ParserTests\ParsingServiceTests\AugmentersTests\AugmentWithHistoryFixture.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\ParseQualityDefinitionFixture.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
{
/*
[SetUp]
public void Setup()
{
QualityDefinitionServiceFixture.SetupDefaultDefinitions();
//QualityDefinitionServiceFixture.SetupDefaultDefinitions();
}
public static object[] SelfQualityParserCases = QualityDefinition.DefaultQualityDefinitions.ToArray();
//public static object[] SelfQualityParserCases = QualityDefinition.DefaultQualityDefinitions.ToArray();
public static object[] OtherSourceQualityParserCases =
{
@ -288,7 +288,7 @@ namespace NzbDrone.Core.Test.ParserTests
ParseAndVerifyQuality(title, Source.UNKNOWN, proper, Resolution.Unknown);
}
[Test, TestCaseSource("SelfQualityParserCases")]
/*[Test, TestCaseSource("SelfQualityParserCases")]
public void parsing_our_own_quality_enum_name(QualityDefinition definition)
{
var fileName = string.Format("My series S01E01 [{0}]", definition.Title);
@ -300,7 +300,7 @@ namespace NzbDrone.Core.Test.ParserTests
if (resolution != null) result.Resolution.Should().Be(resolution);
if (modifier != null) result.Modifier.Should().Be(modifier);
}
}*/
[Test, TestCaseSource("OtherSourceQualityParserCases")]
public void should_parse_quality_from_other_source(string qualityString, Source source, Resolution resolution, Modifier modifier = Modifier.NONE)
@ -352,6 +352,6 @@ namespace NzbDrone.Core.Test.ParserTests
var version = proper ? 2 : 1;
result.Revision.Version.Should().Be(version);
}*/
}
}
}

View File

@ -13,5 +13,6 @@
<package id="NLog" version="4.5.0-rc06" targetFramework="net40" />
<package id="NUnit" version="3.5.0" 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" />
</packages>

View File

@ -49,4 +49,20 @@ namespace NzbDrone.Core.CustomFormats
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.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Messaging;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Composition;
using NzbDrone.Core.Blacklisting;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.History;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Profiles;
@ -17,6 +21,7 @@ namespace NzbDrone.Core.CustomFormats
CustomFormat Insert(CustomFormat customFormat);
List<CustomFormat> All();
CustomFormat GetById(int id);
void Delete(int id);
}
@ -24,6 +29,10 @@ namespace NzbDrone.Core.CustomFormats
{
private readonly ICustomFormatRepository _formatRepository;
private IProfileService _profileService;
private readonly IMediaFileService _mediaFileService;
private readonly IBlacklistService _blacklistService;
private readonly IHistoryService _historyService;
private readonly IPendingReleaseService _pendingReleaseService;
public IProfileService ProfileService
{
@ -45,12 +54,18 @@ namespace NzbDrone.Core.CustomFormats
public static Dictionary<int, CustomFormat> AllCustomFormats;
public CustomFormatService(ICustomFormatRepository formatRepository, ICacheManager cacheManager,
IContainer container,
IContainer container, IHistoryService historyService,/*IMediaFileService mediaFileService, IBlacklistService blacklistService,
IHistoryService historyService, IPendingReleaseService pendingReleaseService,*/
Logger logger)
{
_formatRepository = formatRepository;
_container = container;
_cache = cacheManager.GetCache<Dictionary<int, CustomFormat>>(typeof(CustomFormat), "formats");
/*_mediaFileService = mediaFileService;
_blacklistService = blacklistService;
_historyService = historyService;
_pendingReleaseService = pendingReleaseService;*/
_historyService = historyService;
_logger = logger;
}
@ -69,7 +84,7 @@ namespace NzbDrone.Core.CustomFormats
}
catch (Exception e)
{
_logger.Error("Failure while trying to add the new custom format to all profiles.", e);
_logger.Error(e, "Failure while trying to add the new custom format to all profiles. Deleting again!");
_formatRepository.Delete(ret);
throw;
}
@ -77,6 +92,76 @@ namespace NzbDrone.Core.CustomFormats
return ret;
}
public void Delete(int id)
{
_cache.Clear();
try
{
//First history:
var historyRepo = _container.Resolve<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()
{
return _cache.Get("all", () =>

View File

@ -1,4 +1,5 @@
using System;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using NzbDrone.Common.Extensions;
@ -14,7 +15,9 @@ namespace NzbDrone.Core.CustomFormats
public TagModifier TagModifier { get; set; }
public object Value { get; set; }
public static Regex QualityTagRegex = new Regex(@"^(?<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)
{
@ -29,6 +32,7 @@ namespace NzbDrone.Core.CustomFormats
ParseRawMatch(match);
}
// This function is needed for json deserialization to work.
private FormatTag()
{
@ -74,6 +78,10 @@ namespace NzbDrone.Core.CustomFormats
return movieInfo.Quality.Modifier == (Modifier) Value;
case TagType.Source:
return movieInfo.Quality.Source == (Source) Value;
case TagType.Size:
var size = (movieInfo.ExtraInfo.GetValueOrDefault("Size", 0.0) as long?) ?? 0;
var tuple = Value as (long, long)? ?? (0, 0);
return size > tuple.Item1 && size < tuple.Item2;
case TagType.Indexer:
return (movieInfo.ExtraInfo.GetValueOrDefault("IndexerFlags") as IndexerFlags?)?.HasFlag((IndexerFlags) Value) == true;
default:
@ -191,6 +199,13 @@ namespace NzbDrone.Core.CustomFormats
break;
}
break;
case "g":
TagType = TagType.Size;
var matches = SizeTagRegex.Match(value);
var min = double.Parse(matches.Groups["min"].Value, CultureInfo.InvariantCulture);
var max = double.Parse(matches.Groups["max"].Value, CultureInfo.InvariantCulture);
Value = (min.Gigabytes(), max.Gigabytes());
break;
case "c":
default:
@ -218,6 +233,7 @@ namespace NzbDrone.Core.CustomFormats
Language = 16,
Custom = 32,
Indexer = 64,
Size = 128,
}
[Flags]

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)
{
var left = x.RemoteMovie.ParsedMovieInfo.Quality.CustomFormats.ToArray().ToList();
if (left.Count == 0)
{
left.Add(CustomFormat.None);
}
var left = x.RemoteMovie.ParsedMovieInfo.Quality.CustomFormats.WithNone();
var right = y.RemoteMovie.ParsedMovieInfo.Quality.CustomFormats;
var leftIndicies = QualityModelComparer.GetIndicies(left, x.RemoteMovie.Movie.Profile.Value);

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;
if (wantedLanguage == Language.Any)
{
_logger.Debug("Profile allows any language, accepting release.");
return Decision.Accept();
}
_logger.Debug("Checking if report meets language requirements. {0}", subject.ParsedMovieInfo.Languages.ToExtendedString());
if (!subject.ParsedMovieInfo.Languages.Contains(wantedLanguage))

View File

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

View File

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

View File

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

View File

@ -25,6 +25,8 @@ namespace NzbDrone.Core.History
History Get(int historyId);
List<History> Find(string downloadId, HistoryEventType eventType);
List<History> FindByDownloadId(string downloadId);
List<History> FindByMovieId(int movieId);
void UpdateMany(List<History> toUpdate);
}
public class HistoryService : IHistoryService,
@ -73,6 +75,11 @@ namespace NzbDrone.Core.History
return _historyRepository.FindByDownloadId(downloadId);
}
public List<History> FindByMovieId(int movieId)
{
return _historyRepository.FindByMovieId(movieId);
}
public QualityModel GetBestQualityInHistory(Profile profile, int movieId)
{
var comparer = new QualityModelComparer(profile);
@ -81,6 +88,11 @@ namespace NzbDrone.Core.History
.FirstOrDefault();
}
public void UpdateMany(List<History> toUpdate)
{
_historyRepository.UpdateMany(toUpdate);
}
public void Handle(MovieGrabbedEvent message)
{
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
{
var minimalInfo = shouldUseFolderName
? folderInfo.JsonClone()
: _parsingService.ParseMinimalPathMovieInfo(file);
ParsedMovieInfo modifiedFolderInfo = null;
if (folderInfo != null)
{
modifiedFolderInfo = folderInfo.JsonClone();
// We want the filename to be used for parsing quality, etc. even if we didn't get any movie info from there.
modifiedFolderInfo.SimpleReleaseTitle = Path.GetFileName(file);
}
var minimalInfo = _parsingService.ParseMinimalPathMovieInfo(file) ?? modifiedFolderInfo;
LocalMovie localMovie = null;
//var localMovie = _parsingService.GetLocalMovie(file, movie, shouldUseFolderName ? folderInfo : null, sceneSource);
if (minimalInfo != null)
{
//TODO: make it so media info doesn't ruin the import process of a new movie
var mediaInfo = (_config.EnableMediaInfo || !movie.Path.IsParentPath(file)) ? _videoFileInfoReader.GetMediaInfo(file) : null;
var mediaInfo = (_config.EnableMediaInfo || !movie.Path?.IsParentPath(file) == true) ? _videoFileInfoReader.GetMediaInfo(file) : null;
var size = _diskProvider.GetFileSize(file);
var historyItems = _historyService.FindByDownloadId(downloadClientItem?.DownloadId ?? "");
var firstHistoryItem = historyItems.OrderByDescending(h => h.Date).FirstOrDefault();
var firstHistoryItem = historyItems?.OrderByDescending(h => h.Date)?.FirstOrDefault();
var sizeMovie = new LocalMovie();
sizeMovie.Size = size;
localMovie = _parsingService.GetLocalMovie(file, minimalInfo, movie, new List<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.Size = size;
_logger.Debug("Size: {0}", localMovie.Size);
decision = GetDecision(localMovie, downloadClientItem);
}
else
{
localMovie = new LocalMovie();
@ -201,38 +204,10 @@ namespace NzbDrone.Core.MediaFiles.MovieImport
return null;
}
//TODO: Remove this method, since it is no longer needed.
private bool ShouldUseFolderName(List<string> videoFiles, Movie movie, ParsedMovieInfo folderInfo)
{
if (folderInfo == null)
{
return false;
}
//if (folderInfo.FullSeason)
//{
// return false;
//}
return videoFiles.Count(file =>
{
var size = _diskProvider.GetFileSize(file);
var fileQuality = QualityParser.ParseQuality(file);
//var sample = null;//_detectSample.IsSample(movie, GetQuality(folderInfo, fileQuality, movie), file, size, folderInfo.IsPossibleSpecialEpisode); //Todo to this
return true;
//if (sample)
{
return false;
}
if (SceneChecker.IsSceneTitle(Path.GetFileName(file)))
{
return false;
}
return true;
}) == 1;
return false;
}
private QualityModel GetQuality(ParsedMovieInfo folderInfo, QualityModel fileQuality, Movie movie)

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)
{
var langCode = profile != null ? IsoLanguages.Get(profile.Language).TwoLetterCode : "en";
var langCode = profile != null ? IsoLanguages.Get(profile.Language)?.TwoLetterCode ?? "en" : "en";
var request = _movieBuilder.Create()
.SetSegment("route", "movie")
@ -140,7 +140,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
movie.ImdbId = resource.imdb_id;
movie.Title = resource.title;
movie.TitleSlug = Parser.Parser.ToUrlSlug(resource.title);
movie.CleanTitle = Parser.Parser.CleanSeriesTitle(resource.title);
movie.CleanTitle = resource.title.CleanSeriesTitle();
movie.SortTitle = Parser.Parser.NormalizeTitle(resource.title);
movie.Overview = resource.overview;
movie.Website = resource.homepage;

View File

@ -1,16 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections.Generic;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Datastore.Extensions;
using Marr.Data.QGen;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Parser.RomanNumerals;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Movies;
using CoreParser = NzbDrone.Core.Parser.Parser;
namespace NzbDrone.Core
namespace NzbDrone.Core.Movies
{
public static class QueryExtensions
{

View File

@ -101,6 +101,9 @@
<Reference Include="System.Numerics" />
<Reference Include="System.ServiceModel" />
<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.Extensions" />
<Reference Include="System.Windows.Forms" />
@ -141,11 +144,14 @@
<Compile Include="Datastore\Migration\141_fix_duplicate_alt_titles.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\149_convert_regex_required_tags.cs" />
<Compile Include="DecisionEngine\Specifications\CustomFormatAllowedByProfileSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\MaximumSizeSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\RequiredIndexerFlagsSpecification.cs" />
<Compile Include="Extras\Metadata\Consumers\Xbmc\XbmcNfoDetector.cs" />
<Compile Include="Extras\Others\OtherExtraFileRenamer.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\SameFileSpecification.cs" />
<Compile Include="MediaFiles\Events\MovieFileUpdatedEvent.cs" />
@ -157,6 +163,7 @@
<Compile Include="Datastore\Migration\133_add_minimumavailability.cs" />
<Compile Include="IndexerSearch\CutoffUnmetMoviesSearchCommand.cs" />
<Compile Include="Indexers\HDBits\HDBitsInfo.cs" />
<Compile Include="MediaFiles\UpdateMovieFileQualityService.cs" />
<Compile Include="Movies\AlternativeTitles\AlternativeTitle.cs" />
<Compile Include="Movies\AlternativeTitles\AlternativeTitleRepository.cs" />
<Compile Include="Movies\AlternativeTitles\AlternativeTitleService.cs" />
@ -973,6 +980,7 @@
<Compile Include="Parser\Augmenters\AugmentWithFileSize.cs" />
<Compile Include="Parser\Augmenters\AugmentWithHistory.cs" />
<Compile Include="Parser\Augmenters\AugmentWithMediaInfo.cs" />
<Compile Include="Parser\Augmenters\AugmentWithParsedMovieInfo.cs" />
<Compile Include="Parser\Augmenters\AugmentWithReleaseInfo.cs" />
<Compile Include="Parser\Augmenters\IAugmentParsedMovieInfo.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("ko", "kor", Language.Korean),
new IsoLanguage("hu", "hun", Language.Hungarian),
new IsoLanguage("he", "heb", Language.Hebrew)
new IsoLanguage("he", "heb", Language.Hebrew),
new IsoLanguage("an", "any", Language.Any)
};
public static IsoLanguage Find(string isoCode)

View File

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

View File

@ -16,6 +16,7 @@ namespace NzbDrone.Core.Profiles
Profile Add(Profile profile);
void Update(Profile profile);
void AddCustomFormat(CustomFormat format);
void DeleteCustomFormat(int formatId);
void Delete(int id);
List<Profile> All();
Profile Get(int id);
@ -65,6 +66,21 @@ namespace NzbDrone.Core.Profiles
}
}
public void DeleteCustomFormat(int formatId)
{
var all = All();
foreach (var profile in all)
{
profile.FormatItems = profile.FormatItems.Where(c => c.Format.Id != formatId).ToList();
if (profile.FormatCutoff.Id == formatId)
{
profile.FormatCutoff = CustomFormat.None;
}
Update(profile);
}
}
public void Delete(int id)
{
if (_movieService.GetAllMovies().Any(c => c.ProfileId == id) || _netImportFactory.All().Any(c => c.ProfileId == id))

View File

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

View File

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

View File

@ -9,6 +9,7 @@
<package id="OAuth" version="1.0.3" targetFramework="net40" />
<package id="Prowlin" version="0.9.4456.26422" 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="xmlrpcnet" version="2.5.0" targetFramework="net40" />
</packages>

View File

@ -31,8 +31,11 @@ module.exports = Marionette.Layout.extend({
var qualities = _.map(this.profileSchemaCollection.first().get('items'), function (quality) {
return quality.quality;
});
var formats = _.map(this.profileSchemaCollection.first().get('formatItems'), function (format) {
return format.format;
});
this.selectQualityView = new SelectQualityView({ qualities: qualities });
this.selectQualityView = new SelectQualityView({ qualities: qualities, formats : formats });
this.quality.show(this.selectQualityView);
},
@ -40,4 +43,4 @@ module.exports = Marionette.Layout.extend({
this.trigger('manualimport:selected:quality', { quality: this.selectQualityView.selectedQuality() });
vent.trigger(vent.Commands.CloseModal2Command);
}
});
});

View File

@ -1,22 +1,40 @@
var _ = require('underscore');
var Marionette = require('marionette');
var Backbone = require('backbone');
require('../../Mixins/TagInput');
module.exports = Marionette.ItemView.extend({
template : 'ManualImport/Quality/SelectQualityViewTemplate',
ui : {
select : '.x-select-quality',
proper : 'x-proper'
proper : 'x-proper',
formats: '.x-tags',
},
initialize : function(options) {
this.qualities = options.qualities;
this.formats = options.formats;
this.current = options.current || {};
this.templateHelpers = {
qualities: this.qualities
qualities: this.qualities,
formats: JSON.stringify(_.map(this.formats, function(f) {
return { value : f.id, name : f.name };
})),
};
},
onRender : function() {
if (this.current.formats != undefined) {
this.ui.formats.val(this.current.formats.map(function(m) {return m.id;}).join(","));
}
if (this.current.quality != undefined) {
this.ui.select.val(this.current.quality.id);
}
this.ui.formats.tagInput();
},
selectedQuality : function () {
var selected = parseInt(this.ui.select.val(), 10);
var proper = this.ui.proper.prop('checked');
@ -25,13 +43,21 @@ module.exports = Marionette.ItemView.extend({
return q.id === selected;
});
var formatIds = this.ui.formats.val().split(',');
var formats = _.map(_.filter(this.formats, function(f) {
return formatIds.includes(f.id + "");
}), function(f) {
return { name : f.name, id : f.id};
});
return {
quality : quality,
revision : {
version : proper ? 2 : 1,
real : 0
}
},
customFormats : formats
};
}
});
});

View File

@ -30,4 +30,12 @@
</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>

View File

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

View File

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

View File

@ -16,6 +16,7 @@ var GridPager = require('../../Shared/Grid/Pager');
require('../../Mixins/backbone.signalr.mixin');
var DeleteSelectedView = require('./Delete/DeleteSelectedView');
var Config = require('../../Config');
var CommandController = require('../../Commands/CommandController');
window.shownOnce = false;
module.exports = Marionette.Layout.extend({
@ -116,6 +117,12 @@ module.exports = Marionette.Layout.extend({
successMessage : 'Library was updated!',
errorMessage : 'Library update failed!'
},
{
title : 'Update Custom Formats',
icon : 'icon-radarr-refresh',
className : 'btn-danger',
callback : this._updateQuality
},
{
title : 'Delete selected',
icon : 'icon-radarr-delete-white',
@ -296,6 +303,20 @@ module.exports = Marionette.Layout.extend({
});
this.movieCollection.setPageSize(pageSize, {fetch: false});
this.movieCollection.getPage(currentPage, {fetch: false});
}
},
_updateQuality : function() {
var selected = FullMovieCollection.where({ selected : true});
var files = selected.filter(function(model) {
return model.get("movieFile") !== undefined;
}).map(function(model){
return model.get("movieFile").id;
});
CommandController.Execute('updateMovieFileQuality', {
name : 'updateMovieFileQuality',
movieFileIds : files
});
}
});

View File

@ -4,25 +4,11 @@
<h3>{{relativePath}}</h3>
</div>
<div class="modal-body edit-movie-modal">
<div class="row">
<div class="col-sm-12">
<div class="form-horizontal">
<div class="form-group">
<label class="col-sm-4 control-label">Quality</label>
<div id="select-quality">
<div class="col-sm-4">
<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">
<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 Marionette = require('marionette');
var Qualities = require('../../../../Quality/QualityDefinitionCollection');
var AsModelBoundView = require('../../../../Mixins/AsModelBoundView');
var AsValidatedView = require('../../../../Mixins/AsValidatedView');
var AsEditModalView = require('../../../../Mixins/AsEditModalView');
require('../../../../Mixins/TagInput');
require('../../../../Mixins/FileBrowser');
var LoadingView = require('../../../../Shared/LoadingView');
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',
ui : {
@ -16,34 +16,44 @@ var view = Marionette.ItemView.extend({
tags : '.x-tags'
},
regions : {
selectQuality : '#select-quality'
},
events : {
},
initialize : function() {
this.qualities = new Qualities();
var self = this;
this.listenTo(this.qualities, 'all', this._qualitiesUpdated);
this.qualities.fetch();
initialize : function() {
this.profileSchemaCollection = new ProfileSchemaCollection();
this.profileSchemaCollection.fetch();
},
this.listenTo(this.profileSchemaCollection, 'sync', this._showQuality);
},
onRender : function() {
this.ui.quality.val(this.model.get("quality").quality.id);
},
onRender : function() {
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() {
var qualityId = this.ui.quality.val();
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();
this.model.set({ quality : this.selectQualityView.selectedQuality() });
},
_onAfterSave : function() {

View File

@ -55,7 +55,7 @@
</div>
<div class="row quality-legend-row">
<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 class="col-md-10">
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="alert alert-warning alert-dismissable">
<a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</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.
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 class="advanced-setting">
<div class="row">
<div class="alert alert-warning alert-dismissable">
<a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</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.
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 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 vent = require('vent');
var Marionette = require('marionette');
//var DeleteView = require('../Delete/IndexerDeleteView');
var DeleteView = require('../DeleteCustomFormatView');
var AsModelBoundView = require('../../../Mixins/AsModelBoundView');
var AsValidatedView = require('../../../Mixins/AsValidatedView');
var AsEditModalView = require('../../../Mixins/AsEditModalView');
@ -29,7 +29,7 @@ var view = Marionette.Layout.extend({
testArea : '#x-test-region'
},
//_deleteView : DeleteView,
_deleteView : DeleteView,
initialize : function(options) {
this.targetCollection = options.targetCollection;

View File

@ -31,7 +31,7 @@
</div>
<div class="modal-footer">
{{#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}}
<button class="btn pull-left x-back">Back</button>
{{/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."/>
</div>
</div>
<div class="form-group">
<div class="form-group advanced-setting">
<label class="col-sm-3 control-label">Custom Formats</label>
<div class="col-sm-5">

View File

@ -15,7 +15,7 @@
<select class="form-control" name="language">
{{#each languages}}
{{#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}}
{{/each}}
</select>
@ -58,7 +58,7 @@
</div>
</div>
<div class="form-group">
<div class="form-group advanced-setting">
<label class="col-sm-3 control-label">Custom Format Cutoff</label>
<div class="col-sm-5">

View File

@ -1,10 +1,8 @@
<div class="row quality-definition-row">
<span class="col-md-2 col-sm-3">
{{#if parentQualityDefinition }}
<span class="label label-warning">Custom Format</span>
{{ else }}
{{quality.name}}
{{/if}}
{{quality.name}}
</span>
<span class="col-md-2 col-sm-3">
<input type="text" class="form-control" name="title">
@ -35,28 +33,8 @@
</div>
</div>
</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>
{{#if parentQualityDefinition}}
{{/if}}

View File

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