diff --git a/src/NzbDrone.Api/Profiles/ProfileModule.cs b/src/NzbDrone.Api/Profiles/ProfileModule.cs index aadef54f8..fdab96f9f 100644 --- a/src/NzbDrone.Api/Profiles/ProfileModule.cs +++ b/src/NzbDrone.Api/Profiles/ProfileModule.cs @@ -1,8 +1,7 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.Profiles.Qualities; using Sonarr.Http; -using Sonarr.Http.Mapping; namespace NzbDrone.Api.Profiles { @@ -53,4 +52,4 @@ namespace NzbDrone.Api.Profiles return _profileService.All().ToResource(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Profiles/ProfileResource.cs b/src/NzbDrone.Api/Profiles/ProfileResource.cs index 0365e0935..7c47a9a61 100644 --- a/src/NzbDrone.Api/Profiles/ProfileResource.cs +++ b/src/NzbDrone.Api/Profiles/ProfileResource.cs @@ -1,8 +1,7 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using NzbDrone.Common.Extensions; using Sonarr.Http.REST; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Profiles; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; @@ -27,13 +26,41 @@ namespace NzbDrone.Api.Profiles { if (model == null) return null; + var cutoffItem = model.Items.First(q => + { + if (q.Id == model.Cutoff) return true; + + if (q.Quality == null) return false; + + return q.Quality.Id == model.Cutoff; + }); + + var cutoff = cutoffItem.Items == null || cutoffItem.Items.Empty() + ? cutoffItem.Quality + : cutoffItem.Items.First().Quality; + return new ProfileResource { Id = model.Id, Name = model.Name, - Cutoff = model.Cutoff, - Items = model.Items.ConvertAll(ToResource) + Cutoff = cutoff, + + // Flatten groups so things don't explode + Items = model.Items.SelectMany(i => + { + if (i == null) + { + return null; + } + + if (i.Items.Any()) + { + return i.Items.ConvertAll(ToResource); + } + + return new List {ToResource(i)}; + }).ToList() }; } @@ -57,7 +84,7 @@ namespace NzbDrone.Api.Profiles Id = resource.Id, Name = resource.Name, - Cutoff = (Quality)resource.Cutoff.Id, + Cutoff = resource.Cutoff.Id, Items = resource.Items.ConvertAll(ToModel) }; } @@ -78,4 +105,4 @@ namespace NzbDrone.Api.Profiles return models.Select(ToResource).ToList(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs b/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs index b628948b3..085d9fb97 100644 --- a/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs +++ b/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs @@ -27,7 +27,7 @@ namespace NzbDrone.Api.Profiles .ToList(); var profile = new Profile(); - profile.Cutoff = Quality.Unknown; + profile.Cutoff = Quality.Unknown.Id; profile.Items = items; return new List { profile.ToResource() }; diff --git a/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs b/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs index b3df3828c..3c5cd7964 100644 --- a/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Core.Test.Datastore var profile = new Profile { Name = "Test", - Cutoff = Quality.WEBDL720p, + Cutoff = Quality.WEBDL720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities(), }; diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/117_add_webrip_qualites_in_profileFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/117_add_webrip_qualites_in_profileFixture.cs new file mode 100644 index 000000000..31ad9a7ae --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Migration/117_add_webrip_qualites_in_profileFixture.cs @@ -0,0 +1,120 @@ +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Datastore.Migration; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Migration +{ + [TestFixture] + public class add_webrip_and_br480_qualites_in_profileFixture : MigrationTest + { + private string GenerateQualityJson(int quality, bool allowed) + { + return $"{{ \"quality\": {quality}, \"allowed\": {allowed.ToString().ToLowerInvariant()} }}"; + } + + [Test] + public void should_add_webrip_qualities_and_group_with_webdl() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Profiles").Row(new + { + Id = 0, + Name = "SDTV", + Cutoff = 1, + Items = $"[{GenerateQualityJson(1, true)}, {GenerateQualityJson((int)Quality.WEBRip480p, false)}, {GenerateQualityJson((int)Quality.WEBRip720p, false)}, {GenerateQualityJson((int)Quality.WEBRip1080p, false)}, {GenerateQualityJson((int)Quality.WEBRip2160p, false)}]" + }); + }); + + var profiles = db.Query("SELECT Items FROM Profiles LIMIT 1"); + + var items = profiles.First().Items; + items.Should().HaveCount(6); + items.Select(v => v.Quality).Should().BeEquivalentTo(1, null, null, null, null, null); + items.Select(v => v.Items.Count).Should().BeEquivalentTo(0, 2, 2, 2, 2, 2); + items.Select(v => v.Allowed).Should().BeEquivalentTo(true, false, false, false, false, false); + } + + [Test] + public void should_add_bluray480p_quality_and_group_with_dvd() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Profiles").Row(new + { + Id = 0, + Name = "SDTV", + Cutoff = 1, + Items = $"[{GenerateQualityJson(1, true)}, {GenerateQualityJson((int)Quality.DVD, false)}, {GenerateQualityJson((int)Quality.Bluray480p, false)}]" + }); + }); + + var profiles = db.Query("SELECT Items FROM Profiles LIMIT 1"); + + var items = profiles.First().Items; + items.Should().HaveCount(6); + items.Select(v => v.Quality).Should().BeEquivalentTo(1, null, null, null, null, null); + items.Select(v => v.Items.Count).Should().BeEquivalentTo(0, 2, 2, 2, 2, 2); + items.Select(v => v.Allowed).Should().BeEquivalentTo(true, false, false, false, false, false); + } + + [Test] + public void should_add_webrip_and_webdl_if_webdl_is_missing() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Profiles").Row(new + { + Id = 0, + Name = "SDTV", + Cutoff = 1, + Items = $"[{GenerateQualityJson(1, true)}, {GenerateQualityJson((int)Quality.WEBRip480p, false)}, {GenerateQualityJson((int)Quality.WEBRip720p, false)}, {GenerateQualityJson((int)Quality.WEBRip1080p, false)}]" + }); + }); + + var profiles = db.Query("SELECT Items FROM Profiles LIMIT 1"); + + var items = profiles.First().Items; + items.Should().HaveCount(6); + items.Select(v => v.Quality).Should().BeEquivalentTo(1, null, null, null, null, null); + items.Select(v => v.Items.Count).Should().BeEquivalentTo(0, 2, 2, 2, 2, 2); + items.Select(v => v.Allowed).Should().BeEquivalentTo(true, false, false, false, false, false); + } + + [Test] + public void should_group_webrip_and_webdl_with_the_same_resolution() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Profiles").Row(new + { + Id = 0, + Name = "SDTV", + Cutoff = 1, + Items = $"[{GenerateQualityJson(1, true)}, {GenerateQualityJson((int)Quality.WEBRip480p, false)}, {GenerateQualityJson((int)Quality.WEBRip720p, false)}, {GenerateQualityJson((int)Quality.WEBRip1080p, false)}, {GenerateQualityJson((int)Quality.WEBRip2160p, false)}]" + }); + }); + + var profiles = db.Query("SELECT Items FROM Profiles LIMIT 1"); + var items = profiles.First().Items; + + items[1].Items.First().Quality.Should().Be((int)Quality.WEBRip480p); + items[1].Items.Last().Quality.Should().Be((int)Quality.WEBDL480p); + + items[2].Items.First().Quality.Should().Be((int)Quality.DVD); + items[2].Items.Last().Quality.Should().Be((int)Quality.Bluray480p); + + items[3].Items.First().Quality.Should().Be((int)Quality.WEBRip720p); + items[3].Items.Last().Quality.Should().Be((int)Quality.WEBDL720p); + + items[4].Items.First().Quality.Should().Be((int)Quality.WEBRip1080p); + items[4].Items.Last().Quality.Should().Be((int)Quality.WEBDL1080p); + + items[5].Items.First().Quality.Should().Be((int)Quality.WEBRip2160p); + items[5].Items.Last().Quality.Should().Be((int)Quality.WEBDL2160p); + } + } +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs index 8729c33c1..fa37b8b66 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.CutoffNotMet( new Profile { - Cutoff = Quality.Bluray1080p, + Cutoff = Quality.Bluray1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }, new LanguageProfile @@ -36,7 +36,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.CutoffNotMet( new Profile { - Cutoff = Quality.HDTV720p, + Cutoff = Quality.HDTV720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }, new LanguageProfile @@ -53,7 +53,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.CutoffNotMet( new Profile { - Cutoff = Quality.HDTV720p, + Cutoff = Quality.HDTV720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }, new LanguageProfile @@ -70,7 +70,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.CutoffNotMet( new Profile { - Cutoff = Quality.HDTV720p, + Cutoff = Quality.HDTV720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }, new LanguageProfile @@ -89,7 +89,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.CutoffNotMet( new Profile { - Cutoff = Quality.HDTV720p, + Cutoff = Quality.HDTV720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }, new LanguageProfile @@ -108,7 +108,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Profile _profile = new Profile { - Cutoff = Quality.HDTV720p, + Cutoff = Quality.HDTV720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities(), }; @@ -131,7 +131,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Profile _profile = new Profile { - Cutoff = Quality.HDTV720p, + Cutoff = Quality.HDTV720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities(), }; @@ -155,7 +155,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Profile _profile = new Profile { - Cutoff = Quality.HDTV720p, + Cutoff = Quality.HDTV720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities(), }; @@ -179,7 +179,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Profile _profile = new Profile { - Cutoff = Quality.HDTV720p, + Cutoff = Quality.HDTV720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities(), }; @@ -203,7 +203,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Profile _profile = new Profile { - Cutoff = Quality.HDTV720p, + Cutoff = Quality.HDTV720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities(), }; diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs index d412e4bb0..539222161 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs @@ -35,7 +35,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void Setup() { var fakeSeries = Builder.CreateNew() - .With(c => c.Profile = (LazyLoaded)new Profile { Cutoff = Quality.Bluray1080p }) + .With(c => c.Profile = (LazyLoaded)new Profile { Cutoff = Quality.Bluray1080p.Id }) .Build(); remoteEpisode = new RemoteEpisode diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs index 0bede228a..9e8220378 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs @@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities(), - Cutoff = cutoff, + Cutoff = cutoff.Id, }; var langProfile = new LanguageProfile diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs index adf625e3c..3ffae6ab9 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs @@ -106,7 +106,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_return_true_when_quality_in_queue_is_lower() { - _series.Profile.Value.Cutoff = Quality.Bluray1080p; + _series.Profile.Value.Cutoff = Quality.Bluray1080p.Id; _series.LanguageProfile.Value.Cutoff = Language.Spanish; var remoteEpisode = Builder.CreateNew() @@ -126,7 +126,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_return_true_when_quality_in_queue_is_lower_but_language_is_higher() { - _series.Profile.Value.Cutoff = Quality.Bluray1080p; + _series.Profile.Value.Cutoff = Quality.Bluray1080p.Id; _series.LanguageProfile.Value.Cutoff = Language.Spanish; var remoteEpisode = Builder.CreateNew() @@ -196,7 +196,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_return_false_when_quality_in_queue_is_better() { - _series.Profile.Value.Cutoff = Quality.Bluray1080p; + _series.Profile.Value.Cutoff = Quality.Bluray1080p.Id; var remoteEpisode = Builder.CreateNew() .With(r => r.Series = _series) @@ -294,7 +294,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_return_false_if_quality_and_language_in_queue_meets_cutoff() { - _series.Profile.Value.Cutoff = _remoteEpisode.ParsedEpisodeInfo.Quality.Quality; + _series.Profile.Value.Cutoff = _remoteEpisode.ParsedEpisodeInfo.Quality.Quality.Id; var remoteEpisode = Builder.CreateNew() .With(r => r.Series = _series) diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs index c60d6be82..4e212bd97 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs @@ -58,7 +58,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync _profile.Items.Add(new ProfileQualityItem { Allowed = true, Quality = Quality.WEBDL720p }); _profile.Items.Add(new ProfileQualityItem { Allowed = true, Quality = Quality.Bluray720p }); - _profile.Cutoff = Quality.WEBDL720p; + _profile.Cutoff = Quality.WEBDL720p.Id; _langProfile.Cutoff = Language.Spanish; _langProfile.Languages = Languages.LanguageFixture.GetDefaultLanguages(); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedEpisodeFileSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedEpisodeFileSpecificationFixture.cs index 72559e807..842eb102b 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedEpisodeFileSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedEpisodeFileSpecificationFixture.cs @@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync }; var fakeSeries = Builder.CreateNew() - .With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p }) + .With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p.Id }) .With(c => c.Path = @"C:\Series\My.Series".AsOsAgnostic()) .Build(); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/HistorySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/HistorySpecificationFixture.cs index eb0927662..2fc8d68c4 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/HistorySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/HistorySpecificationFixture.cs @@ -47,7 +47,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync }; _fakeSeries = Builder.CreateNew() - .With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }) + .With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }) .With(l => l.LanguageProfile = new LanguageProfile { Cutoff = Language.Spanish, Languages = LanguageFixture.GetDefaultLanguages() }) .Build(); @@ -163,7 +163,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync [Test] public void should_not_be_upgradable_if_episode_is_of_same_quality_as_existing() { - _fakeSeries.Profile = new Profile { Cutoff = Quality.Bluray1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }; + _fakeSeries.Profile = new Profile { Cutoff = Quality.Bluray1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }; _parseResultSingle.ParsedEpisodeInfo.Quality = new QualityModel(Quality.WEBDL1080p, new Revision(version: 1)); _upgradableQuality = new Tuple(new QualityModel(Quality.WEBDL1080p, new Revision(version: 1)), Language.English); @@ -175,7 +175,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync [Test] public void should_be_upgradable_if_episode_is_of_same_quality_as_existing_but_new_has_better_language() { - _fakeSeries.Profile = new Profile { Cutoff = Quality.WEBDL1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }; + _fakeSeries.Profile = new Profile { Cutoff = Quality.WEBDL1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }; _parseResultSingle.ParsedEpisodeInfo.Quality = new QualityModel(Quality.WEBDL1080p, new Revision(version: 1)); _parseResultSingle.ParsedEpisodeInfo.Language = Language.Spanish; _upgradableQuality = new Tuple(new QualityModel(Quality.WEBDL1080p, new Revision(version: 1)), Language.English); @@ -188,7 +188,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync [Test] public void should_not_be_upgradable_if_cutoff_already_met() { - _fakeSeries.Profile = new Profile { Cutoff = Quality.WEBDL1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }; + _fakeSeries.Profile = new Profile { Cutoff = Quality.WEBDL1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }; _parseResultSingle.ParsedEpisodeInfo.Quality = new QualityModel(Quality.WEBDL1080p, new Revision(version: 1)); _upgradableQuality = new Tuple(new QualityModel(Quality.WEBDL1080p, new Revision(version: 1)), Language.Spanish); @@ -216,7 +216,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync public void should_return_false_if_cutoff_already_met_and_cdh_is_disabled() { GivenCdhDisabled(); - _fakeSeries.Profile = new Profile { Cutoff = Quality.WEBDL1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }; + _fakeSeries.Profile = new Profile { Cutoff = Quality.WEBDL1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() }; _parseResultSingle.ParsedEpisodeInfo.Quality = new QualityModel(Quality.Bluray1080p, new Revision(version: 1)); _upgradableQuality = new Tuple(new QualityModel(Quality.WEBDL1080p, new Revision(version: 1)), Language.Spanish); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs index 59745e929..351d27663 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync var doubleEpisodeList = new List { new Episode { EpisodeFile = _firstFile, EpisodeFileId = 1 }, new Episode { EpisodeFile = _secondFile, EpisodeFileId = 1 }, new Episode { EpisodeFile = null } }; var fakeSeries = Builder.CreateNew() - .With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p }) + .With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p.Id }) .Build(); _parseResultMulti = new RemoteEpisode diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs index 9935b52f3..ff2706d39 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs @@ -42,7 +42,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var languages = Languages.LanguageFixture.GetDefaultLanguages(Language.English, Language.Spanish); var fakeSeries = Builder.CreateNew() - .With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p, Items = Qualities.QualityFixture.GetDefaultQualities()}) + .With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities()}) .With(l => l.LanguageProfile = new LanguageProfile { Cutoff = Language.Spanish, Languages = languages }) .Build(); diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs index 2064e01dd..8f280da4e 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests _profile = new Profile { Name = "Test", - Cutoff = Quality.HDTV720p, + Cutoff = Quality.HDTV720p.Id, Items = new List { new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p }, diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs index ebc2bbdda..c48361d19 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests _profile = new Profile { Name = "Test", - Cutoff = Quality.HDTV720p, + Cutoff = Quality.HDTV720p.Id, Items = new List { new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p }, diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs index 7d466305b..847ed6835 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests _profile = new Profile { Name = "Test", - Cutoff = Quality.HDTV720p, + Cutoff = Quality.HDTV720p.Id, Items = new List { new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p }, diff --git a/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs b/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs index 2f4139433..c8abc87d6 100644 --- a/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs +++ b/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs @@ -30,13 +30,13 @@ namespace NzbDrone.Core.Test.HistoryTests { _profile = new Profile { - Cutoff = Quality.WEBDL720p, + Cutoff = Quality.WEBDL720p.Id, Items = QualityFixture.GetDefaultQualities(), }; _profileCustom = new Profile { - Cutoff = Quality.WEBDL720p, + Cutoff = Quality.WEBDL720p.Id, Items = QualityFixture.GetDefaultQualities(Quality.DVD), }; diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 2e582fbad..cdc11717b 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -135,6 +135,7 @@ + @@ -343,6 +344,7 @@ + diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index 4373959bf..49a091cc3 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.Test.ParserTests new object[] { Quality.SDTV }, new object[] { Quality.DVD }, new object[] { Quality.WEBDL480p }, + new object[] { Quality.Bluray480p }, new object[] { Quality.HDTV720p }, new object[] { Quality.HDTV1080p }, new object[] { Quality.HDTV2160p }, @@ -67,22 +68,12 @@ namespace NzbDrone.Core.Test.ParserTests ParseAndVerifyQuality(title, Quality.SDTV, proper); } - [TestCase("WEEDS.S03E01-06.DUAL.XviD.Bluray.AC3-REPACK.-HELLYWOOD.avi", true)] [TestCase("The.Shield.S01E13.NTSC.x264-CtrlSD", false)] - [TestCase("WEEDS.S03E01-06.DUAL.BDRip.XviD.AC3.-HELLYWOOD", false)] - [TestCase("WEEDS.S03E01-06.DUAL.BDRip.X-viD.AC3.-HELLYWOOD", false)] - [TestCase("WEEDS.S03E01-06.DUAL.BDRip.AC3.-HELLYWOOD", false)] - [TestCase("WEEDS.S03E01-06.DUAL.BDRip.XviD.AC3.-HELLYWOOD.avi", false)] - [TestCase("WEEDS.S03E01-06.DUAL.XviD.Bluray.AC3.-HELLYWOOD.avi", false)] [TestCase("The.Girls.Next.Door.S03E06.DVDRip.XviD-WiDE", false)] [TestCase("The.Girls.Next.Door.S03E06.DVD.Rip.XviD-WiDE", false)] [TestCase("the.shield.1x13.circles.ws.xvidvd-tns", false)] [TestCase("the_x-files.9x18.sunshine_days.ac3.ws_dvdrip_xvid-fov.avi", false)] [TestCase("[FroZen] Miyuki - 23 [DVD][7F6170E6]", false)] - [TestCase("Hannibal.S01E05.576p.BluRay.DD5.1.x264-HiSD", false)] - [TestCase("Hannibal.S01E05.480p.BluRay.DD5.1.x264-HiSD", false)] - [TestCase("Heidi Girl of the Alps (BD)(640x480(RAW) (BATCH 1) (1-13)", false)] - [TestCase("[Doki] Clannad - 02 (848x480 XviD BD MP3) [95360783]", false)] public void should_parse_dvd_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.DVD, proper); @@ -99,6 +90,28 @@ namespace NzbDrone.Core.Test.ParserTests ParseAndVerifyQuality(title, Quality.WEBDL480p, proper); } + [TestCase("WEEDS.S03E01-06.DUAL.XviD.Bluray.AC3-REPACK.-HELLYWOOD.avi", true)] + [TestCase("WEEDS.S03E01-06.DUAL.BDRip.XviD.AC3.-HELLYWOOD", false)] + [TestCase("WEEDS.S03E01-06.DUAL.BDRip.X-viD.AC3.-HELLYWOOD", false)] + [TestCase("WEEDS.S03E01-06.DUAL.BDRip.AC3.-HELLYWOOD", false)] + [TestCase("WEEDS.S03E01-06.DUAL.BDRip.XviD.AC3.-HELLYWOOD.avi", false)] + [TestCase("WEEDS.S03E01-06.DUAL.XviD.Bluray.AC3.-HELLYWOOD.avi", false)] + [TestCase("Hannibal.S01E05.576p.BluRay.DD5.1.x264-HiSD", false)] + [TestCase("Hannibal.S01E05.480p.BluRay.DD5.1.x264-HiSD", false)] + [TestCase("Heidi Girl of the Alps (BD)(640x480(RAW) (BATCH 1) (1-13)", false)] + [TestCase("[Doki] Clannad - 02 (848x480 XviD BD MP3) [95360783]", false)] + public void should_parse_bluray480p_quality(string title, bool proper) + { + ParseAndVerifyQuality(title, Quality.Bluray480p, proper); + } + + [TestCase("Clarissa.Explains.It.All.S02E10.480p.HULU.WEBRip.x264-Puffin", false)] + [TestCase("Duck.Dynasty.S10E14.Techs.And.Balances.480p.AE.WEBRip.AAC2.0.x264-SEA", false)] + public void should_parse_webrip480p_quality(string title, bool proper) + { + ParseAndVerifyQuality(title, Quality.WEBRip480p, proper); + } + [TestCase("Dexter - S01E01 - Title [HDTV]", false)] [TestCase("Dexter - S01E01 - Title [HDTV-720p]", false)] [TestCase("Pawn Stars S04E87 REPACK 720p HDTV x264 aAF", true)] @@ -152,7 +165,6 @@ namespace NzbDrone.Core.Test.ParserTests ParseAndVerifyQuality(title, Quality.HDTV2160p, proper); } - [TestCase("Arrested.Development.S04E01.720p.WEBRip.AAC2.0.x264-NFRiP", false)] [TestCase("Vanguard S01E04 Mexicos Death Train 720p WEB DL", false)] [TestCase("Hawaii Five 0 S02E21 720p WEB DL DD5 1 H 264", false)] [TestCase("Castle S04E22 720p WEB DL DD5 1 H 264 NFHD", false)] @@ -176,7 +188,14 @@ namespace NzbDrone.Core.Test.ParserTests ParseAndVerifyQuality(title, Quality.WEBDL720p, proper); } - [TestCase("Arrested.Development.S04E01.iNTERNAL.1080p.WEBRip.x264-QRUS", false)] + [TestCase("Arrested.Development.S04E01.720p.WEBRip.AAC2.0.x264-NFRiP", false)] + [TestCase("American.Gods.S01E07.A.Prayer.For.Mad.Sweeney.720p.AMZN.WEBRip.DD5.1.x264-NTb", false)] + [TestCase("LEGO.Star.Wars.The.Freemaker.Adventures.S07E01.A.New.Home.720p.DSNY.WEBRip.AAC2.0.x264-TVSmash", false)] + public void should_parse_webrip720p_quality(string title, bool proper) + { + ParseAndVerifyQuality(title, Quality.WEBRip720p, proper); + } + [TestCase("CSI NY S09E03 1080p WEB DL DD5 1 H264 NFHD", false)] [TestCase("Two and a Half Men S10E03 1080p WEB DL DD5 1 H 264 NFHD", false)] [TestCase("Criminal.Minds.S08E01.1080p.WEB-DL.DD5.1.H264-NFHD", false)] @@ -202,10 +221,15 @@ namespace NzbDrone.Core.Test.ParserTests ParseAndVerifyQuality(title, Quality.WEBDL1080p, proper); } - [TestCase("CASANOVA S01E01.2160P AMZN WEBRIP DD2.0 HI10P X264-TROLLUHD", false)] - [TestCase("JUST ADD MAGIC S01E01.2160P AMZN WEBRIP DD2.0 X264-TROLLUHD", false)] - [TestCase("The.Man.In.The.High.Castle.S01E01.2160p.AMZN.WEBRip.DD2.0.Hi10p.X264-TrollUHD", false)] - [TestCase("The Man In the High Castle S01E01 2160p AMZN WEBRip DD2.0 Hi10P x264-TrollUHD", false)] + [TestCase("Arrested.Development.S04E01.iNTERNAL.1080p.WEBRip.x264-QRUS", false)] + [TestCase("Blue.Bloods.S07E20.1080p.AMZN.WEBRip.DDP5.1.x264-ViSUM ac3.(NLsub)", false)] + [TestCase("Better.Call.Saul.S03E09.1080p.NF.WEBRip.DD5.1.x264-ViSUM", false)] + public void should_parse_webrip1080p_quality(string title, bool proper) + { + ParseAndVerifyQuality(title, Quality.WEBRip1080p, proper); + } + + [TestCase("The.Nightly.Show.2016.03.14.2160p.WEB.x264-spamTV", false)] [TestCase("The.Nightly.Show.2016.03.14.2160p.WEB.h264-spamTV", false)] [TestCase("The.Nightly.Show.2016.03.14.2160p.WEB.PROPER.h264-spamTV", true)] @@ -216,6 +240,17 @@ namespace NzbDrone.Core.Test.ParserTests ParseAndVerifyQuality(title, Quality.WEBDL2160p, proper); } + [TestCase("CASANOVA S01E01.2160P AMZN WEBRIP DD2.0 HI10P X264-TROLLUHD", false)] + [TestCase("JUST ADD MAGIC S01E01.2160P AMZN WEBRIP DD2.0 X264-TROLLUHD", false)] + [TestCase("The.Man.In.The.High.Castle.S01E01.2160p.AMZN.WEBRip.DD2.0.Hi10p.X264-TrollUHD", false)] + [TestCase("The Man In the High Castle S01E01 2160p AMZN WEBRip DD2.0 Hi10P x264-TrollUHD", false)] + [TestCase("House.of.Cards.US.S05E08.Chapter.60.2160p.NF.WEBRip.DD5.1.x264-NTb.NLsubs", false)] + [TestCase("Bill Nye Saves the World S01 2160p Netflix WEBRip DD5.1 x264-TrollUHD", false)] + public void should_parse_webrip2160p_quality(string title, bool proper) + { + ParseAndVerifyQuality(title, Quality.WEBRip2160p, proper); + } + [TestCase("WEEDS.S03E01-06.DUAL.Bluray.AC3.-HELLYWOOD.avi", false)] [TestCase("Chuck - S01E03 - Come Fly With Me - 720p BluRay.mkv", false)] [TestCase("The Big Bang Theory.S03E01.The Electric Can Opener Fluctuation.m2ts", false)] diff --git a/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs b/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs index 90657296a..02e53dcd4 100644 --- a/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; @@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.Profiles var profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p), - Cutoff = Quality.Bluray1080p, + Cutoff = Quality.Bluray1080p.Id, Name = "TestProfile" }; diff --git a/src/NzbDrone.Core.Test/Profiles/Qualities/QualityIndexCompareToFixture.cs b/src/NzbDrone.Core.Test/Profiles/Qualities/QualityIndexCompareToFixture.cs new file mode 100644 index 000000000..0e4922203 --- /dev/null +++ b/src/NzbDrone.Core.Test/Profiles/Qualities/QualityIndexCompareToFixture.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Profiles.Qualities; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Qualities +{ + [TestFixture] + public class QualityIndexCompareToFixture : CoreTest + { + [TestCase(1, 0, 1, 0, 0)] + [TestCase(1, 1, 1, 0, 1)] + [TestCase(2, 0, 1, 0, 1)] + [TestCase(1, 0, 1, 1, -1)] + [TestCase(1, 0, 2, 0, -1)] + public void should_match_expected_when_respect_group_order_is_true(int leftIndex, int leftGroupIndex, int rightIndex, int rightGroupIndex, int expected) + { + var left = new QualityIndex(leftIndex, leftGroupIndex); + var right = new QualityIndex(rightIndex, rightGroupIndex); + left.CompareTo(right, true).Should().Be(expected); + } + + [TestCase(1, 0, 1, 0, 0)] + [TestCase(1, 1, 1, 0, 0)] + [TestCase(2, 0, 1, 0, 1)] + [TestCase(1, 0, 1, 1, 0)] + [TestCase(1, 0, 2, 0, -1)] + public void should_match_expected_when_respect_group_order_is_false(int leftIndex, int leftGroupIndex, int rightIndex, int rightGroupIndex, int expected) + { + var left = new QualityIndex(leftIndex, leftGroupIndex); + var right = new QualityIndex(rightIndex, rightGroupIndex); + left.CompareTo(right, false).Should().Be(expected); + } + } +} diff --git a/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs index 8a5aac2fc..2182589c6 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs +++ b/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs @@ -1,4 +1,5 @@ -using FluentAssertions; +using System.Collections.Generic; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; @@ -21,6 +22,50 @@ namespace NzbDrone.Core.Test.Qualities Subject = new QualityModelComparer(new Profile { Items = QualityFixture.GetDefaultQualities(Quality.Bluray720p, Quality.DVD) }); } + private void GivenGroupedProfile() + { + var profile = new Profile + { + Items = new List + { + new ProfileQualityItem + { + Allowed = false, + Quality = Quality.SDTV + }, + new ProfileQualityItem + { + Allowed = false, + Quality = Quality.DVD + }, + new ProfileQualityItem + { + Allowed = true, + Items = new List + { + new ProfileQualityItem + { + Allowed = true, + Quality = Quality.HDTV720p + }, + new ProfileQualityItem + { + Allowed = true, + Quality = Quality.WEBDL720p + } + } + }, + new ProfileQualityItem + { + Allowed = true, + Quality = Quality.Bluray720p + } + } + }; + + Subject = new QualityModelComparer(profile); + } + [Test] public void should_be_greater_when_first_quality_is_greater_than_second() { @@ -72,5 +117,31 @@ namespace NzbDrone.Core.Test.Qualities compare.Should().BeGreaterThan(0); } + + [Test] + public void should_ignore_group_order_by_default() + { + GivenGroupedProfile(); + + var first = new QualityModel(Quality.HDTV720p); + var second = new QualityModel(Quality.WEBDL720p); + + var compare = Subject.Compare(first, second); + + compare.Should().Be(0); + } + + [Test] + public void should_respect_group_order() + { + GivenGroupedProfile(); + + var first = new QualityModel(Quality.HDTV720p); + var second = new QualityModel(Quality.WEBDL720p); + + var compare = Subject.Compare(first, second, true); + + compare.Should().BeLessThan(0); + } } } diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs index 9e44fbac8..e43288255 100644 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs @@ -31,7 +31,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests var profile = new Profile { Id = 1, - Cutoff = Quality.WEBDL480p, + Cutoff = Quality.WEBDL480p.Id, Items = new List { new ProfileQualityItem { Allowed = true, Quality = Quality.SDTV }, diff --git a/src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/SeriesRepositoryFixture.cs b/src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/SeriesRepositoryFixture.cs index 8816e0231..4d2b1bb2e 100644 --- a/src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/SeriesRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/SeriesRepositoryFixture.cs @@ -1,4 +1,4 @@ -using FizzWare.NBuilder; +using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; using System.Linq; @@ -22,7 +22,7 @@ namespace NzbDrone.Core.Test.TvTests.SeriesRepositoryTests { Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p), - Cutoff = Quality.Bluray1080p, + Cutoff = Quality.Bluray1080p.Id, Name = "TestProfile" }; diff --git a/src/NzbDrone.Core/Datastore/Migration/117_add_webrip_and_br480_qualites_in_profile.cs b/src/NzbDrone.Core/Datastore/Migration/117_add_webrip_and_br480_qualites_in_profile.cs new file mode 100644 index 000000000..9adb7f5a0 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/117_add_webrip_and_br480_qualites_in_profile.cs @@ -0,0 +1,181 @@ +using System.Collections.Generic; +using System.Data; +using System.Linq; +using FluentMigrator; +using Newtonsoft.Json; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(117)] + public class add_webrip_and_br480_qualites_in_profile : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(ConvertProfile); + } + + private void ConvertProfile(IDbConnection conn, IDbTransaction tran) + { + var updater = new ProfileUpdater116(conn, tran); + + updater.CreateGroupAt(8, 1000, "WEB 480p", new[] {12, 8}); // Group WEBRip480p with WEBDL480p + updater.CreateGroupAt(2, 1001, "DVD", new[] {2, 13}); // Group Bluray480p with DVD + updater.CreateGroupAt(5, 1002, "WEB 720p", new[] {14, 5}); // Group WEBRip720p with WEBDL720p + updater.CreateGroupAt(3, 1003, "WEB 1080p", new[] {15, 3}); // Group WEBRip1080p with WEBDL1080p + updater.CreateGroupAt(18, 1004, "WEB 2160p", new[] {17, 18}); // Group WEBRip2160p with WEBDL2160p + + updater.Commit(); + } + } + + public class Profile117 + { + public int Id { get; set; } + public string Name { get; set; } + public int Cutoff { get; set; } + public List Items { get; set; } + } + + public class ProfileItem117 + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public int Id { get; set; } + + public string Name { get; set; } + public int? Quality { get; set; } + public List Items { get; set; } + public bool Allowed { get; set; } + + public ProfileItem117() + { + Items = new List(); + } + } + + public class ProfileUpdater116 + { + private readonly IDbConnection _connection; + private readonly IDbTransaction _transaction; + + private List _profiles; + private HashSet _changedProfiles = new HashSet(); + + public ProfileUpdater116(IDbConnection conn, IDbTransaction tran) + { + _connection = conn; + _transaction = tran; + + _profiles = GetProfiles(); + } + + public void Commit() + { + foreach (var profile in _changedProfiles) + { + using (var updateProfileCmd = _connection.CreateCommand()) + { + updateProfileCmd.Transaction = _transaction; + updateProfileCmd.CommandText = + "UPDATE Profiles SET Name = ?, Cutoff = ?, Items = ? WHERE Id = ?"; + updateProfileCmd.AddParameter(profile.Name); + updateProfileCmd.AddParameter(profile.Cutoff); + updateProfileCmd.AddParameter(profile.Items.ToJson()); + updateProfileCmd.AddParameter(profile.Id); + + updateProfileCmd.ExecuteNonQuery(); + } + } + + _changedProfiles.Clear(); + } + + public void CreateGroupAt(int find, int groupId, string name, int[] qualities) + { + foreach (var profile in _profiles) + { + var findIndex = profile.Items.FindIndex(v => v.Quality == find); + + if (findIndex > -1) + { + var findQuality = profile.Items[findIndex]; + + profile.Items.Insert(findIndex, new ProfileItem117 + { + Id = groupId, + Name = name, + Quality = null, + Items = qualities.Select(q => new ProfileItem117 + { + Quality = q, + Allowed = findQuality.Allowed + }).ToList(), + Allowed = findQuality.Allowed + }); + } + else + { + // If the ID isn't found for some reason (mangled migration 71?) + + profile.Items.Add(new ProfileItem117 + { + Id = groupId, + Name = name, + Quality = null, + Items = qualities.Select(q => new ProfileItem117 + { + Quality = q, + Allowed = false + }).ToList(), + Allowed = false + }); + } + + foreach (var quality in qualities) + { + var index = profile.Items.FindIndex(v => v.Quality == quality); + + if (index > -1) + { + profile.Items.RemoveAt(index); + } + + if (profile.Cutoff == quality) + { + profile.Cutoff = groupId; + } + } + + _changedProfiles.Add(profile); + } + } + + private List GetProfiles() + { + var profiles = new List(); + + using (var getProfilesCmd = _connection.CreateCommand()) + { + getProfilesCmd.Transaction = _transaction; + getProfilesCmd.CommandText = @"SELECT Id, Name, Cutoff, Items FROM Profiles"; + + using (var profileReader = getProfilesCmd.ExecuteReader()) + { + while (profileReader.Read()) + { + profiles.Add(new Profile117 + { + Id = profileReader.GetInt32(0), + Name = profileReader.GetString(1), + Cutoff = profileReader.GetInt32(2), + Items = Json.Deserialize>(profileReader.GetString(3)) + }); + } + } + } + + return profiles; + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 49ea94bea..4c675f943 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -103,6 +103,7 @@ namespace NzbDrone.Core.Datastore .HasOne(episode => episode.EpisodeFile, episode => episode.EpisodeFileId); Mapper.Entity().RegisterModel("QualityDefinitions") + .Ignore(d => d.GroupName) .Ignore(d => d.Weight); Mapper.Entity().RegisterModel("Profiles"); diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs index 91d983955..b85ceb606 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs @@ -58,7 +58,7 @@ namespace NzbDrone.Core.DecisionEngine private int CompareQuality(DownloadDecision x, DownloadDecision y) { - return CompareAll(CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.Series.Profile.Value.Items.FindIndex(v => v.Quality == remoteEpisode.ParsedEpisodeInfo.Quality.Quality)), + return CompareAll(CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.Series.Profile.Value.GetIndex(remoteEpisode.ParsedEpisodeInfo.Quality.Quality)), CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.ParsedEpisodeInfo.Quality.Revision.Real), CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.ParsedEpisodeInfo.Quality.Revision.Version)); } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs index a5ebdee26..e030d4d2d 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs @@ -21,6 +21,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { + var profile = subject.Series.Profile.Value; + foreach (var file in subject.Episodes.Where(c => c.EpisodeFileId != 0).Select(c => c.EpisodeFile.Value)) { if (file == null) @@ -30,14 +32,18 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } _logger.Debug("Comparing file quality and language with report. Existing file is {0} - {1}", file.Quality, file.Language); - if (!_upgradableSpecification.CutoffNotMet(subject.Series.Profile, + if (!_upgradableSpecification.CutoffNotMet(profile, subject.Series.LanguageProfile, file.Quality, file.Language, subject.ParsedEpisodeInfo.Quality)) { _logger.Debug("Cutoff already met, rejecting."); - return Decision.Reject("Existing file meets cutoff: {0} - {1}", subject.Series.Profile.Value.Cutoff, subject.Series.LanguageProfile.Value.Cutoff); + + var qualityCutoffIndex = profile.GetIndex(profile.Cutoff); + var qualityCutoff = profile.Items[qualityCutoffIndex.Index]; + + return Decision.Reject("Existing file meets cutoff: {0} - {1}", qualityCutoff, subject.Series.LanguageProfile.Value.Cutoff); } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs index d8dcd9afb..b6a291e9d 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs @@ -1,3 +1,4 @@ +using System; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -19,7 +20,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { _logger.Debug("Checking if report meets quality requirements. {0}", subject.ParsedEpisodeInfo.Quality); - if (!subject.Series.Profile.Value.Items.Exists(v => v.Allowed && v.Quality == subject.ParsedEpisodeInfo.Quality.Quality)) + + var profile = subject.Series.Profile.Value; + var qualityIndex = profile.GetIndex(subject.ParsedEpisodeInfo.Quality.Quality); + var qualityOrGroup = profile.Items[qualityIndex.Index]; + + if (!qualityOrGroup.Allowed) { _logger.Debug("Quality {0} rejected by Series' quality profile", subject.ParsedEpisodeInfo.Quality); return Decision.Reject("{0} is not wanted in profile", subject.ParsedEpisodeInfo.Quality.Quality); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs index 5da33d0da..1540cb6f9 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using NLog; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.IndexerSearch.Definitions; @@ -73,8 +73,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync } // If quality meets or exceeds the best allowed quality in the profile accept it immediately - var bestQualityInProfile = new QualityModel(profile.LastAllowedQuality()); - var isBestInProfile = comparer.Compare(subject.ParsedEpisodeInfo.Quality, bestQualityInProfile) >= 0; + var bestQualityInProfile = profile.LastAllowedQuality(); + var isBestInProfile = comparer.Compare(subject.ParsedEpisodeInfo.Quality.Quality, bestQualityInProfile) >= 0; var isBestInProfileLanguage = comparerLanguage.Compare(subject.ParsedEpisodeInfo.Language, languageProfile.LastAllowedLanguage()) >= 0; if (isBestInProfile && isBestInProfileLanguage && isPreferredProtocol) diff --git a/src/NzbDrone.Core/DecisionEngine/UpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/UpgradableSpecification.cs index a8eec69e3..5a1fb1dc2 100644 --- a/src/NzbDrone.Core/DecisionEngine/UpgradableSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/UpgradableSpecification.cs @@ -55,7 +55,7 @@ namespace NzbDrone.Core.DecisionEngine public bool IsUpgradable(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality, Language newLanguage) { // If qualities are the same then check language - if (newQuality != null && currentQuality == newQuality) + if (newQuality != null && new QualityModelComparer(profile).Compare(newQuality, currentQuality) == 0) { return IsLanguageUpgradable(languageProfile, currentLanguage, newLanguage); } @@ -72,7 +72,7 @@ namespace NzbDrone.Core.DecisionEngine public bool QualityCutoffNotMet(Profile profile, QualityModel currentQuality, QualityModel newQuality = null) { - var qualityCompare = new QualityModelComparer(profile).Compare(currentQuality.Quality, profile.Cutoff); + var qualityCompare = new QualityModelComparer(profile).Compare(currentQuality.Quality.Id, profile.Cutoff); if (qualityCompare < 0) { @@ -116,6 +116,7 @@ namespace NzbDrone.Core.DecisionEngine { var compare = newQuality.Revision.CompareTo(currentQuality.Revision); + // Comparing the quality directly because we don't want to upgrade to a proper for a webrip from a webdl or vice versa if (currentQuality.Quality == newQuality.Quality && compare > 0) { _logger.Debug("New quality is a better revision for existing quality"); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs index 2a7af0413..77d05e4e7 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs @@ -5,8 +5,6 @@ using NzbDrone.Core.Download; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.Languages; -using NzbDrone.Core.Profiles.Qualities; -using System.Collections.Generic; namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications { @@ -23,7 +21,6 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications { var qualityComparer = new QualityModelComparer(localEpisode.Series.Profile); var languageComparer = new LanguageComparer(localEpisode.Series.LanguageProfile); - var profile = localEpisode.Series.Profile.Value; if (localEpisode.Episodes.Any(e => e.EpisodeFileId != 0 && qualityComparer.Compare(e.EpisodeFile.Value.Quality, localEpisode.Quality) > 0)) { diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 2960a7190..5411d6512 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -320,6 +320,7 @@ + @@ -1003,6 +1004,7 @@ + diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index 2124d0bd8..a177e2f37 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -16,7 +16,8 @@ namespace NzbDrone.Core.Parser private static readonly Regex SourceRegex = new Regex(@"\b(?: (?BluRay|Blu-Ray|HD-?DVD|BD)| - (?WEB[-_. ]DL|WEBDL|WebRip|AmazonHD|iTunesHD|NetflixU?HD|WebHD|[. ]WEB[. ](?:[xh]26[45]|DDP?5[. ]1)|\d+0p[. ]WEB[. ]|WEB-DLMux)| + (?WEB[-_. ]DL|WEBDL|AmazonHD|iTunesHD|NetflixU?HD|WebHD|[. ]WEB[. ](?:[xh]26[45]|DDP?5[. ]1)|\d+0p[. ]WEB[. ]|WEB-DLMux)| + (?WebRip)| (?HDTV)| (?BDRip)| (?BRRip)| @@ -99,7 +100,7 @@ namespace NzbDrone.Core.Parser { if (codecRegex.Groups["xvid"].Success || codecRegex.Groups["divx"].Success) { - result.Quality = Quality.DVD; + result.Quality = Quality.Bluray480p; return result; } @@ -117,7 +118,7 @@ namespace NzbDrone.Core.Parser if (resolution == Resolution.R480P || resolution == Resolution.R576p) { - result.Quality = Quality.DVD; + result.Quality = Quality.Bluray480p; return result; } @@ -155,6 +156,30 @@ namespace NzbDrone.Core.Parser return result; } + if (sourceMatch.Groups["webrip"].Success) + { + if (resolution == Resolution.R2160p) + { + result.Quality = Quality.WEBRip2160p; + return result; + } + + if (resolution == Resolution.R1080p) + { + result.Quality = Quality.WEBRip1080p; + return result; + } + + if (resolution == Resolution.R720p) + { + result.Quality = Quality.WEBRip720p; + return result; + } + + result.Quality = Quality.WEBRip480p; + return result; + } + if (sourceMatch.Groups["hdtv"].Success) { if (resolution == Resolution.R2160p) @@ -197,7 +222,7 @@ namespace NzbDrone.Core.Parser result.Quality = Quality.Bluray1080p; return result; default: - result.Quality = Quality.DVD; + result.Quality = Quality.Bluray480p; return result; } } diff --git a/src/NzbDrone.Core/Profiles/Qualities/Profile.cs b/src/NzbDrone.Core/Profiles/Qualities/Profile.cs index b4eb565a0..9749f17be 100644 --- a/src/NzbDrone.Core/Profiles/Qualities/Profile.cs +++ b/src/NzbDrone.Core/Profiles/Qualities/Profile.cs @@ -8,12 +8,59 @@ namespace NzbDrone.Core.Profiles.Qualities public class Profile : ModelBase { public string Name { get; set; } - public Quality Cutoff { get; set; } + public int Cutoff { get; set; } public List Items { get; set; } public Quality LastAllowedQuality() { - return Items.Last(q => q.Allowed).Quality; + var lastAllowed = Items.Last(q => q.Allowed); + + if (lastAllowed.Quality != null) + { + return lastAllowed.Quality; + } + + // Returning any item from the group will work, + // returning the last because it's the true last quality. + return lastAllowed.Items.Last().Quality; + } + + public QualityIndex GetIndex(Quality quality) + { + return GetIndex(quality.Id); + } + + public QualityIndex GetIndex(int id) + { + for (var i = 0; i < Items.Count; i++) + { + var item = Items[i]; + var quality = item.Quality; + + // Quality matches by ID + if (quality != null && quality.Id == id) + { + return new QualityIndex(i); + } + + // Group matches by ID + if (item.Id > 0 && item.Id == id) + { + return new QualityIndex(i); + } + + for (var g = 0; g < item.Items.Count; g++) + { + var groupItem = item.Items[g]; + + if (groupItem.Quality.Id == id) + { + return new QualityIndex(i, g); + } + } + } + + return new QualityIndex(); } } } diff --git a/src/NzbDrone.Core/Profiles/Qualities/ProfileQualityItem.cs b/src/NzbDrone.Core/Profiles/Qualities/ProfileQualityItem.cs index 25da900ef..5a5b256ea 100644 --- a/src/NzbDrone.Core/Profiles/Qualities/ProfileQualityItem.cs +++ b/src/NzbDrone.Core/Profiles/Qualities/ProfileQualityItem.cs @@ -1,11 +1,47 @@ -using NzbDrone.Core.Datastore; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Qualities; namespace NzbDrone.Core.Profiles.Qualities { public class ProfileQualityItem : IEmbeddedDocument { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public int Id { get; set; } + + public string Name { get; set; } public Quality Quality { get; set; } + public List Items { get; set; } public bool Allowed { get; set; } + + public ProfileQualityItem() + { + Items = new List(); + } + + public List GetQualities() + { + if (Quality == null) + { + return Items.Select(s => s.Quality).ToList(); + } + + return new List{ Quality }; + } + + public override string ToString() + { + var qualitiesString = string.Join(", ", GetQualities()); + + if (Name.IsNotNullOrWhiteSpace()) + { + return $"{Name} ({qualitiesString})"; + } + + return qualitiesString; + } } } diff --git a/src/NzbDrone.Core/Profiles/Qualities/ProfileService.cs b/src/NzbDrone.Core/Profiles/Qualities/ProfileService.cs index 31d19343d..f7a12fa56 100644 --- a/src/NzbDrone.Core/Profiles/Qualities/ProfileService.cs +++ b/src/NzbDrone.Core/Profiles/Qualities/ProfileService.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.Profiles.Qualities List All(); Profile Get(int id); bool Exists(int id); + Profile GetDefaultProfile(string name, Quality cutoff = null, params Quality[] allowed); } public class ProfileService : IProfileService, IHandle @@ -66,22 +67,7 @@ namespace NzbDrone.Core.Profiles.Qualities { return _profileRepository.Exists(id); } - - private Profile AddDefaultProfile(string name, Quality cutoff, params Quality[] allowed) - { - var items = Quality.DefaultQualityDefinitions - .OrderBy(v => v.Weight) - .Select(v => new ProfileQualityItem { Quality = v.Quality, Allowed = allowed.Contains(v.Quality) }) - .ToList(); - - var profile = new Profile { Name = name, - Cutoff = cutoff, - Items = items, - }; - - return Add(profile); - } - + public void Handle(ApplicationStartedEvent message) { if (All().Any()) return; @@ -90,42 +76,107 @@ namespace NzbDrone.Core.Profiles.Qualities AddDefaultProfile("Any", Quality.SDTV, Quality.SDTV, + Quality.WEBRip480p, Quality.WEBDL480p, Quality.DVD, Quality.HDTV720p, Quality.HDTV1080p, + Quality.WEBRip720p, Quality.WEBDL720p, + Quality.WEBRip1080p, Quality.WEBDL1080p, Quality.Bluray720p, Quality.Bluray1080p); AddDefaultProfile("SD", Quality.SDTV, Quality.SDTV, + Quality.WEBRip480p, Quality.WEBDL480p, Quality.DVD); AddDefaultProfile("HD-720p", Quality.HDTV720p, Quality.HDTV720p, + Quality.WEBRip720p, Quality.WEBDL720p, Quality.Bluray720p); AddDefaultProfile("HD-1080p", Quality.HDTV1080p, Quality.HDTV1080p, + Quality.WEBRip1080p, Quality.WEBDL1080p, Quality.Bluray1080p); AddDefaultProfile("Ultra-HD", Quality.HDTV2160p, Quality.HDTV2160p, + Quality.WEBRip2160p, Quality.WEBDL2160p, Quality.Bluray2160p); AddDefaultProfile("HD - 720p/1080p", Quality.HDTV720p, Quality.HDTV720p, Quality.HDTV1080p, + Quality.WEBRip720p, Quality.WEBDL720p, + Quality.WEBRip1080p, Quality.WEBDL1080p, Quality.Bluray720p, Quality.Bluray1080p); } + + public Profile GetDefaultProfile(string name, Quality cutoff = null, params Quality[] allowed) + { + var groupedQualites = Quality.DefaultQualityDefinitions.GroupBy(q => q.Weight); + var items = new List(); + var groupId = 1000; + var profileCutoff = cutoff == null ? Quality.Unknown.Id : cutoff.Id; + + foreach (var group in groupedQualites) + { + if (group.Count() == 1) + { + var quality = group.First().Quality; + + items.Add(new ProfileQualityItem { Quality = group.First().Quality, Allowed = allowed.Contains(quality) }); + continue; + } + + var groupAllowed = group.Any(g => allowed.Contains(g.Quality)); + + items.Add(new ProfileQualityItem + { + Id = groupId, + Name = group.First().GroupName, + Items = group.Select(g => new ProfileQualityItem + { + Quality = g.Quality, + Allowed = groupAllowed + }).ToList(), + Allowed = groupAllowed + }); + + if (group.Any(g => g.Quality.Id == profileCutoff)) + { + profileCutoff = groupId; + } + + groupId++; + } + + var qualityProfile = new Profile + { + Name = name, + Cutoff = profileCutoff, + Items = items + }; + + return qualityProfile; + } + + private Profile AddDefaultProfile(string name, Quality cutoff, params Quality[] allowed) + { + var profile = GetDefaultProfile(name, cutoff, allowed); + + return Add(profile); + } } } diff --git a/src/NzbDrone.Core/Profiles/Qualities/QualityIndex.cs b/src/NzbDrone.Core/Profiles/Qualities/QualityIndex.cs new file mode 100644 index 000000000..95b115b2f --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Qualities/QualityIndex.cs @@ -0,0 +1,55 @@ +using System; + +namespace NzbDrone.Core.Profiles.Qualities +{ + public class QualityIndex : IComparable, IComparable + { + public int Index { get; set; } + public int GroupIndex { get; set; } + + public QualityIndex() + { + Index = 0; + GroupIndex = 0; + } + + public QualityIndex(int index) + { + Index = index; + GroupIndex = 0; + } + + public QualityIndex(int index, int groupIndex) + { + Index = index; + GroupIndex = groupIndex; + } + + public int CompareTo(object obj) + { + return CompareTo((QualityIndex)obj, true); + } + + public int CompareTo(QualityIndex other) + { + return CompareTo(other, true); + } + + public int CompareTo(QualityIndex right, bool respectGroupOrder) + { + if (right == null) + { + return 1; + } + + var indexCompare = Index.CompareTo(right.Index); + + if (respectGroupOrder && indexCompare == 0) + { + return GroupIndex.CompareTo(right.GroupIndex); + } + + return indexCompare;; + } + } +} diff --git a/src/NzbDrone.Core/Qualities/Quality.cs b/src/NzbDrone.Core/Qualities/Quality.cs index fedbd0b34..360160eaf 100644 --- a/src/NzbDrone.Core/Qualities/Quality.cs +++ b/src/NzbDrone.Core/Qualities/Quality.cs @@ -71,12 +71,12 @@ namespace NzbDrone.Core.Qualities public static Quality HDTV1080p => new Quality(9, "HDTV-1080p", QualitySource.Television, 1080); public static Quality RAWHD => new Quality(10, "Raw-HD", QualitySource.TelevisionRaw, 1080); //public static Quality HDTV480p { get { return new Quality(11, "HDTV-480p", QualitySource.Television, 480); } } - //public static Quality WEBRip480p { get { return new Quality(12, "WEBRip-480p", QualitySource.WebRip, 480); } } - //public static Quality Bluray480p { get { return new Quality(13, "Bluray-480p", QualitySource.Bluray, 480); } } - //public static Quality WEBRip720p { get { return new Quality(14, "WEBRip-720p", QualitySource.WebRip, 720); } } - //public static Quality WEBRip1080p { get { return new Quality(15, "WEBRip-1080p", QualitySource.WebRip, 1080); } } + public static Quality WEBRip480p { get { return new Quality(12, "WEBRip-480p", QualitySource.WebRip, 480); } } + public static Quality Bluray480p { get { return new Quality(13, "Bluray-480p", QualitySource.Bluray, 480); } } + public static Quality WEBRip720p { get { return new Quality(14, "WEBRip-720p", QualitySource.WebRip, 720); } } + public static Quality WEBRip1080p { get { return new Quality(15, "WEBRip-1080p", QualitySource.WebRip, 1080); } } public static Quality HDTV2160p => new Quality(16, "HDTV-2160p", QualitySource.Television, 2160); - //public static Quality WEBRip2160p { get { return new Quality(17, "WEBRip-2160p", QualitySource.WebRip, 2160); } } + public static Quality WEBRip2160p { get { return new Quality(17, "WEBRip-2160p", QualitySource.WebRip, 2160); } } public static Quality WEBDL2160p => new Quality(18, "WEBDL-2160p", QualitySource.Web, 2160); public static Quality Bluray2160p => new Quality(19, "Bluray-2160p", QualitySource.Bluray, 2160); @@ -87,17 +87,22 @@ namespace NzbDrone.Core.Qualities Unknown, SDTV, DVD, - WEBDL1080p, + WEBRip480p, + WEBDL480p, + Bluray480p, HDTV720p, + WEBRip720p, WEBDL720p, Bluray720p, Bluray1080p, - WEBDL480p, HDTV1080p, + WEBRip1080p, + WEBDL1080p, RAWHD, HDTV2160p, + WEBRip2160p, WEBDL2160p, - Bluray2160p, + Bluray2160p }; AllLookup = new Quality[All.Select(v => v.Id).Max() + 1]; @@ -110,18 +115,23 @@ namespace NzbDrone.Core.Qualities { new QualityDefinition(Quality.Unknown) { Weight = 1, MinSize = 0, MaxSize = 100 }, new QualityDefinition(Quality.SDTV) { Weight = 2, MinSize = 0, MaxSize = 100 }, - new QualityDefinition(Quality.WEBDL480p) { Weight = 3, MinSize = 0, MaxSize = 100 }, - new QualityDefinition(Quality.DVD) { Weight = 4, MinSize = 0, MaxSize = 100 }, - new QualityDefinition(Quality.HDTV720p) { Weight = 5, MinSize = 0, MaxSize = 100 }, - new QualityDefinition(Quality.HDTV1080p) { Weight = 6, MinSize = 0, MaxSize = 100 }, - new QualityDefinition(Quality.RAWHD) { Weight = 7, MinSize = 0, MaxSize = null }, - new QualityDefinition(Quality.WEBDL720p) { Weight = 8, MinSize = 0, MaxSize = 100 }, - new QualityDefinition(Quality.Bluray720p) { Weight = 9, MinSize = 0, MaxSize = 100 }, - new QualityDefinition(Quality.WEBDL1080p) { Weight = 10, MinSize = 0, MaxSize = 100 }, - new QualityDefinition(Quality.Bluray1080p) { Weight = 11, MinSize = 0, MaxSize = 100 }, - new QualityDefinition(Quality.HDTV2160p) { Weight = 12, MinSize = 0, MaxSize = null }, - new QualityDefinition(Quality.WEBDL2160p) { Weight = 13, MinSize = 0, MaxSize = null }, - new QualityDefinition(Quality.Bluray2160p) { Weight = 14, MinSize = 0, MaxSize = null }, + new QualityDefinition(Quality.WEBRip480p) { Weight = 3, MinSize = 0, MaxSize = 100, GroupName = "WEB 480p" }, + new QualityDefinition(Quality.WEBDL480p) { Weight = 3, MinSize = 0, MaxSize = 100, GroupName = "WEB 480p" }, + new QualityDefinition(Quality.DVD) { Weight = 4, MinSize = 0, MaxSize = 100, GroupName = "DVD" }, + new QualityDefinition(Quality.Bluray480p) { Weight = 5, MinSize = 0, MaxSize = 100, GroupName = "DVD" }, + new QualityDefinition(Quality.HDTV720p) { Weight = 6, MinSize = 0, MaxSize = 100 }, + new QualityDefinition(Quality.HDTV1080p) { Weight = 7, MinSize = 0, MaxSize = 100 }, + new QualityDefinition(Quality.RAWHD) { Weight = 8, MinSize = 0, MaxSize = null }, + new QualityDefinition(Quality.WEBRip720p) { Weight = 9, MinSize = 0, MaxSize = 100, GroupName = "WEB 720p" }, + new QualityDefinition(Quality.WEBDL720p) { Weight = 9, MinSize = 0, MaxSize = 100, GroupName = "WEB 720p" }, + new QualityDefinition(Quality.Bluray720p) { Weight = 10, MinSize = 0, MaxSize = 100 }, + new QualityDefinition(Quality.WEBRip1080p) { Weight = 11, MinSize = 0, MaxSize = 100, GroupName = "WEB 1080p" }, + new QualityDefinition(Quality.WEBDL1080p) { Weight = 11, MinSize = 0, MaxSize = 100, GroupName = "WEB 1080p" }, + new QualityDefinition(Quality.Bluray1080p) { Weight = 12, MinSize = 0, MaxSize = 100 }, + new QualityDefinition(Quality.HDTV2160p) { Weight = 13, MinSize = 0, MaxSize = null }, + new QualityDefinition(Quality.WEBRip2160p) { Weight = 14, MinSize = 0, MaxSize = null, GroupName = "WEB 2160p" }, + new QualityDefinition(Quality.WEBDL2160p) { Weight = 14, MinSize = 0, MaxSize = null, GroupName = "WEB 2160p" }, + new QualityDefinition(Quality.Bluray2160p) { Weight = 15, MinSize = 0, MaxSize = null } }; } diff --git a/src/NzbDrone.Core/Qualities/QualityDefinition.cs b/src/NzbDrone.Core/Qualities/QualityDefinition.cs index 372002333..6417c05db 100644 --- a/src/NzbDrone.Core/Qualities/QualityDefinition.cs +++ b/src/NzbDrone.Core/Qualities/QualityDefinition.cs @@ -1,4 +1,5 @@ -using NzbDrone.Core.Datastore; +using Newtonsoft.Json; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Qualities @@ -9,6 +10,7 @@ namespace NzbDrone.Core.Qualities public string Title { get; set; } + public string GroupName { get; set; } public int Weight { get; set; } public double? MinSize { get; set; } diff --git a/src/NzbDrone.Core/Qualities/QualityModel.cs b/src/NzbDrone.Core/Qualities/QualityModel.cs index a4c17fc39..610dc1211 100644 --- a/src/NzbDrone.Core/Qualities/QualityModel.cs +++ b/src/NzbDrone.Core/Qualities/QualityModel.cs @@ -52,7 +52,7 @@ namespace NzbDrone.Core.Qualities return 1; } - if(definition.Weight < otherDefinition.Weight) + if (definition.Weight < otherDefinition.Weight) { return -1; } diff --git a/src/NzbDrone.Core/Qualities/QualityModelComparer.cs b/src/NzbDrone.Core/Qualities/QualityModelComparer.cs index 61be027d6..303cfd008 100644 --- a/src/NzbDrone.Core/Qualities/QualityModelComparer.cs +++ b/src/NzbDrone.Core/Qualities/QualityModelComparer.cs @@ -16,17 +16,35 @@ namespace NzbDrone.Core.Qualities _profile = profile; } + public int Compare(int left, int right, bool respectGroupOrder = false) + { + var leftIndex = _profile.GetIndex(left); + var rightIndex = _profile.GetIndex(right); + + return leftIndex.CompareTo(rightIndex, respectGroupOrder); + } + public int Compare(Quality left, Quality right) { - int leftIndex = _profile.Items.FindIndex(v => v.Quality == left); - int rightIndex = _profile.Items.FindIndex(v => v.Quality == right); + return Compare(left, right, false); + } - return leftIndex.CompareTo(rightIndex); + public int Compare(Quality left, Quality right, bool respectGroupOrder) + { + var leftIndex = _profile.GetIndex(left); + var rightIndex = _profile.GetIndex(right); + + return leftIndex.CompareTo(rightIndex, respectGroupOrder); } public int Compare(QualityModel left, QualityModel right) { - int result = Compare(left.Quality, right.Quality); + return Compare(left, right, false); + } + + public int Compare(QualityModel left, QualityModel right, bool respectGroupOrder) + { + int result = Compare(left.Quality, right.Quality, respectGroupOrder); if (result == 0) { diff --git a/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs b/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs index b3e65739e..4142fd80d 100644 --- a/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs @@ -19,14 +19,12 @@ namespace NzbDrone.Core.Tv private readonly IEpisodeRepository _episodeRepository; private readonly IProfileService _profileService; private readonly ILanguageProfileService _languageProfileService; - private readonly Logger _logger; public EpisodeCutoffService(IEpisodeRepository episodeRepository, IProfileService profileService, ILanguageProfileService languageProfileService, Logger logger) { _episodeRepository = episodeRepository; _profileService = profileService; _languageProfileService = languageProfileService; - _logger = logger; } public PagingSpec EpisodesWhereCutoffUnmet(PagingSpec pagingSpec) @@ -39,11 +37,12 @@ namespace NzbDrone.Core.Tv //Get all items less than the cutoff foreach (var profile in profiles) { - var cutoffIndex = profile.Items.FindIndex(v => v.Quality == profile.Cutoff); - var belowCutoff = profile.Items.Take(cutoffIndex).ToList(); + var cutoffIndex = profile.GetIndex(profile.Cutoff); + var belowCutoff = profile.Items.Take(cutoffIndex.Index).ToList(); + if (belowCutoff.Any()) { - qualitiesBelowCutoff.Add(new QualitiesBelowCutoff(profile.Id, belowCutoff.Select(i => i.Quality.Id))); + qualitiesBelowCutoff.Add(new QualitiesBelowCutoff(profile.Id, belowCutoff.SelectMany(i => i.GetQualities().Select(q => q.Id)))); } } diff --git a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs index 3a20d4c70..44423640e 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityCutoffValidator.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityCutoffValidator.cs new file mode 100644 index 000000000..86a531315 --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityCutoffValidator.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Validators; + +namespace Sonarr.Api.V3.Profiles.Quality +{ + public static class QualityCutoffValidator + { + public static IRuleBuilderOptions ValidCutoff(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.SetValidator(new ValidCutoffValidator()); + } + } + + public class ValidCutoffValidator : PropertyValidator + { + public ValidCutoffValidator() + : base("Cutoff must be an allowed quality or group") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var cutoff = (int)context.PropertyValue; + dynamic instance = context.ParentContext.InstanceToValidate; + var items = instance.Items as IList; + + var cutoffItem = items.SingleOrDefault(i => (i.Quality == null && i.Id == cutoff) || i.Quality?.Id == cutoff); + + if (cutoffItem == null) return false; + + if (!cutoffItem.Allowed) return false; + + return true; + } + } +} diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityItemsValidator.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityItemsValidator.cs new file mode 100644 index 000000000..2a6849ff4 --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityItemsValidator.cs @@ -0,0 +1,197 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Validators; +using NzbDrone.Common.Extensions; + +namespace Sonarr.Api.V3.Profiles.Quality +{ + public static class QualityItemsValidator + { + public static IRuleBuilderOptions> ValidItems(this IRuleBuilder> ruleBuilder) + { + ruleBuilder.SetValidator(new NotEmptyValidator(null)); + ruleBuilder.SetValidator(new AllowedValidator()); + ruleBuilder.SetValidator(new QualityNameValidator()); + ruleBuilder.SetValidator(new EmptyItemGroupNameValidator()); + ruleBuilder.SetValidator(new ItemGroupIdValidator()); + ruleBuilder.SetValidator(new UniqueIdValidator()); + ruleBuilder.SetValidator(new UniqueQualityIdValidator()); + return ruleBuilder.SetValidator(new ItemGroupNameValidator()); + } + } + + public class AllowedValidator : PropertyValidator + { + public AllowedValidator() + : base("Must contain at least one allowed quality") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var list = context.PropertyValue as IList; + + if (list == null) + { + return false; + } + + if (!list.Any(c => c.Allowed)) + { + return false; + } + + return true; + } + } + + public class EmptyItemGroupNameValidator : PropertyValidator + { + public EmptyItemGroupNameValidator() + : base("Groups must not be empty") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var items = context.PropertyValue as IList; + + if (items.Any(i => i.Name.IsNotNullOrWhiteSpace() && i.Items.Empty())) + { + return false; + } + + return true; + } + } + + public class QualityNameValidator : PropertyValidator + { + public QualityNameValidator() + : base("Individual qualities should not be named") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var items = context.PropertyValue as IList; + + if (items.Any(i => i.Name.IsNotNullOrWhiteSpace() && i.Quality != null)) + { + return false; + } + + return true; + } + } + + public class ItemGroupNameValidator : PropertyValidator + { + public ItemGroupNameValidator() + : base("Groups must have a name") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var items = context.PropertyValue as IList; + + if (items.Any(i => i.Quality == null && i.Name.IsNullOrWhiteSpace())) + { + return false; + } + + return true; + } + } + + public class ItemGroupIdValidator : PropertyValidator + { + public ItemGroupIdValidator() + : base("Groups must have an ID") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var items = context.PropertyValue as IList; + + if (items.Any(i => i.Quality == null && i.Id == 0)) + { + return false; + } + + return true; + } + } + + public class UniqueIdValidator : PropertyValidator + { + public UniqueIdValidator() + : base("Groups must have a unique ID") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var items = context.PropertyValue as IList; + + if (items.Where(i => i.Id > 0).Select(i => i.Id).GroupBy(i => i).Any(g => g.Count() > 1)) + { + return false; + } + + return true; + } + } + + public class UniqueQualityIdValidator : PropertyValidator + { + public UniqueQualityIdValidator() + : base("Qualities can only be used once") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var items = context.PropertyValue as IList; + var qualityIds = new HashSet(); + + foreach (var item in items) + { + if (item.Id > 0) + { + foreach (var quality in item.Items) + { + if (qualityIds.Contains(quality.Quality.Id)) + { + return false; + } + + qualityIds.Add(quality.Quality.Id); + } + } + + else + { + if (qualityIds.Contains(item.Quality.Id)) + { + return false; + } + + qualityIds.Add(item.Quality.Id); + } + } + + return true; + } + } +} diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileModule.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileModule.cs index 12588af1f..a57fb3253 100644 --- a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileModule.cs +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileModule.cs @@ -13,8 +13,10 @@ namespace Sonarr.Api.V3.Profiles.Quality { _profileService = profileService; SharedValidator.RuleFor(c => c.Name).NotEmpty(); - SharedValidator.RuleFor(c => c.Cutoff).NotNull(); - SharedValidator.RuleFor(c => c.Items).MustHaveAllowedQuality(); + // TODO: Need to validate the cutoff is allowed and the ID/quality ID exists + // TODO: Need to validate the Items to ensure groups have names and at no item has no name, no items and no quality + SharedValidator.RuleFor(c => c.Cutoff).ValidCutoff(); + SharedValidator.RuleFor(c => c.Items).ValidItems(); GetResourceAll = GetAll; GetResourceById = GetById; diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs index 8a72ab313..674d84d1a 100644 --- a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs @@ -8,14 +8,21 @@ namespace Sonarr.Api.V3.Profiles.Quality public class QualityProfileResource : RestResource { public string Name { get; set; } - public NzbDrone.Core.Qualities.Quality Cutoff { get; set; } + public int Cutoff { get; set; } public List Items { get; set; } } public class QualityProfileQualityItemResource : RestResource { + public string Name { get; set; } public NzbDrone.Core.Qualities.Quality Quality { get; set; } + public List Items { get; set; } public bool Allowed { get; set; } + + public QualityProfileQualityItemResource() + { + Items = new List(); + } } public static class ProfileResourceMapper @@ -27,7 +34,6 @@ namespace Sonarr.Api.V3.Profiles.Quality return new QualityProfileResource { Id = model.Id, - Name = model.Name, Cutoff = model.Cutoff, Items = model.Items.ConvertAll(ToResource), @@ -40,7 +46,10 @@ namespace Sonarr.Api.V3.Profiles.Quality return new QualityProfileQualityItemResource { + Id = model.Id, + Name = model.Name, Quality = model.Quality, + Items = model.Items.ConvertAll(ToResource), Allowed = model.Allowed }; } @@ -52,9 +61,8 @@ namespace Sonarr.Api.V3.Profiles.Quality return new Profile { Id = resource.Id, - Name = resource.Name, - Cutoff = (NzbDrone.Core.Qualities.Quality)resource.Cutoff.Id, + Cutoff = resource.Cutoff, Items = resource.Items.ConvertAll(ToModel) }; } @@ -65,7 +73,10 @@ namespace Sonarr.Api.V3.Profiles.Quality return new ProfileQualityItem { - Quality = (NzbDrone.Core.Qualities.Quality)resource.Quality.Id, + Id = resource.Id, + Name = resource.Name, + Quality = resource.Quality != null ? (NzbDrone.Core.Qualities.Quality)resource.Quality.Id : null, + Items = resource.Items.ConvertAll(ToModel), Allowed = resource.Allowed }; } diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileSchemaModule.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileSchemaModule.cs index afcdfd82b..c2c7b7f0e 100644 --- a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileSchemaModule.cs +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileSchemaModule.cs @@ -1,34 +1,24 @@ -using System.Linq; using NzbDrone.Core.Profiles.Qualities; -using NzbDrone.Core.Qualities; using Sonarr.Http; namespace Sonarr.Api.V3.Profiles.Quality { public class QualityProfileSchemaModule : SonarrRestModule { - private readonly IQualityDefinitionService _qualityDefinitionService; + private readonly IProfileService _profileService; - public QualityProfileSchemaModule(IQualityDefinitionService qualityDefinitionService) + public QualityProfileSchemaModule(IProfileService profileService) : base("/qualityprofile/schema") { - _qualityDefinitionService = qualityDefinitionService; - + _profileService = profileService; GetResourceSingle = GetSchema; } private QualityProfileResource GetSchema() { - var items = _qualityDefinitionService.All() - .OrderBy(v => v.Weight) - .Select(v => new ProfileQualityItem { Quality = v.Quality, Allowed = false }) - .ToList(); - - var qualityProfile = new Profile(); - qualityProfile.Cutoff = NzbDrone.Core.Qualities.Quality.Unknown; - qualityProfile.Items = items; + var qualityProfile = _profileService.GetDefaultProfile(string.Empty); return qualityProfile.ToResource(); } } -} \ No newline at end of file +} diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileValidation.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileValidation.cs deleted file mode 100644 index f1f62fa89..000000000 --- a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileValidation.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FluentValidation; -using FluentValidation.Validators; - -namespace Sonarr.Api.V3.Profiles.Quality -{ - public static class QualityProfileValidation - { - public static IRuleBuilderOptions> MustHaveAllowedQuality(this IRuleBuilder> ruleBuilder) - { - ruleBuilder.SetValidator(new NotEmptyValidator(null)); - - return ruleBuilder.SetValidator(new AllowedValidator()); - } - } - - public class AllowedValidator : PropertyValidator - { - public AllowedValidator() - : base("Must contain at least one allowed quality") - { - - } - - protected override bool IsValid(PropertyValidatorContext context) - { - var list = context.PropertyValue as IList; - - if (list == null) - { - return false; - } - - if (!list.Any(c => c.Allowed)) - { - return false; - } - - return true; - } - } -} diff --git a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj index 22229c496..540aec752 100644 --- a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj +++ b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj @@ -170,7 +170,6 @@ -