Quality groups

pull/2789/head
Mark McDowall 7 years ago committed by Taloth Saldono
parent 068ea1e934
commit f31ac39e37

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

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

@ -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<ProfileResource> { profile.ToResource() };

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

@ -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<add_webrip_and_br480_qualites_in_profile>
{
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<Profile117>("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<Profile117>("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<Profile117>("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<Profile117>("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);
}
}
}

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

@ -35,7 +35,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
public void Setup()
{
var fakeSeries = Builder<Series>.CreateNew()
.With(c => c.Profile = (LazyLoaded<Profile>)new Profile { Cutoff = Quality.Bluray1080p })
.With(c => c.Profile = (LazyLoaded<Profile>)new Profile { Cutoff = Quality.Bluray1080p.Id })
.Build();
remoteEpisode = new RemoteEpisode

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

@ -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<RemoteEpisode>.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<RemoteEpisode>.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<RemoteEpisode>.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<RemoteEpisode>.CreateNew()
.With(r => r.Series = _series)

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

@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
};
var fakeSeries = Builder<Series>.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();

@ -47,7 +47,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
};
_fakeSeries = Builder<Series>.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<QualityModel, Language>(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<QualityModel, Language>(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<QualityModel, Language>(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<QualityModel, Language>(new QualityModel(Quality.WEBDL1080p, new Revision(version: 1)), Language.Spanish);

@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
var doubleEpisodeList = new List<Episode> { new Episode { EpisodeFile = _firstFile, EpisodeFileId = 1 }, new Episode { EpisodeFile = _secondFile, EpisodeFileId = 1 }, new Episode { EpisodeFile = null } };
var fakeSeries = Builder<Series>.CreateNew()
.With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p })
.With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p.Id })
.Build();
_parseResultMulti = new RemoteEpisode

@ -42,7 +42,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
var languages = Languages.LanguageFixture.GetDefaultLanguages(Language.English, Language.Spanish);
var fakeSeries = Builder<Series>.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();

@ -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<ProfileQualityItem>
{
new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p },

@ -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<ProfileQualityItem>
{
new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p },

@ -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<ProfileQualityItem>
{
new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p },

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

@ -135,6 +135,7 @@
<Compile Include="Datastore\DatabaseRelationshipFixture.cs" />
<Compile Include="Datastore\MappingExtentionFixture.cs" />
<Compile Include="Datastore\MarrDataLazyLoadingFixture.cs" />
<Compile Include="Datastore\Migration\117_add_webrip_qualites_in_profileFixture.cs" />
<Compile Include="Datastore\Migration\108_fix_metadata_file_extensionsFixture.cs" />
<Compile Include="Datastore\Migration\109_import_extra_files_configFixture.cs" />
<Compile Include="Datastore\Migration\110_fix_extra_files_configFixture.cs" />
@ -343,6 +344,7 @@
<Compile Include="ParserTests\MiniSeriesEpisodeParserFixture.cs" />
<Compile Include="ParserTests\ValidateParsedEpisodeInfoFixture.cs" />
<Compile Include="Profiles\Delay\DelayProfileServiceFixture.cs" />
<Compile Include="Profiles\Qualities\QualityIndexCompareToFixture.cs" />
<Compile Include="Qualities\RevisionComparableFixture.cs" />
<Compile Include="QueueTests\QueueServiceFixture.cs" />
<Compile Include="RemotePathMappingsTests\RemotePathMappingServiceFixture.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)]

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

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

@ -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<ProfileQualityItem>
{
new ProfileQualityItem
{
Allowed = false,
Quality = Quality.SDTV
},
new ProfileQualityItem
{
Allowed = false,
Quality = Quality.DVD
},
new ProfileQualityItem
{
Allowed = true,
Items = new List<ProfileQualityItem>
{
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);
}
}
}

@ -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<ProfileQualityItem>
{
new ProfileQualityItem { Allowed = true, Quality = Quality.SDTV },

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

@ -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<ProfileItem117> 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<ProfileItem117> Items { get; set; }
public bool Allowed { get; set; }
public ProfileItem117()
{
Items = new List<ProfileItem117>();
}
}
public class ProfileUpdater116
{
private readonly IDbConnection _connection;
private readonly IDbTransaction _transaction;
private List<Profile117> _profiles;
private HashSet<Profile117> _changedProfiles = new HashSet<Profile117>();
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<Profile117> GetProfiles()
{
var profiles = new List<Profile117>();
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<List<ProfileItem117>>(profileReader.GetString(3))
});
}
}
}
return profiles;
}
}
}

