diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeAllowedSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeAllowedSpecificationFixture.cs index 202f4bd8b..527ea5bba 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeAllowedSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeAllowedSpecificationFixture.cs @@ -40,13 +40,11 @@ public void Setup() { new ProfileFormatItem { - Id = 1, Format = _customFormatOne, Score = 50 }, new ProfileFormatItem { - Id = 1, Format = _customFormatTwo, Score = 100 } diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupQualityProfileFormatItemsFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupQualityProfileFormatItemsFixture.cs new file mode 100644 index 000000000..ae9878b69 --- /dev/null +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupQualityProfileFormatItemsFixture.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Housekeeping.Housekeepers; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Housekeeping.Housekeepers +{ + [TestFixture] + public class CleanupQualityProfileFormatItemsFixture : DbTest + { + [SetUp] + public void Setup() + { + Mocker.SetConstant( + new QualityProfileFormatItemsCleanupRepository(Mocker.Resolve(), Mocker.Resolve())); + + Mocker.SetConstant( + new CustomFormatRepository(Mocker.Resolve(), Mocker.Resolve())); + } + + [Test] + public void should_remove_orphaned_custom_formats() + { + var qualityProfile = Builder.CreateNew() + .With(h => h.Items = Qualities.QualityFixture.GetDefaultQualities()) + .With(h => h.MinFormatScore = 50) + .With(h => h.CutoffFormatScore = 100) + .With(h => h.FormatItems = new List + { + Builder.CreateNew() + .With(c => c.Format = new CustomFormat("My Custom Format") { Id = 0 }) + .Build() + }) + .BuildNew(); + + Db.Insert(qualityProfile); + Subject.Clean(); + + var result = AllStoredModels; + + result.Should().HaveCount(1); + result.First().FormatItems.Should().BeEmpty(); + result.First().MinFormatScore.Should().Be(0); + result.First().CutoffFormatScore.Should().Be(0); + } + + [Test] + public void should_not_remove_unorphaned_custom_formats() + { + var minFormatScore = 50; + var cutoffFormatScore = 100; + + var customFormat = Builder.CreateNew() + .With(h => h.Specifications = new List()) + .BuildNew(); + + Db.Insert(customFormat); + + var qualityProfile = Builder.CreateNew() + .With(h => h.Items = Qualities.QualityFixture.GetDefaultQualities()) + .With(h => h.MinFormatScore = minFormatScore) + .With(h => h.CutoffFormatScore = cutoffFormatScore) + .With(h => h.FormatItems = new List + { + Builder.CreateNew() + .With(c => c.Format = customFormat) + .Build() + }) + .BuildNew(); + + Db.Insert(qualityProfile); + + Subject.Clean(); + var result = AllStoredModels; + + result.Should().HaveCount(1); + result.First().FormatItems.Should().HaveCount(1); + result.First().MinFormatScore.Should().Be(minFormatScore); + result.First().CutoffFormatScore.Should().Be(cutoffFormatScore); + } + + [Test] + public void should_add_missing_custom_formats() + { + var minFormatScore = 50; + var cutoffFormatScore = 100; + + var customFormat1 = Builder.CreateNew() + .With(h => h.Id = 1) + .With(h => h.Name = "Custom Format 1") + .With(h => h.Specifications = new List()) + .BuildNew(); + + var customFormat2 = Builder.CreateNew() + .With(h => h.Id = 2) + .With(h => h.Name = "Custom Format 2") + .With(h => h.Specifications = new List()) + .BuildNew(); + + Db.Insert(customFormat1); + Db.Insert(customFormat2); + + var qualityProfile = Builder.CreateNew() + .With(h => h.Items = Qualities.QualityFixture.GetDefaultQualities()) + .With(h => h.MinFormatScore = minFormatScore) + .With(h => h.CutoffFormatScore = cutoffFormatScore) + .With(h => h.FormatItems = new List + { + Builder.CreateNew() + .With(c => c.Format = customFormat1) + .Build() + }) + .BuildNew(); + + Db.Insert(qualityProfile); + + Subject.Clean(); + var result = AllStoredModels; + + result.Should().HaveCount(1); + result.First().FormatItems.Should().HaveCount(2); + result.First().MinFormatScore.Should().Be(minFormatScore); + result.First().CutoffFormatScore.Should().Be(cutoffFormatScore); + } + } +} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupQualityProfileFormatItems.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupQualityProfileFormatItems.cs new file mode 100644 index 000000000..73c386a00 --- /dev/null +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupQualityProfileFormatItems.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Profiles; + +namespace NzbDrone.Core.Housekeeping.Housekeepers +{ + public class CleanupQualityProfileFormatItems : IHousekeepingTask + { + private readonly IQualityProfileFormatItemsCleanupRepository _repository; + private readonly ICustomFormatRepository _customFormatRepository; + + public CleanupQualityProfileFormatItems(IQualityProfileFormatItemsCleanupRepository repository, + ICustomFormatRepository customFormatRepository) + { + _repository = repository; + _customFormatRepository = customFormatRepository; + } + + public void Clean() + { + var customFormats = _customFormatRepository.All().ToDictionary(c => c.Id); + var profiles = _repository.All(); + var updatedProfiles = new List(); + + foreach (var profile in profiles) + { + var formatItems = new List(); + + // Make sure the profile doesn't include formats that have been removed + profile.FormatItems.ForEach(p => + { + if (p.Format != null && customFormats.ContainsKey(p.Format.Id)) + { + formatItems.Add(p); + } + }); + + // Make sure the profile includes all available formats + foreach (var customFormat in customFormats) + { + if (formatItems.None(f => f.Format.Id == customFormat.Key)) + { + formatItems.Insert(0, new ProfileFormatItem + { + Format = customFormat.Value, + Score = 0 + }); + } + } + + var previousIds = profile.FormatItems.Select(i => i.Format.Id).ToList(); + var ids = formatItems.Select(i => i.Format.Id).ToList(); + + // Update the profile if any formats were added or removed + if (ids.Except(previousIds).Any() || previousIds.Except(ids).Any()) + { + profile.FormatItems = formatItems; + + if (profile.FormatItems.Empty()) + { + profile.MinFormatScore = 0; + profile.CutoffFormatScore = 0; + } + + updatedProfiles.Add(profile); + } + } + + if (updatedProfiles.Any()) + { + _repository.SetFields(updatedProfiles, p => p.FormatItems, p => p.MinFormatScore, p => p.CutoffFormatScore); + } + } + } + + public interface IQualityProfileFormatItemsCleanupRepository : IBasicRepository + { + } + + public class QualityProfileFormatItemsCleanupRepository : BasicRepository, IQualityProfileFormatItemsCleanupRepository + { + public QualityProfileFormatItemsCleanupRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + } +} diff --git a/src/NzbDrone.Core/Profiles/ProfileFormatItem.cs b/src/NzbDrone.Core/Profiles/ProfileFormatItem.cs index 30b3582ee..7f498d172 100644 --- a/src/NzbDrone.Core/Profiles/ProfileFormatItem.cs +++ b/src/NzbDrone.Core/Profiles/ProfileFormatItem.cs @@ -1,4 +1,3 @@ -using System.Text.Json.Serialization; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Datastore; @@ -6,8 +5,6 @@ namespace NzbDrone.Core.Profiles { public class ProfileFormatItem : IEmbeddedDocument { - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public int Id { get; set; } public CustomFormat Format { get; set; } public int Score { get; set; } } diff --git a/src/NzbDrone.Core/Profiles/ProfileRepository.cs b/src/NzbDrone.Core/Profiles/ProfileRepository.cs index 8d312a53c..e0462f73c 100644 --- a/src/NzbDrone.Core/Profiles/ProfileRepository.cs +++ b/src/NzbDrone.Core/Profiles/ProfileRepository.cs @@ -34,10 +34,20 @@ protected override List Query(SqlBuilder builder) // all the custom formats foreach (var profile in profiles) { + var formatItems = new List(); + foreach (var formatItem in profile.FormatItems) { - formatItem.Format = cfs[formatItem.Format.Id]; + // Skip any format that has been removed, but the profile wasn't updated properly + if (cfs.ContainsKey(formatItem.Format.Id)) + { + formatItem.Format = cfs[formatItem.Format.Id]; + + formatItems.Add(formatItem); + } } + + profile.FormatItems = formatItems; } return profiles; diff --git a/src/NzbDrone.Core/Profiles/ProfileService.cs b/src/NzbDrone.Core/Profiles/ProfileService.cs index 09e79ed4a..afbe8c2a5 100644 --- a/src/NzbDrone.Core/Profiles/ProfileService.cs +++ b/src/NzbDrone.Core/Profiles/ProfileService.cs @@ -250,7 +250,6 @@ public Profile GetDefaultProfile(string name, Quality cutoff = null, params Qual var formatItems = _formatService.All().Select(format => new ProfileFormatItem { - Id = format.Id, Score = 0, Format = format }).ToList();