New: User defined scores for each Custom Format

Brings it more into line with Sonarr preferred words
This commit is contained in:
ta264
2020-02-25 22:10:52 +00:00
parent da80793204
commit 50d6c5e61e
49 changed files with 547 additions and 791 deletions
@@ -23,13 +23,10 @@ namespace NzbDrone.Api.Profiles
SharedValidator.RuleFor(c => c.FormatItems).Must(items =>
{
var all = _formatService.All().Select(f => f.Id).ToList();
all.Add(CustomFormat.None.Id);
var ids = items.Select(i => i.Format.Id);
return all.Except(ids).Empty();
}).WithMessage("All Custom Formats and no extra ones need to be present inside your Profile! Try refreshing your browser.");
SharedValidator.RuleFor(c => c.FormatCutoff)
.Must(c => _formatService.All().Select(f => f.Id).Contains(c.Id) || c.Id == CustomFormat.None.Id).WithMessage("The Custom Format Cutoff must be a valid Custom Format! Try refreshing your browser.");
GetResourceAll = GetAll;
GetResourceById = GetById;
+9 -23
View File
@@ -15,7 +15,8 @@ namespace NzbDrone.Api.Profiles
public Quality Cutoff { get; set; }
public string PreferredTags { get; set; }
public List<ProfileQualityItemResource> Items { get; set; }
public CustomFormatResource FormatCutoff { get; set; }
public int MinFormatScore { get; set; }
public int CutoffFormatScore { get; set; }
public List<ProfileFormatItemResource> FormatItems { get; set; }
public Language Language { get; set; }
}
@@ -29,7 +30,7 @@ namespace NzbDrone.Api.Profiles
public class ProfileFormatItemResource : RestResource
{
public CustomFormatResource Format { get; set; }
public bool Allowed { get; set; }
public int Score { get; set; }
}
public static class ProfileResourceMapper
@@ -60,23 +61,6 @@ namespace NzbDrone.Api.Profiles
? cutoffItem.Quality
: cutoffItem.Items.First().Quality;
var formatCutoffItem = model.FormatItems.First(q =>
{
if (q.Id == model.FormatCutoff)
{
return true;
}
if (q.Format == null)
{
return false;
}
return q.Format.Id == model.FormatCutoff;
});
var formatCutoff = formatCutoffItem.Format;
return new ProfileResource
{
Id = model.Id,
@@ -100,7 +84,8 @@ namespace NzbDrone.Api.Profiles
return new List<ProfileQualityItemResource> { ToResource(i) };
}).ToList(),
FormatCutoff = formatCutoff.ToResource(),
MinFormatScore = model.MinFormatScore,
CutoffFormatScore = model.CutoffFormatScore,
FormatItems = model.FormatItems.ConvertAll(ToResource),
Language = model.Language
};
@@ -125,7 +110,7 @@ namespace NzbDrone.Api.Profiles
return new ProfileFormatItemResource
{
Format = model.Format.ToResource(),
Allowed = model.Allowed
Score = model.Score,
};
}
@@ -144,7 +129,8 @@ namespace NzbDrone.Api.Profiles
Cutoff = resource.Cutoff.Id,
PreferredTags = resource.PreferredTags.Split(',').ToList(),
Items = resource.Items.ConvertAll(ToModel),
FormatCutoff = resource.FormatCutoff.ToModel().Id,
MinFormatScore = resource.MinFormatScore,
CutoffFormatScore = resource.CutoffFormatScore,
FormatItems = resource.FormatItems.ConvertAll(ToModel),
Language = resource.Language
};
@@ -169,7 +155,7 @@ namespace NzbDrone.Api.Profiles
return new ProfileFormatItem
{
Format = resource.Format.ToModel(),
Allowed = resource.Allowed
Score = resource.Score
};
}
@@ -32,21 +32,18 @@ namespace NzbDrone.Api.Profiles
var formatItems = _formatService.All().Select(v => new ProfileFormatItem
{
Format = v,
Allowed = true
Score = 0
}).ToList();
formatItems.Insert(0, new ProfileFormatItem
var profile = new Profile
{
Format = CustomFormat.None,
Allowed = true
});
var profile = new Profile();
profile.Cutoff = Quality.Unknown.Id;
profile.Items = items;
profile.FormatCutoff = CustomFormat.None.Id;
profile.FormatItems = formatItems;
profile.Language = Language.English;
Cutoff = Quality.Unknown.Id,
Items = items,
MinFormatScore = 0,
CutoffFormatScore = 0,
FormatItems = formatItems,
Language = Language.English
};
return new List<ProfileResource> { profile.ToResource() };
}
@@ -1,130 +0,0 @@
using System.Collections.Generic;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Profiles;
using NzbDrone.Core.Test.CustomFormats;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Qualities
{
[TestFixture]
public class CustomFormatsComparerFixture : CoreTest
{
private CustomFormat _customFormat1;
private CustomFormat _customFormat2;
private CustomFormat _customFormat3;
private CustomFormat _customFormat4;
public CustomFormatsComparer Subject { get; set; }
[SetUp]
public void Setup()
{
}
private void GivenDefaultProfileWithFormats()
{
_customFormat1 = new CustomFormat("My Format 1", new LanguageSpecification { Value = (int)Language.English }) { Id = 1 };
_customFormat2 = new CustomFormat("My Format 2", new LanguageSpecification { Value = (int)Language.French }) { Id = 2 };
_customFormat3 = new CustomFormat("My Format 3", new LanguageSpecification { Value = (int)Language.Spanish }) { Id = 3 };
_customFormat4 = new CustomFormat("My Format 4", new LanguageSpecification { Value = (int)Language.Italian }) { Id = 4 };
CustomFormatsFixture.GivenCustomFormats(CustomFormat.None, _customFormat1, _customFormat2, _customFormat3, _customFormat4);
Subject = new CustomFormatsComparer(new Profile { Items = QualityFixture.GetDefaultQualities(), FormatItems = CustomFormatsFixture.GetSampleFormatItems() });
}
[Test]
public void should_be_lesser_when_first_format_is_worse()
{
GivenDefaultProfileWithFormats();
var first = new List<CustomFormat> { _customFormat1 };
var second = new List<CustomFormat> { _customFormat2 };
var compare = Subject.Compare(first, second);
compare.Should().BeLessThan(0);
}
[Test]
public void should_be_zero_when_formats_are_equal()
{
GivenDefaultProfileWithFormats();
var first = new List<CustomFormat> { _customFormat2 };
var second = new List<CustomFormat> { _customFormat2 };
var compare = Subject.Compare(first, second);
compare.Should().Be(0);
}
[Test]
public void should_be_greater_when_first_format_is_better()
{
GivenDefaultProfileWithFormats();
var first = new List<CustomFormat> { _customFormat3 };
var second = new List<CustomFormat> { _customFormat2 };
var compare = Subject.Compare(first, second);
compare.Should().BeGreaterThan(0);
}
[Test]
public void should_be_greater_when_multiple_formats_better()
{
GivenDefaultProfileWithFormats();
var first = new List<CustomFormat> { _customFormat3, _customFormat4 };
var second = new List<CustomFormat> { _customFormat2 };
var compare = Subject.Compare(first, second);
compare.Should().BeGreaterThan(0);
}
[Test]
public void should_be_greater_when_best_format_is_better()
{
GivenDefaultProfileWithFormats();
var first = new List<CustomFormat> { _customFormat1, _customFormat3 };
var second = new List<CustomFormat> { _customFormat2 };
var compare = Subject.Compare(first, second);
compare.Should().BeGreaterThan(0);
}
[Test]
public void should_be_greater_when_best_format_equal_but_more_lower_formats()
{
GivenDefaultProfileWithFormats();
var first = new List<CustomFormat> { _customFormat1, _customFormat2 };
var second = new List<CustomFormat> { _customFormat2 };
var compare = Subject.Compare(first, second);
compare.Should().BeGreaterThan(0);
}
[Test]
public void should_not_be_greater_when_best_format_worse_but_more_lower_formats()
{
GivenDefaultProfileWithFormats();
var first = new List<CustomFormat> { _customFormat1, _customFormat2, _customFormat3 };
var second = new List<CustomFormat> { _customFormat4 };
var compare = Subject.Compare(first, second);
compare.Should().BeLessThan(0);
}
}
}
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using NzbDrone.Core.CustomFormats;
@@ -19,19 +20,15 @@ namespace NzbDrone.Core.Test.CustomFormats
public static List<ProfileFormatItem> GetSampleFormatItems(params string[] allowed)
{
return _customFormats.Select(f => new ProfileFormatItem { Format = f, Allowed = allowed.Contains(f.Name) }).ToList();
var allowedItems = _customFormats.Where(x => allowed.Contains(x.Name)).Select((f, index) => new ProfileFormatItem { Format = f, Score = (int)Math.Pow(2, index) }).ToList();
var disallowedItems = _customFormats.Where(x => !allowed.Contains(x.Name)).Select(f => new ProfileFormatItem { Format = f, Score = -1 * (int)Math.Pow(2, allowedItems.Count) });
return disallowedItems.Concat(allowedItems).ToList();
}
public static List<ProfileFormatItem> GetDefaultFormatItems()
{
return new List<ProfileFormatItem>
{
new ProfileFormatItem
{
Allowed = true,
Format = CustomFormat.None
}
};
return new List<ProfileFormatItem>();
}
}
}
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
@@ -32,8 +33,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
_format2.Id = 2;
var fakeSeries = Builder<Movie>.CreateNew()
.With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p.Id })
.Build();
.With(c => c.Profile = new Profile
{
Cutoff = Quality.Bluray1080p.Id,
MinFormatScore = 1
})
.Build();
_remoteMovie = new RemoteMovie
{
@@ -41,32 +46,38 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
ParsedMovieInfo = new ParsedMovieInfo { Quality = new QualityModel(Quality.DVD, new Revision(version: 2)) },
};
CustomFormatsFixture.GivenCustomFormats(CustomFormat.None, _format1, _format2);
CustomFormatsFixture.GivenCustomFormats(_format1, _format2);
}
[Test]
public void should_allow_if_format_is_defined_in_profile()
public void should_allow_if_format_score_greater_than_min()
{
_remoteMovie.CustomFormats = new List<CustomFormat> { _format1 };
_remoteMovie.Movie.Profile.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name);
_remoteMovie.CustomFormatScore = _remoteMovie.Movie.Profile.CalculateCustomFormatScore(_remoteMovie.CustomFormats);
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
}
[Test]
public void should_deny_if_format_is_defined_in_profile()
public void should_deny_if_format_score_not_greater_than_min()
{
_remoteMovie.CustomFormats = new List<CustomFormat> { _format2 };
_remoteMovie.Movie.Profile.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name);
_remoteMovie.CustomFormatScore = _remoteMovie.Movie.Profile.CalculateCustomFormatScore(_remoteMovie.CustomFormats);
Console.WriteLine(_remoteMovie.CustomFormatScore);
Console.WriteLine(_remoteMovie.Movie.Profile.MinFormatScore);
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
}
[Test]
public void should_deny_if_one_format_is_defined_in_profile()
public void should_deny_if_format_score_not_greater_than_min_2()
{
_remoteMovie.CustomFormats = new List<CustomFormat> { _format2, _format1 };
_remoteMovie.Movie.Profile.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name);
_remoteMovie.CustomFormatScore = _remoteMovie.Movie.Profile.CalculateCustomFormatScore(_remoteMovie.CustomFormats);
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
}
@@ -76,24 +87,28 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
_remoteMovie.CustomFormats = new List<CustomFormat> { _format2, _format1 };
_remoteMovie.Movie.Profile.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name, _format2.Name);
_remoteMovie.CustomFormatScore = _remoteMovie.Movie.Profile.CalculateCustomFormatScore(_remoteMovie.CustomFormats);
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
}
[Test]
public void should_deny_if_no_format_was_parsed_and_none_not_in_profile()
public void should_deny_if_no_format_was_parsed_and_min_score_positive()
{
_remoteMovie.CustomFormats = new List<CustomFormat> { };
_remoteMovie.Movie.Profile.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name, _format2.Name);
_remoteMovie.CustomFormatScore = _remoteMovie.Movie.Profile.CalculateCustomFormatScore(_remoteMovie.CustomFormats);
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
}
[Test]
public void should_allow_if_no_format_was_parsed_and_none_in_profile()
public void should_allow_if_no_format_was_parsed_min_score_is_zero()
{
_remoteMovie.CustomFormats = new List<CustomFormat> { };
_remoteMovie.Movie.Profile.FormatItems = CustomFormatsFixture.GetSampleFormatItems(CustomFormat.None.Name, _format1.Name, _format2.Name);
_remoteMovie.Movie.Profile.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name, _format2.Name);
_remoteMovie.Movie.Profile.MinFormatScore = 0;
_remoteMovie.CustomFormatScore = _remoteMovie.Movie.Profile.CalculateCustomFormatScore(_remoteMovie.CustomFormats);
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
}
@@ -40,9 +40,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
private void GivenProfile(Profile profile)
{
CustomFormatsFixture.GivenCustomFormats(CustomFormat.None);
profile.FormatItems = CustomFormatsFixture.GetSampleFormatItems("None");
profile.FormatCutoff = CustomFormat.None.Id;
CustomFormatsFixture.GivenCustomFormats();
profile.FormatItems = CustomFormatsFixture.GetSampleFormatItems();
profile.MinFormatScore = 0;
_remoteMovie.Movie.Profile = profile;
Console.WriteLine(profile.ToJson());
@@ -74,7 +74,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
_customFormat = new CustomFormat("My Format", new ResolutionSpecification { Value = (int)Resolution.R1080p }) { Id = 1 };
CustomFormatsFixture.GivenCustomFormats(_customFormat, CustomFormat.None);
CustomFormatsFixture.GivenCustomFormats(_customFormat);
}
[Test]
@@ -126,8 +126,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
Cutoff = Quality.HDTV720p.Id,
Items = Qualities.QualityFixture.GetDefaultQualities(),
FormatCutoff = CustomFormat.None.Id,
FormatItems = CustomFormatsFixture.GetSampleFormatItems("None", "My Format")
MinFormatScore = 0,
FormatItems = CustomFormatsFixture.GetSampleFormatItems("My Format")
});
GivenFileQuality(new QualityModel(Quality.HDTV720p));
@@ -135,7 +135,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
GivenCustomFormatHigher();
GivenOldCustomFormats(new List<CustomFormat> { CustomFormat.None });
GivenOldCustomFormats(new List<CustomFormat>());
GivenNewCustomFormats(new List<CustomFormat> { _customFormat });
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Mocker.Resolve<UpgradableSpecification>();
_upgradeHistory = Mocker.Resolve<HistorySpecification>();
CustomFormatsFixture.GivenCustomFormats(CustomFormat.None);
CustomFormatsFixture.GivenCustomFormats();
_fakeMovie = Builder<Movie>.CreateNew()
.With(c => c.Profile = new Profile
@@ -46,7 +46,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Items = Qualities.QualityFixture.GetDefaultQualities(),
Cutoff = Quality.Bluray1080p.Id,
FormatItems = CustomFormatsFixture.GetSampleFormatItems("None"),
FormatCutoff = CustomFormat.None.Id
MinFormatScore = 0
})
.Build();
@@ -162,8 +162,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
Items = Qualities.QualityFixture.GetDefaultQualities(),
Cutoff = Quality.Bluray1080p.Id,
FormatItems = CustomFormatsFixture.GetSampleFormatItems("None"),
FormatCutoff = CustomFormat.None.Id
FormatItems = CustomFormatsFixture.GetSampleFormatItems(),
MinFormatScore = 0
};
_parseResultSingle.ParsedMovieInfo.Quality = new QualityModel(Quality.WEBDL1080p, new Revision(version: 1));
@@ -185,8 +185,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
Items = Qualities.QualityFixture.GetDefaultQualities(),
Cutoff = Quality.WEBDL1080p.Id,
FormatItems = CustomFormatsFixture.GetSampleFormatItems("None"),
FormatCutoff = CustomFormat.None.Id
FormatItems = CustomFormatsFixture.GetSampleFormatItems(),
MinFormatScore = 0
};
_parseResultSingle.ParsedMovieInfo.Quality = new QualityModel(Quality.WEBDL1080p, new Revision(version: 1));
@@ -220,8 +220,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
Items = Qualities.QualityFixture.GetDefaultQualities(),
Cutoff = Quality.WEBDL1080p.Id,
FormatItems = CustomFormatsFixture.GetSampleFormatItems("None"),
FormatCutoff = CustomFormat.None.Id
FormatItems = CustomFormatsFixture.GetSampleFormatItems(),
MinFormatScore = 0
};
_parseResultSingle.ParsedMovieInfo.Quality = new QualityModel(Quality.Bluray1080p, new Revision(version: 1));
@@ -36,7 +36,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
_customFormat1 = new CustomFormat("My Format 1", new LanguageSpecification { Value = (int)Language.English }) { Id = 1 };
_customFormat2 = new CustomFormat("My Format 2", new LanguageSpecification { Value = (int)Language.French }) { Id = 2 };
CustomFormatsFixture.GivenCustomFormats(CustomFormat.None, _customFormat1, _customFormat2);
CustomFormatsFixture.GivenCustomFormats(_customFormat1, _customFormat2);
}
private RemoteMovie GivenRemoteMovie(QualityModel quality, int age = 0, long size = 0, DownloadProtocol downloadProtocol = DownloadProtocol.Usenet)
@@ -51,7 +51,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
Items = Qualities.QualityFixture.GetDefaultQualities(),
PreferredTags = new List<string> { "DTS-HD", "SPARKS" },
FormatItems = CustomFormatsFixture.GetSampleFormatItems()
FormatItems = CustomFormatsFixture.GetSampleFormatItems(_customFormat1.Name, _customFormat2.Name),
MinFormatScore = 0
})
.With(m => m.Title = "A Movie").Build();
@@ -62,6 +63,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
remoteMovie.Release.Title = "A Movie 1998";
remoteMovie.CustomFormats = new List<CustomFormat>();
remoteMovie.CustomFormatScore = 0;
return remoteMovie;
}
@@ -328,11 +330,11 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
var quality1 = new QualityModel(Quality.Bluray720p);
var remoteMovie1 = GivenRemoteMovie(quality1);
remoteMovie1.CustomFormats.Add(CustomFormat.None);
var quality2 = new QualityModel(Quality.Bluray720p);
var remoteMovie2 = GivenRemoteMovie(quality2);
remoteMovie2.CustomFormats.Add(_customFormat1);
remoteMovie2.CustomFormatScore = remoteMovie2.Movie.Profile.CalculateCustomFormatScore(remoteMovie2.CustomFormats);
var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteMovie1));
@@ -348,10 +350,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
var quality1 = new QualityModel(Quality.Bluray720p);
var remoteMovie1 = GivenRemoteMovie(quality1);
remoteMovie1.CustomFormats.Add(_customFormat1);
remoteMovie1.CustomFormatScore = remoteMovie1.Movie.Profile.CalculateCustomFormatScore(remoteMovie1.CustomFormats);
var quality2 = new QualityModel(Quality.Bluray720p);
var remoteMovie2 = GivenRemoteMovie(quality2);
remoteMovie2.CustomFormats.Add(_customFormat2);
remoteMovie2.CustomFormatScore = remoteMovie2.Movie.Profile.CalculateCustomFormatScore(remoteMovie2.CustomFormats);
var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteMovie1));
@@ -367,10 +371,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
var quality1 = new QualityModel(Quality.Bluray720p);
var remoteMovie1 = GivenRemoteMovie(quality1);
remoteMovie1.CustomFormats.Add(_customFormat1);
remoteMovie1.CustomFormatScore = remoteMovie1.Movie.Profile.CalculateCustomFormatScore(remoteMovie1.CustomFormats);
var quality2 = new QualityModel(Quality.Bluray720p);
var remoteMovie2 = GivenRemoteMovie(quality2);
remoteMovie2.CustomFormats.AddRange(new List<CustomFormat> { _customFormat1, _customFormat2 });
remoteMovie2.CustomFormatScore = remoteMovie2.Movie.Profile.CalculateCustomFormatScore(remoteMovie2.CustomFormats);
var decisions = new List<DownloadDecision>();
decisions.Add(new DownloadDecision(remoteMovie1));
@@ -48,7 +48,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[SetUp]
public void Setup()
{
CustomFormatsFixture.GivenCustomFormats(CustomFormat.None, _customFormat1, _customFormat2);
CustomFormatsFixture.GivenCustomFormats(_customFormat1, _customFormat2);
}
private void GivenAutoDownloadPropers(bool autoDownloadPropers)
@@ -73,7 +73,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
var profile = new Profile
{
Items = Qualities.QualityFixture.GetDefaultQualities(),
FormatItems = CustomFormatsFixture.GetSampleFormatItems()
FormatItems = CustomFormatsFixture.GetSampleFormatItems(_customFormat1.Name, _customFormat2.Name),
MinFormatScore = 0
};
Subject.IsUpgradable(profile,
@@ -29,14 +29,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
Mocker.Resolve<UpgradableSpecification>();
CustomFormatsFixture.GivenCustomFormats(CustomFormat.None);
CustomFormatsFixture.GivenCustomFormats();
_movie = Builder<Movie>.CreateNew()
.With(e => e.Profile = new Profile
{
Items = Qualities.QualityFixture.GetDefaultQualities(),
FormatItems = CustomFormatsFixture.GetSampleFormatItems("None"),
FormatCutoff = CustomFormat.None.Id,
FormatItems = CustomFormatsFixture.GetSampleFormatItems(),
MinFormatScore = 0,
UpgradeAllowed = true
})
.Build();
@@ -48,7 +48,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
_remoteMovie = Builder<RemoteMovie>.CreateNew()
.With(r => r.Movie = _movie)
.With(r => r.ParsedMovieInfo = new ParsedMovieInfo { Quality = new QualityModel(Quality.DVD) })
.With(x => x.CustomFormats = new List<CustomFormat> { CustomFormat.None })
.With(x => x.CustomFormats = new List<CustomFormat>())
.Build();
Mocker.GetMock<ICustomFormatCalculationService>()
@@ -104,7 +104,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
Quality = new QualityModel(Quality.SDTV)
})
.With(x => x.CustomFormats = new List<CustomFormat> { CustomFormat.None })
.With(x => x.CustomFormats = new List<CustomFormat>())
.Build();
GivenQueue(new List<RemoteMovie> { remoteMovie });
@@ -31,7 +31,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Mocker.Resolve<UpgradableSpecification>();
_upgradeDisk = Mocker.Resolve<UpgradeDiskSpecification>();
CustomFormatsFixture.GivenCustomFormats(CustomFormat.None);
CustomFormatsFixture.GivenCustomFormats();
_firstFile = new MovieFile { Quality = new QualityModel(Quality.Bluray1080p, new Revision(version: 2)), DateAdded = DateTime.Now };
@@ -39,8 +39,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
.With(c => c.Profile = new Profile
{
Cutoff = Quality.Bluray1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities(),
FormatItems = CustomFormatsFixture.GetSampleFormatItems("None"),
FormatCutoff = CustomFormat.None.Id
FormatItems = CustomFormatsFixture.GetSampleFormatItems(),
MinFormatScore = 0
})
.With(e => e.MovieFile = _firstFile)
.Build();
@@ -36,7 +36,7 @@ namespace NzbDrone.Core.Test.MovieTests.MovieRepositoryTests
{
Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p),
FormatItems = CustomFormatsFixture.GetDefaultFormatItems(),
FormatCutoff = CustomFormat.None.Id,
MinFormatScore = 0,
Cutoff = Quality.Bluray1080p.Id,
Name = "TestProfile"
};
@@ -22,7 +22,7 @@ namespace NzbDrone.Core.Test.Profiles
var profile = new Profile
{
Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p),
FormatCutoff = CustomFormat.None.Id,
MinFormatScore = 0,
FormatItems = CustomFormatsFixture.GetDefaultFormatItems(),
Cutoff = Quality.Bluray1080p.Id,
Name = "TestProfile"
@@ -17,13 +17,6 @@ namespace NzbDrone.Core.CustomFormats
Specifications = specs.ToList();
}
public static CustomFormat None => new CustomFormat
{
Id = 0,
Name = "None",
Specifications = new List<ICustomFormatSpecification>()
};
public string Name { get; set; }
public List<ICustomFormatSpecification> Specifications { get; set; }
@@ -1,34 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Core.Profiles;
namespace NzbDrone.Core.CustomFormats
{
public class CustomFormatsComparer : IComparer<List<CustomFormat>>
{
private readonly Profile _profile;
public CustomFormatsComparer(Profile profile)
{
Ensure.That(profile, () => profile).IsNotNull();
Ensure.That(profile.Items, () => profile.Items).HasItems();
_profile = profile;
}
public int Compare(List<CustomFormat> left, List<CustomFormat> right)
{
var leftIndicies = _profile.GetIndices(left);
var rightIndicies = _profile.GetIndices(right);
// Summing powers of two ensures last format always trumps, but we order correctly if we
// have extra formats lower down the list
var leftTotal = leftIndicies.Select(x => Math.Pow(2, x)).Sum();
var rightTotal = rightIndicies.Select(x => Math.Pow(2, x)).Sum();
return leftTotal.CompareTo(rightTotal);
}
}
}
@@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using Dapper;
using FluentMigrator;
using NzbDrone.Core.Datastore.Converters;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(169)]
public class custom_format_scores : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Profiles").AddColumn("MinFormatScore").AsInt32().WithDefaultValue(0);
Alter.Table("Profiles").AddColumn("CutoffFormatScore").AsInt32().WithDefaultValue(0);
Execute.WithConnection(MigrateOrderToScores);
Delete.Column("FormatCutoff").FromTable("Profiles");
}
private void MigrateOrderToScores(IDbConnection conn, IDbTransaction tran)
{
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<ProfileFormatItem168>>());
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<ProfileFormatItem169>>());
var rows = conn.Query<Profile168>("SELECT Id, FormatCutoff, FormatItems from Profiles", transaction: tran);
var newRows = new List<Profile169>();
foreach (var row in rows)
{
// Things ranked less than None should have a negative score
// Things ranked higher than None have a positive score
var allowedBelowNone = new List<ProfileFormatItem168>();
var allowedAboveNone = new List<ProfileFormatItem168>();
var disallowed = new List<ProfileFormatItem168>();
var noneEnabled = row.FormatItems.Single(x => x.Format == 0).Allowed;
// If none was disabled, we count everything as above none
var foundNone = !noneEnabled;
foreach (var item in row.FormatItems)
{
if (item.Format == 0)
{
foundNone = true;
}
else if (!item.Allowed)
{
disallowed.Add(item);
}
else if (foundNone)
{
allowedAboveNone.Add(item);
}
else
{
allowedBelowNone.Add(item);
}
}
// Set up allowed with scores 1, 2, 4, 8 etc so they replicate existing ranking behaviour
var allowedPositive = allowedAboveNone.Select((x, index) => new ProfileFormatItem169
{
Format = x.Format,
Score = (int)Math.Pow(2, index)
}).ToList();
// reverse so we have most wanted first
allowedBelowNone.Reverse();
var allowedNegative = allowedBelowNone.Select((x, index) => new ProfileFormatItem169
{
Format = x.Format,
Score = -1 * (int)Math.Pow(2, index)
}).ToList();
// The minimum format score should be the minimum score achievable by the allowed formats
// By construction, if None disabled then allowedNegative is empty and min is 1
// If none was enabled, we could have some below None (with negative score) and
// we should set min score negative to allow for these
// If someone had no allowed formats and none disabled then keep minScore at 0
// (This was a broken config that meant nothing would download)
var minScore = 0;
if (allowedPositive.Any() && !noneEnabled)
{
minScore = 1;
}
else if (allowedNegative.Any())
{
minScore = ((int)Math.Pow(2, allowedNegative.Count) * -1) + 1;
}
// Previously anything matching a disabled format was banned from downloading
// To replicate this, set score negative enough that matching a disabled format
// must produce a score below the minimum
var disallowedScore = (-1 * (int)Math.Pow(2, allowedPositive.Count)) + Math.Max(minScore, 0);
var newDisallowed = disallowed.Select(x => new ProfileFormatItem169
{
Format = x.Format,
Score = disallowedScore
});
var newItems = newDisallowed.Concat(allowedNegative).Concat(allowedPositive).OrderBy(x => x.Score).ToList();
// Set the cutoff score to be the score associated with old cutoff format.
// This can never be achieved by any combination of lesser formats given the 2^n scoring scheme
// If the cutoff is None (Id == 0) then set cutoff score to zero
var cutoffScore = 0;
if (row.FormatCutoff != 0)
{
cutoffScore = newItems.Single(x => x.Format == row.FormatCutoff).Score;
}
newRows.Add(new Profile169
{
Id = row.Id,
MinFormatScore = minScore,
CutoffFormatScore = cutoffScore,
FormatItems = newItems
});
}
var sql = $"UPDATE Profiles SET MinFormatScore = @MinFormatScore, CutoffFormatScore = @CutoffFormatScore, FormatItems = @FormatItems WHERE Id = @Id";
conn.Execute(sql, newRows, transaction: tran);
}
private class Profile168 : ModelBase
{
public int FormatCutoff { get; set; }
public List<ProfileFormatItem168> FormatItems { get; set; }
}
private class ProfileFormatItem168
{
public int Format { get; set; }
public bool Allowed { get; set; }
}
private class Profile169 : ModelBase
{
public int MinFormatScore { get; set; }
public int CutoffFormatScore { get; set; }
public List<ProfileFormatItem169> FormatItems { get; set; }
}
private class ProfileFormatItem169
{
public int Format { get; set; }
public int Score { get; set; }
}
}
}
@@ -55,7 +55,7 @@ namespace NzbDrone.Core.DecisionEngine
private int CompareQuality(DownloadDecision x, DownloadDecision y)
{
return CompareAll(CompareBy(x.RemoteMovie, y.RemoteMovie, remoteMovie => remoteMovie.Movie.Profile.GetIndex(remoteMovie.ParsedMovieInfo.Quality.Quality)),
CompareBy(x.RemoteMovie, y.RemoteMovie, remoteMovie => remoteMovie.Movie.Profile.GetIndices(remoteMovie.CustomFormats).Select(i => Math.Pow(2, i)).Sum()),
CompareBy(x.RemoteMovie, y.RemoteMovie, remoteMovie => remoteMovie.CustomFormatScore),
CompareBy(x.RemoteMovie, y.RemoteMovie, remoteMovie => remoteMovie.ParsedMovieInfo.Quality.Revision.Real),
CompareBy(x.RemoteMovie, y.RemoteMovie, remoteMovie => remoteMovie.ParsedMovieInfo.Quality.Revision.Version));
}
@@ -108,6 +108,7 @@ namespace NzbDrone.Core.DecisionEngine
result.ReleaseName = report.Title;
var remoteMovie = result.RemoteMovie;
remoteMovie.CustomFormats = _formatCalculator.ParseCustomFormat(parsedMovieInfo);
remoteMovie.CustomFormatScore = remoteMovie?.Movie?.Profile?.CalculateCustomFormatScore(remoteMovie.CustomFormats) ?? 0;
remoteMovie.Release = report;
remoteMovie.MappingResult = result.MappingResultType;
@@ -1,8 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
@@ -10,26 +6,17 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
{
public class CustomFormatAllowedbyProfileSpecification : IDecisionEngineSpecification
{
private readonly Logger _logger;
public CustomFormatAllowedbyProfileSpecification(Logger logger)
{
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
{
var formats = subject.CustomFormats.Any() ? subject.CustomFormats : new List<CustomFormat> { CustomFormat.None };
_logger.Debug("Checking if report meets custom format requirements. {0}", formats.ConcatToString());
var notAllowedFormats = subject.Movie.Profile.FormatItems.Where(v => v.Allowed == false).Select(f => f.Format).ToList();
var notWantedFormats = notAllowedFormats.Intersect(formats);
if (notWantedFormats.Any())
var minScore = subject.Movie.Profile.MinFormatScore;
var score = subject.CustomFormatScore;
if (score < minScore)
{
_logger.Debug("Custom Formats {0} rejected by Movie's profile", notWantedFormats.ConcatToString());
return Decision.Reject("Custom Formats {0} not wanted in profile", notWantedFormats.ConcatToString());
return Decision.Reject("Custom Formats {0} have score {1} below Movie's minimum {2}", subject.CustomFormats.ConcatToString(), score, minScore);
}
return Decision.Accept();
@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
@@ -55,9 +54,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
return true;
}
var customFormatCompare = new CustomFormatsComparer(profile).Compare(newCustomFormats, currentCustomFormats);
var currentFormatScore = profile.CalculateCustomFormatScore(currentCustomFormats);
var newFormatScore = profile.CalculateCustomFormatScore(newCustomFormats);
if (customFormatCompare <= 0)
if (newFormatScore <= currentFormatScore)
{
_logger.Debug("New item's custom formats [{0}] do not improve on [{1}], skipping",
newCustomFormats.ConcatToString(),
@@ -88,15 +88,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
private bool CustomFormatCutoffNotMet(Profile profile, List<CustomFormat> currentFormats)
{
var cutoff = new List<CustomFormat> { profile.FormatItems.Single(x => x.Format.Id == profile.FormatCutoff).Format };
var cutoffCompare = new CustomFormatsComparer(profile).Compare(currentFormats, cutoff);
if (cutoffCompare < 0)
{
return true;
}
return false;
var score = profile.CalculateCustomFormatScore(currentFormats);
return score < profile.CutoffFormatScore;
}
public bool CutoffNotMet(Profile profile, QualityModel currentQuality, List<CustomFormat> currentFormats, QualityModel newQuality = null)
@@ -120,7 +113,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
public bool IsUpgradeAllowed(Profile qualityProfile, QualityModel currentQuality, List<CustomFormat> currentCustomFormats, QualityModel newQuality, List<CustomFormat> newCustomFormats)
{
var isQualityUpgrade = new QualityModelComparer(qualityProfile).Compare(newQuality, currentQuality) > 0;
var isCustomFormatUpgrade = new CustomFormatsComparer(qualityProfile).Compare(newCustomFormats, currentCustomFormats) > 0;
var isCustomFormatUpgrade = qualityProfile.CalculateCustomFormatScore(newCustomFormats) > qualityProfile.CalculateCustomFormatScore(currentCustomFormats);
if ((isQualityUpgrade || isCustomFormatUpgrade) && qualityProfile.UpgradeAllowed)
{
@@ -10,6 +10,7 @@ namespace NzbDrone.Core.Parser.Model
public ReleaseInfo Release { get; set; }
public ParsedMovieInfo ParsedMovieInfo { get; set; }
public List<CustomFormat> CustomFormats { get; set; }
public int CustomFormatScore { get; set; }
public Movie Movie { get; set; }
public MappingResultType MappingResult { get; set; }
public bool DownloadAllowed { get; set; }
+4 -4
View File
@@ -17,7 +17,8 @@ namespace NzbDrone.Core.Profiles
public string Name { get; set; }
public int Cutoff { get; set; }
public List<ProfileQualityItem> Items { get; set; }
public int FormatCutoff { get; set; }
public int MinFormatScore { get; set; }
public int CutoffFormatScore { get; set; }
public List<ProfileFormatItem> FormatItems { get; set; }
public List<string> PreferredTags { get; set; }
public Language Language { get; set; }
@@ -75,10 +76,9 @@ namespace NzbDrone.Core.Profiles
return new QualityIndex();
}
public List<int> GetIndices(List<CustomFormat> formats)
public int CalculateCustomFormatScore(List<CustomFormat> formats)
{
var allFormats = formats.Any() ? formats : new List<CustomFormat> { CustomFormat.None };
return allFormats.Select(f => FormatItems.FindIndex(v => Equals(v.Format, f))).ToList();
return FormatItems.Where(x => formats.Contains(x.Format)).Sum(x => x.Score);
}
}
}
@@ -9,6 +9,6 @@ namespace NzbDrone.Core.Profiles
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public int Id { get; set; }
public CustomFormat Format { get; set; }
public bool Allowed { get; set; }
public int Score { get; set; }
}
}
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Profiles
{
foreach (var formatItem in profile.FormatItems)
{
formatItem.Format = formatItem.Format.Id == 0 ? CustomFormat.None : cfs[formatItem.Format.Id];
formatItem.Format = cfs[formatItem.Format.Id];
}
}
+9 -15
View File
@@ -89,7 +89,7 @@ namespace NzbDrone.Core.Profiles
{
profile.FormatItems.Insert(0, new ProfileFormatItem
{
Allowed = false,
Score = 0,
Format = message.CustomFormat
});
@@ -103,9 +103,10 @@ namespace NzbDrone.Core.Profiles
foreach (var profile in all)
{
profile.FormatItems = profile.FormatItems.Where(c => c.Format.Id != message.CustomFormat.Id).ToList();
if (profile.FormatCutoff == message.CustomFormat.Id)
if (!profile.FormatItems.Any())
{
profile.FormatCutoff = CustomFormat.None.Id;
profile.MinFormatScore = 0;
profile.CutoffFormatScore = 0;
}
Update(profile);
@@ -243,20 +244,12 @@ namespace NzbDrone.Core.Profiles
groupId++;
}
var formatItems = new List<ProfileFormatItem>
{
new ProfileFormatItem
{
Id = 0,
Allowed = true,
Format = CustomFormat.None
}
}.Concat(_formatService.All().Select(format => new ProfileFormatItem
var formatItems = _formatService.All().Select(format => new ProfileFormatItem
{
Id = format.Id,
Allowed = false,
Score = 0,
Format = format
})).ToList();
}).ToList();
var qualityProfile = new Profile
{
@@ -264,7 +257,8 @@ namespace NzbDrone.Core.Profiles
Cutoff = profileCutoff,
Items = items,
Language = Language.English,
FormatCutoff = CustomFormat.None.Id,
MinFormatScore = 0,
CutoffFormatScore = 0,
FormatItems = formatItems
};
@@ -94,7 +94,7 @@ namespace Radarr.Api.V3.CustomFormats
yield return new ReleaseTitleSpecification
{
Name = "Simple Hardcoded Subs",
Value = @"C_RX_subs?"
Value = @"subs?"
};
yield return new ReleaseTitleSpecification
@@ -34,11 +34,6 @@ namespace Radarr.Api.V3.CustomFormats
public static CustomFormat ToModel(this CustomFormatResource resource, List<ICustomFormatSpecification> specifications)
{
if (resource.Id == 0 && resource.Name == "None")
{
return CustomFormat.None;
}
return new CustomFormat
{
Id = resource.Id,
@@ -17,6 +17,7 @@ namespace Radarr.Api.V3.Indexers
public string Guid { get; set; }
public QualityModel Quality { get; set; }
public List<CustomFormatResource> CustomFormats { get; set; }
public int CustomFormatScore { get; set; }
public int QualityWeight { get; set; }
public int Age { get; set; }
public double AgeHours { get; set; }
@@ -71,6 +72,7 @@ namespace Radarr.Api.V3.Indexers
Guid = releaseInfo.Guid,
Quality = parsedMovieInfo.Quality,
CustomFormats = remoteMovie.CustomFormats.ToResource(),
CustomFormatScore = remoteMovie.CustomFormatScore,
//QualityWeight
Age = releaseInfo.Age,
@@ -26,13 +26,17 @@ namespace Radarr.Api.V3.Profiles.Quality
SharedValidator.RuleFor(c => c.FormatItems).Must(items =>
{
var all = _formatService.All().Select(f => f.Id).ToList();
all.Add(CustomFormat.None.Id);
var ids = items.Select(i => i.Format);
return all.Except(ids).Empty();
}).WithMessage("All Custom Formats and no extra ones need to be present inside your Profile! Try refreshing your browser.");
SharedValidator.RuleFor(c => c.FormatCutoff)
.Must(c => _formatService.All().Select(f => f.Id).Contains(c) || c == CustomFormat.None.Id).WithMessage("The Custom Format Cutoff must be a valid Custom Format! Try refreshing your browser.");
SharedValidator.RuleFor(c => c).Custom((profile, context) =>
{
if (profile.FormatItems.Sum(x => x.Score) < profile.MinFormatScore)
{
context.AddFailure("Minimum Custom Format Score can never be satisfied");
}
});
GetResourceAll = GetAll;
GetResourceById = GetById;
@@ -15,29 +15,30 @@ namespace Radarr.Api.V3.Profiles.Quality
public int Cutoff { get; set; }
public string PreferredTags { get; set; }
public List<QualityProfileQualityItemResource> Items { get; set; }
public int FormatCutoff { get; set; }
public int MinFormatScore { get; set; }
public int CutoffFormatScore { get; set; }
public List<ProfileFormatItemResource> FormatItems { get; set; }
public Language Language { get; set; }
}
public class QualityProfileQualityItemResource : RestResource
{
public string Name { get; set; }
public NzbDrone.Core.Qualities.Quality Quality { get; set; }
public List<QualityProfileQualityItemResource> Items { get; set; }
public bool Allowed { get; set; }
public QualityProfileQualityItemResource()
{
Items = new List<QualityProfileQualityItemResource>();
}
public string Name { get; set; }
public NzbDrone.Core.Qualities.Quality Quality { get; set; }
public List<QualityProfileQualityItemResource> Items { get; set; }
public bool Allowed { get; set; }
}
public class ProfileFormatItemResource : RestResource
{
public int Format { get; set; }
public string Name { get; set; }
public bool Allowed { get; set; }
public int Score { get; set; }
}
public static class ProfileResourceMapper
@@ -57,7 +58,8 @@ namespace Radarr.Api.V3.Profiles.Quality
Cutoff = model.Cutoff,
PreferredTags = model.PreferredTags != null ? string.Join(",", model.PreferredTags) : "",
Items = model.Items.ConvertAll(ToResource),
FormatCutoff = model.FormatCutoff,
MinFormatScore = model.MinFormatScore,
CutoffFormatScore = model.CutoffFormatScore,
FormatItems = model.FormatItems.ConvertAll(ToResource),
Language = model.Language
};
@@ -86,7 +88,7 @@ namespace Radarr.Api.V3.Profiles.Quality
{
Format = model.Format.Id,
Name = model.Format.Name,
Allowed = model.Allowed
Score = model.Score
};
}
@@ -105,7 +107,8 @@ namespace Radarr.Api.V3.Profiles.Quality
Cutoff = resource.Cutoff,
PreferredTags = resource.PreferredTags.Split(',').ToList(),
Items = resource.Items.ConvertAll(ToModel),
FormatCutoff = resource.FormatCutoff,
MinFormatScore = resource.MinFormatScore,
CutoffFormatScore = resource.CutoffFormatScore,
FormatItems = resource.FormatItems.ConvertAll(ToModel),
Language = resource.Language
};
@@ -133,7 +136,7 @@ namespace Radarr.Api.V3.Profiles.Quality
return new ProfileFormatItem
{
Format = new CustomFormat { Id = resource.Format },
Allowed = resource.Allowed
Score = resource.Score
};
}