@ -103,6 +103,7 @@ namespace NzbDrone.Core.Datastore
.HasOne(episode => episode.EpisodeFile, episode => episode.EpisodeFileId);
Mapper.Entity<QualityDefinition>().RegisterModel("QualityDefinitions")
.Ignore(d => d.GroupName)
.Ignore(d => d.Weight);
Mapper.Entity<Profile>().RegisterModel("Profiles");

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

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

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

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

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

@ -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))
{

@ -320,6 +320,7 @@
<Compile Include="Datastore\Migration\111_create_language_profiles.cs" />
<Compile Include="Datastore\Migration\115_add_downloadclient_status.cs" />
<Compile Include="Datastore\Migration\121_update_animetosho_url.cs" />
<Compile Include="Datastore\Migration\117_add_webrip_and_br480_qualites_in_profile.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationContext.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationController.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationDbFactory.cs" />
@ -1003,6 +1004,7 @@
<Compile Include="Profiles\Qualities\ProfileQualityItem.cs" />
<Compile Include="Profiles\Qualities\ProfileRepository.cs" />
<Compile Include="Profiles\Qualities\ProfileService.cs" />
<Compile Include="Profiles\Qualities\QualityIndex.cs" />
<Compile Include="ProgressMessaging\ProgressMessageContext.cs" />
<Compile Include="Qualities\QualityDetectionSource.cs" />
<Compile Include="Qualities\QualitySource.cs" />

@ -16,7 +16,8 @@ namespace NzbDrone.Core.Parser
private static readonly Regex SourceRegex = new Regex(@"\b(?:
(?<bluray>BluRay|Blu-Ray|HD-?DVD|BD)|
(?<webdl>WEB[-_. ]DL|WEBDL|WebRip|AmazonHD|iTunesHD|NetflixU?HD|WebHD|[. ]WEB[. ](?:[xh]26[45]|DDP?5[. ]1)|\d+0p[. ]WEB[. ]|WEB-DLMux)|
(?<webdl>WEB[-_. ]DL|WEBDL|AmazonHD|iTunesHD|NetflixU?HD|WebHD|[. ]WEB[. ](?:[xh]26[45]|DDP?5[. ]1)|\d+0p[. ]WEB[. ]|WEB-DLMux)|
(?<webrip>WebRip)|
(?<hdtv>HDTV)|
(?<bdrip>BDRip)|
(?<brrip>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;
}
}

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

@ -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<ProfileQualityItem> Items { get; set; }
public bool Allowed { get; set; }
public ProfileQualityItem()
{
Items = new List<ProfileQualityItem>();
}
public List<Quality> GetQualities()
{
if (Quality == null)
{
return Items.Select(s => s.Quality).ToList();
}
return new List<Quality>{ Quality };
}
public override string ToString()
{
var qualitiesString = string.Join(", ", GetQualities());
if (Name.IsNotNullOrWhiteSpace())
{
return $"{Name} ({qualitiesString})";
}
return qualitiesString;
}
}
}

@ -16,6 +16,7 @@ namespace NzbDrone.Core.Profiles.Qualities
List<Profile> All();
Profile Get(int id);
bool Exists(int id);
Profile GetDefaultProfile(string name, Quality cutoff = null, params Quality[] allowed);
}
public class ProfileService : IProfileService, IHandle<ApplicationStartedEvent>
@ -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<ProfileQualityItem>();
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);
}
}
}

@ -0,0 +1,55 @@
using System;
namespace NzbDrone.Core.Profiles.Qualities
{
public class QualityIndex : IComparable, IComparable<QualityIndex>
{
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;;
}
}
}

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

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

@ -52,7 +52,7 @@ namespace NzbDrone.Core.Qualities
return 1;
}
if(definition.Weight < otherDefinition.Weight)
if (definition.Weight < otherDefinition.Weight)
{
return -1;
}

@ -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);
}
public int Compare(Quality left, Quality right, bool respectGroupOrder)
{
var leftIndex = _profile.GetIndex(left);
var rightIndex = _profile.GetIndex(right);
return leftIndex.CompareTo(rightIndex);
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)
{

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

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

@ -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<T, int> ValidCutoff<T>(this IRuleBuilder<T, int> ruleBuilder)
{
return ruleBuilder.SetValidator(new ValidCutoffValidator<T>());
}
}
public class ValidCutoffValidator<T> : 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<QualityProfileQualityItemResource>;
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;
}
}
}

