From 2c004e1f9665763111fcd964b81338bdbe735865 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 9 Jan 2023 08:58:25 -0800 Subject: [PATCH] Fixed: Unable to load UI if Quality Profiles contain removed Custom Format items --- ...CleanupQualityProfileFormatItemsFixture.cs | 81 +++++++++++++++++++ .../CleanupQualityProfileFormatItems.cs | 72 +++++++++++++++++ .../Qualities/QualityProfileRepository.cs | 12 ++- .../Qualities/QualityProfileService.cs | 5 +- 4 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupQualityProfileFormatItemsFixture.cs create mode 100644 src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupQualityProfileFormatItems.cs 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..53d1f0e5b --- /dev/null +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupQualityProfileFormatItemsFixture.cs @@ -0,0 +1,81 @@ +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.Profiles.Qualities; +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())); + } + + [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 = Builder.CreateListOfSize(1).Build().ToList()) + .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(f => f.Id = customFormat.Id).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); + } + } +} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupQualityProfileFormatItems.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupQualityProfileFormatItems.cs new file mode 100644 index 000000000..6c04ce114 --- /dev/null +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupQualityProfileFormatItems.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Linq; +using Dapper; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Profiles.Qualities; + +namespace NzbDrone.Core.Housekeeping.Housekeepers +{ + public class CleanupQualityProfileFormatItems : IHousekeepingTask + { + private readonly IMainDatabase _database; + private readonly IQualityProfileFormatItemsCleanupRepository _repository; + + public CleanupQualityProfileFormatItems(IMainDatabase database, IQualityProfileFormatItemsCleanupRepository repository) + { + _database = database; + _repository = repository; + } + + public void Clean() + { + var customFormatIds = GetCustomFormatIds(); + var profiles = _repository.All(); + var updatedProfiles = new List(); + + foreach (var profile in profiles) + { + var formatItems = profile.FormatItems.Where(f => customFormatIds.Contains(f.Id)).ToList(); + + if (formatItems.Count != profile.FormatItems.Count) + { + 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); + } + } + + private HashSet GetCustomFormatIds() + { + using (var mapper = _database.OpenConnection()) + { + return new HashSet(mapper.Query("SELECT Id FROM CustomFormats")); + } + } + } + + 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/Qualities/QualityProfileRepository.cs b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileRepository.cs index 723fc6223..65b82eb75 100644 --- a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileRepository.cs +++ b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileRepository.cs @@ -34,10 +34,20 @@ namespace NzbDrone.Core.Profiles.Qualities // 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/Qualities/QualityProfileService.cs b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs index 248c702ff..aaa657456 100644 --- a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs +++ b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.CustomFormats.Events; using NzbDrone.Core.ImportLists; @@ -149,6 +150,7 @@ namespace NzbDrone.Core.Profiles.Qualities public void Handle(CustomFormatAddedEvent message) { var all = All(); + foreach (var profile in all) { profile.FormatItems.Insert(0, new ProfileFormatItem @@ -167,7 +169,8 @@ namespace NzbDrone.Core.Profiles.Qualities foreach (var profile in all) { profile.FormatItems = profile.FormatItems.Where(c => c.Format.Id != message.CustomFormat.Id).ToList(); - if (!profile.FormatItems.Any()) + + if (profile.FormatItems.Empty()) { profile.MinFormatScore = 0; profile.CutoffFormatScore = 0;