@ -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<T, IList<QualityProfileQualityItemResource>> ValidItems<T>(this IRuleBuilder<T, IList<QualityProfileQualityItemResource>> ruleBuilder)
{
ruleBuilder.SetValidator(new NotEmptyValidator(null));
ruleBuilder.SetValidator(new AllowedValidator<T>());
ruleBuilder.SetValidator(new QualityNameValidator<T>());
ruleBuilder.SetValidator(new EmptyItemGroupNameValidator<T>());
ruleBuilder.SetValidator(new ItemGroupIdValidator<T>());
ruleBuilder.SetValidator(new UniqueIdValidator<T>());
ruleBuilder.SetValidator(new UniqueQualityIdValidator<T>());
return ruleBuilder.SetValidator(new ItemGroupNameValidator<T>());
}
}
public class AllowedValidator<T> : PropertyValidator
{
public AllowedValidator()
: base("Must contain at least one allowed quality")
{
}
protected override bool IsValid(PropertyValidatorContext context)
{
var list = context.PropertyValue as IList<QualityProfileQualityItemResource>;
if (list == null)
{
return false;
}
if (!list.Any(c => c.Allowed))
{
return false;
}
return true;
}
}
public class EmptyItemGroupNameValidator<T> : PropertyValidator
{
public EmptyItemGroupNameValidator()
: base("Groups must not be empty")
{
}
protected override bool IsValid(PropertyValidatorContext context)
{
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
if (items.Any(i => i.Name.IsNotNullOrWhiteSpace() && i.Items.Empty()))
{
return false;
}
return true;
}
}
public class QualityNameValidator<T> : PropertyValidator
{
public QualityNameValidator()
: base("Individual qualities should not be named")
{
}
protected override bool IsValid(PropertyValidatorContext context)
{
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
if (items.Any(i => i.Name.IsNotNullOrWhiteSpace() && i.Quality != null))
{
return false;
}
return true;
}
}
public class ItemGroupNameValidator<T> : PropertyValidator
{
public ItemGroupNameValidator()
: base("Groups must have a name")
{
}
protected override bool IsValid(PropertyValidatorContext context)
{
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
if (items.Any(i => i.Quality == null && i.Name.IsNullOrWhiteSpace()))
{
return false;
}
return true;
}
}
public class ItemGroupIdValidator<T> : PropertyValidator
{
public ItemGroupIdValidator()
: base("Groups must have an ID")
{
}
protected override bool IsValid(PropertyValidatorContext context)
{
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
if (items.Any(i => i.Quality == null && i.Id == 0))
{
return false;
}
return true;
}
}
public class UniqueIdValidator<T> : PropertyValidator
{
public UniqueIdValidator()
: base("Groups must have a unique ID")
{
}
protected override bool IsValid(PropertyValidatorContext context)
{
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
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<T> : PropertyValidator
{
public UniqueQualityIdValidator()
: base("Qualities can only be used once")
{
}
protected override bool IsValid(PropertyValidatorContext context)
{
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
var qualityIds = new HashSet<int>();
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;
}
}
}

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

@ -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<QualityProfileQualityItemResource> Items { 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 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
};
}

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

@ -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<T, IList<QualityProfileQualityItemResource>> MustHaveAllowedQuality<T>(this IRuleBuilder<T, IList<QualityProfileQualityItemResource>> ruleBuilder)
{
ruleBuilder.SetValidator(new NotEmptyValidator(null));
return ruleBuilder.SetValidator(new AllowedValidator<T>());
}
}
public class AllowedValidator<T> : PropertyValidator
{
public AllowedValidator()
: base("Must contain at least one allowed quality")
{
}
protected override bool IsValid(PropertyValidatorContext context)
{
var list = context.PropertyValue as IList<QualityProfileQualityItemResource>;
if (list == null)
{
return false;
}
if (!list.Any(c => c.Allowed))
{
return false;
}
return true;
}
}
}

@ -170,7 +170,6 @@
<Compile Include="Profiles\Quality\QualityProfileModule.cs" />
<Compile Include="Profiles\Quality\QualityProfileResource.cs" />
<Compile Include="Profiles\Quality\QualityProfileSchemaModule.cs" />
<Compile Include="Profiles\Quality\QualityProfileValidation.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ProviderModuleBase.cs" />
<Compile Include="ProviderResource.cs" />

Loading…
Cancel
Save