From 1379166c075c57a2cfbda0d70b16601060d63d56 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 8 Jan 2025 18:54:25 -0800 Subject: [PATCH] Fixed: Series being unmonitored when still in Import List Closes #7555 --- .../ImportListItemServiceFixture.cs | 32 ++--- .../ImportListSyncServiceFixture.cs | 116 ++++++++++++++++-- .../Migration/217_add_mal_and_anilist_ids.cs | 15 +++ .../ImportListItemRepository.cs | 22 +--- .../ImportListItems/ImportListItemService.cs | 32 +++-- .../ImportLists/ImportListSyncService.cs | 12 +- .../SkyHook/Resource/ShowResource.cs | 3 +- .../MetadataSource/SkyHook/SkyHookProxy.cs | 2 + src/NzbDrone.Core/Tv/RefreshSeriesService.cs | 2 + src/NzbDrone.Core/Tv/Series.cs | 4 + 10 files changed, 173 insertions(+), 67 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/217_add_mal_and_anilist_ids.cs diff --git a/src/NzbDrone.Core.Test/ImportListTests/ImportListItemServiceFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/ImportListItemServiceFixture.cs index ff6ac9afa..c0fcb9dcc 100644 --- a/src/NzbDrone.Core.Test/ImportListTests/ImportListItemServiceFixture.cs +++ b/src/NzbDrone.Core.Test/ImportListTests/ImportListItemServiceFixture.cs @@ -14,7 +14,7 @@ namespace NzbDrone.Core.Test.ImportListTests { private void GivenExisting(List existing) { - Mocker.GetMock() + Mocker.GetMock() .Setup(v => v.GetAllForLists(It.IsAny>())) .Returns(existing); } @@ -58,13 +58,13 @@ namespace NzbDrone.Core.Test.ImportListTests numDeleted.Should().Be(1); - Mocker.GetMock() + Mocker.GetMock() .Verify(v => v.InsertMany(It.Is>(s => s.Count == 1 && s[0].TvdbId == newItem.TvdbId)), Times.Once()); - Mocker.GetMock() + Mocker.GetMock() .Verify(v => v.UpdateMany(It.Is>(s => s.Count == 1 && s[0].TvdbId == updatedItem.TvdbId)), Times.Once()); - Mocker.GetMock() + Mocker.GetMock() .Verify(v => v.DeleteMany(It.Is>(s => s.Count == 1 && s[0].TvdbId != newItem.TvdbId && s[0].TvdbId != updatedItem.TvdbId)), Times.Once()); } @@ -107,13 +107,13 @@ namespace NzbDrone.Core.Test.ImportListTests numDeleted.Should().Be(1); - Mocker.GetMock() + Mocker.GetMock() .Verify(v => v.InsertMany(It.Is>(s => s.Count == 1 && s[0].ImdbId == newItem.ImdbId)), Times.Once()); - Mocker.GetMock() + Mocker.GetMock() .Verify(v => v.UpdateMany(It.Is>(s => s.Count == 1 && s[0].ImdbId == updatedItem.ImdbId)), Times.Once()); - Mocker.GetMock() + Mocker.GetMock() .Verify(v => v.DeleteMany(It.Is>(s => s.Count == 1 && s[0].ImdbId != newItem.ImdbId && s[0].ImdbId != updatedItem.ImdbId)), Times.Once()); } @@ -156,13 +156,13 @@ namespace NzbDrone.Core.Test.ImportListTests numDeleted.Should().Be(1); - Mocker.GetMock() + Mocker.GetMock() .Verify(v => v.InsertMany(It.Is>(s => s.Count == 1 && s[0].TmdbId == newItem.TmdbId)), Times.Once()); - Mocker.GetMock() + Mocker.GetMock() .Verify(v => v.UpdateMany(It.Is>(s => s.Count == 1 && s[0].TmdbId == updatedItem.TmdbId)), Times.Once()); - Mocker.GetMock() + Mocker.GetMock() .Verify(v => v.DeleteMany(It.Is>(s => s.Count == 1 && s[0].TmdbId != newItem.TmdbId && s[0].TmdbId != updatedItem.TmdbId)), Times.Once()); } @@ -205,13 +205,13 @@ namespace NzbDrone.Core.Test.ImportListTests numDeleted.Should().Be(1); - Mocker.GetMock() + Mocker.GetMock() .Verify(v => v.InsertMany(It.Is>(s => s.Count == 1 && s[0].MalId == newItem.MalId)), Times.Once()); - Mocker.GetMock() + Mocker.GetMock() .Verify(v => v.UpdateMany(It.Is>(s => s.Count == 1 && s[0].MalId == updatedItem.MalId)), Times.Once()); - Mocker.GetMock() + Mocker.GetMock() .Verify(v => v.DeleteMany(It.Is>(s => s.Count == 1 && s[0].MalId != newItem.MalId && s[0].MalId != updatedItem.MalId)), Times.Once()); } @@ -254,13 +254,13 @@ namespace NzbDrone.Core.Test.ImportListTests numDeleted.Should().Be(1); - Mocker.GetMock() + Mocker.GetMock() .Verify(v => v.InsertMany(It.Is>(s => s.Count == 1 && s[0].AniListId == newItem.AniListId)), Times.Once()); - Mocker.GetMock() + Mocker.GetMock() .Verify(v => v.UpdateMany(It.Is>(s => s.Count == 1 && s[0].AniListId == updatedItem.AniListId)), Times.Once()); - Mocker.GetMock() + Mocker.GetMock() .Verify(v => v.DeleteMany(It.Is>(s => s.Count == 1 && s[0].AniListId != newItem.AniListId && s[0].AniListId != updatedItem.AniListId)), Times.Once()); } } diff --git a/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs index 26e8cab7e..d0491fb74 100644 --- a/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs +++ b/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs @@ -42,24 +42,45 @@ namespace NzbDrone.Core.Test.ImportListTests .TheFirst(1) .With(s => s.TvdbId = 6) .With(s => s.ImdbId = "6") + .With(s => s.TmdbId = 6) + .With(s => s.MalIds = new HashSet { 6 }) + .With(s => s.AniListIds = new HashSet { 6 }) + .With(s => s.Monitored = true) .TheNext(1) .With(s => s.TvdbId = 7) .With(s => s.ImdbId = "7") + .With(s => s.TmdbId = 7) + .With(s => s.MalIds = new HashSet { 7 }) + .With(s => s.AniListIds = new HashSet { 7 }) + .With(s => s.Monitored = true) .TheNext(1) .With(s => s.TvdbId = 8) .With(s => s.ImdbId = "8") + .With(s => s.TmdbId = 8) + .With(s => s.MalIds = new HashSet { 8 }) + .With(s => s.AniListIds = new HashSet { 8 }) + .With(s => s.Monitored = true) .Build().ToList(); _list2Series = Builder.CreateListOfSize(3) .TheFirst(1) .With(s => s.TvdbId = 6) .With(s => s.ImdbId = "6") + .With(s => s.TmdbId = 6) + .With(s => s.MalId = 6) + .With(s => s.AniListId = 6) .TheNext(1) .With(s => s.TvdbId = 7) .With(s => s.ImdbId = "7") + .With(s => s.TmdbId = 7) + .With(s => s.MalId = 7) + .With(s => s.AniListId = 7) .TheNext(1) .With(s => s.TvdbId = 8) .With(s => s.ImdbId = "8") + .With(s => s.TmdbId = 8) + .With(s => s.MalId = 8) + .With(s => s.AniListId = 8) .Build().ToList(); _importListFetch = new ImportListFetchResult(_list1Series, false); @@ -110,6 +131,10 @@ namespace NzbDrone.Core.Test.ImportListTests Mocker.GetMock() .Setup(v => v.All()) .Returns(new List()); + + Mocker.GetMock() + .Setup(s => s.All()) + .Returns(new List()); } private void WithTvdbId() @@ -153,6 +178,19 @@ namespace NzbDrone.Core.Test.ImportListTests }); } + private List WithImportListItems(int count) + { + var importListItems = Builder.CreateListOfSize(count) + .Build() + .ToList(); + + Mocker.GetMock() + .Setup(s => s.All()) + .Returns(importListItems); + + return importListItems; + } + private void WithMonitorType(MonitorTypes monitor) { _importLists.ForEach(li => (li.Definition as ImportListDefinition).ShouldMonitor = monitor); @@ -285,16 +323,18 @@ namespace NzbDrone.Core.Test.ImportListTests { WithList(1, true); WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor); + var importListItems = WithImportListItems(_existingSeries.Count - 1); _importListFetch.Series.ForEach(m => m.ImportListId = 1); - Mocker.GetMock() - .Setup(v => v.Exists(6, It.IsAny())) - .Returns(true); + for (var i = 0; i < importListItems.Count; i++) + { + importListItems[i].TvdbId = _existingSeries[i].TvdbId; + } Subject.Execute(_commandAll); Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.Is>(s => s.Count > 0 && s.All(m => !m.Monitored)), true), Times.Once()); + .Verify(v => v.UpdateSeries(It.Is>(s => s.Count == 1 && s.All(m => !m.Monitored)), true), Times.Once()); } [Test] @@ -302,18 +342,75 @@ namespace NzbDrone.Core.Test.ImportListTests { WithList(1, true); WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor); + var importListItems = WithImportListItems(_existingSeries.Count - 1); _importListFetch.Series.ForEach(m => m.ImportListId = 1); - var x = _importLists; + for (var i = 0; i < importListItems.Count; i++) + { + importListItems[i].ImdbId = _existingSeries[i].ImdbId; + } - Mocker.GetMock() - .Setup(v => v.Exists(It.IsAny(), "6")) - .Returns(true); + Subject.Execute(_commandAll); + + Mocker.GetMock() + .Verify(v => v.UpdateSeries(It.Is>(s => s.Count == 1 && s.All(m => !m.Monitored)), true), Times.Once()); + } + + [Test] + public void should_not_clean_on_clean_library_if_tmdb_match() + { + WithList(1, true); + WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor); + var importListItems = WithImportListItems(_existingSeries.Count - 1); + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + + for (var i = 0; i < importListItems.Count; i++) + { + importListItems[i].TmdbId = _existingSeries[i].TmdbId; + } + + Subject.Execute(_commandAll); + + Mocker.GetMock() + .Verify(v => v.UpdateSeries(It.Is>(s => s.Count == 1 && s.All(m => !m.Monitored)), true), Times.Once()); + } + + [Test] + public void should_not_clean_on_clean_library_if_malid_match() + { + WithList(1, true); + WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor); + var importListItems = WithImportListItems(_existingSeries.Count - 1); + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + + for (var i = 0; i < importListItems.Count; i++) + { + importListItems[i].MalId = _existingSeries[i].MalIds.First(); + } + + Subject.Execute(_commandAll); + + Mocker.GetMock() + .Verify(v => v.UpdateSeries(It.Is>(s => s.Count == 1 && s.All(m => !m.Monitored)), true), Times.Once()); + } + + [Test] + public void should_not_clean_on_clean_library_if_anilistid_match() + { + WithList(1, true); + WithCleanLevel(ListSyncLevelType.KeepAndUnmonitor); + var importListItems = WithImportListItems(_existingSeries.Count - 1); + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + + for (var i = 0; i < importListItems.Count; i++) + { + importListItems[i].AniListId = _existingSeries[i].AniListIds.First(); + } Subject.Execute(_commandAll); Mocker.GetMock() - .Verify(v => v.UpdateSeries(It.Is>(s => s.Count > 0 && s.All(m => !m.Monitored)), true), Times.Once()); + .Verify(v => v.UpdateSeries(It.Is>(s => s.Count == 1 && s.All(m => !m.Monitored)), true), Times.Once()); } [Test] @@ -345,6 +442,7 @@ namespace NzbDrone.Core.Test.ImportListTests Mocker.GetMock() .Verify(v => v.UpdateSeries(It.IsAny>(), It.IsAny()), Times.Never()); + Mocker.GetMock() .Verify(v => v.DeleteSeries(It.IsAny>(), It.IsAny(), It.IsAny()), Times.Never()); } diff --git a/src/NzbDrone.Core/Datastore/Migration/217_add_mal_and_anilist_ids.cs b/src/NzbDrone.Core/Datastore/Migration/217_add_mal_and_anilist_ids.cs new file mode 100644 index 000000000..55a2c3b46 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/217_add_mal_and_anilist_ids.cs @@ -0,0 +1,15 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(217)] + public class add_mal_and_anilist_ids : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Series").AddColumn("MalIds").AsString().WithDefaultValue("[]"); + Alter.Table("Series").AddColumn("AniListIds").AsString().WithDefaultValue("[]"); + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemRepository.cs b/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemRepository.cs index abe546026..91a1c3a56 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemRepository.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemRepository.cs @@ -1,18 +1,16 @@ using System.Collections.Generic; -using System.Linq; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.ImportLists.ImportListItems { - public interface IImportListItemInfoRepository : IBasicRepository + public interface IImportListItemRepository : IBasicRepository { List GetAllForLists(List listIds); - bool Exists(int tvdbId, string imdbId); } - public class ImportListItemRepository : BasicRepository, IImportListItemInfoRepository + public class ImportListItemRepository : BasicRepository, IImportListItemRepository { public ImportListItemRepository(IMainDatabase database, IEventAggregator eventAggregator) : base(database, eventAggregator) @@ -23,21 +21,5 @@ namespace NzbDrone.Core.ImportLists.ImportListItems { return Query(x => listIds.Contains(x.ImportListId)); } - - public bool Exists(int tvdbId, string imdbId) - { - List items; - - if (string.IsNullOrWhiteSpace(imdbId)) - { - items = Query(x => x.TvdbId == tvdbId); - } - else - { - items = Query(x => x.TvdbId == tvdbId || x.ImdbId == imdbId); - } - - return items.Any(); - } } } diff --git a/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemService.cs b/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemService.cs index 793d6c548..f7faf607b 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemService.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemService.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser.Model; @@ -10,21 +9,18 @@ namespace NzbDrone.Core.ImportLists.ImportListItems { public interface IImportListItemService { + List All(); List GetAllForLists(List listIds); int SyncSeriesForList(List listSeries, int listId); - bool Exists(int tvdbId, string imdbId); } public class ImportListItemService : IImportListItemService, IHandleAsync> { - private readonly IImportListItemInfoRepository _importListSeriesRepository; - private readonly Logger _logger; + private readonly IImportListItemRepository _importListItemRepository; - public ImportListItemService(IImportListItemInfoRepository importListSeriesRepository, - Logger logger) + public ImportListItemService(IImportListItemRepository importListItemRepository) { - _importListSeriesRepository = importListSeriesRepository; - _logger = logger; + _importListItemRepository = importListItemRepository; } public int SyncSeriesForList(List listSeries, int listId) @@ -58,27 +54,27 @@ namespace NzbDrone.Core.ImportLists.ImportListItems existingItem.ReleaseDate = item.ReleaseDate; }); - _importListSeriesRepository.InsertMany(toAdd); - _importListSeriesRepository.UpdateMany(toUpdate); - _importListSeriesRepository.DeleteMany(existingListSeries); + _importListItemRepository.InsertMany(toAdd); + _importListItemRepository.UpdateMany(toUpdate); + _importListItemRepository.DeleteMany(existingListSeries); return existingListSeries.Count; } - public List GetAllForLists(List listIds) + public List All() { - return _importListSeriesRepository.GetAllForLists(listIds).ToList(); + return _importListItemRepository.All().ToList(); } - public void HandleAsync(ProviderDeletedEvent message) + public List GetAllForLists(List listIds) { - var seriesOnList = _importListSeriesRepository.GetAllForLists(new List { message.ProviderId }); - _importListSeriesRepository.DeleteMany(seriesOnList); + return _importListItemRepository.GetAllForLists(listIds).ToList(); } - public bool Exists(int tvdbId, string imdbId) + public void HandleAsync(ProviderDeletedEvent message) { - return _importListSeriesRepository.Exists(tvdbId, imdbId); + var seriesOnList = _importListItemRepository.GetAllForLists(new List { message.ProviderId }); + _importListItemRepository.DeleteMany(seriesOnList); } private ImportListItemInfo FindItem(List existingItems, ImportListItemInfo item) diff --git a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs index 291ecba27..489b4a7bc 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs @@ -299,12 +299,18 @@ namespace NzbDrone.Core.ImportLists var seriesToUpdate = new List(); var seriesInLibrary = _seriesService.GetAllSeries(); + var allListItems = _importListItemService.All(); foreach (var series in seriesInLibrary) { - var seriesExists = _importListItemService.Exists(series.TvdbId, series.ImdbId); - - if (!seriesExists) + var seriesExists = allListItems.Where(l => + l.TvdbId == series.TvdbId || + l.ImdbId == series.ImdbId || + l.TmdbId == series.TmdbId || + series.MalIds.Contains(l.MalId) || + series.AniListIds.Contains(l.AniListId)).ToList(); + + if (!seriesExists.Any()) { switch (_configService.ListSyncLevel) { diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs index 5cc7fce36..b7a9d7a42 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs @@ -24,7 +24,8 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource public int? TvRageId { get; set; } public int? TvMazeId { get; set; } public int? TmdbId { get; set; } - + public HashSet MalIds { get; set; } + public HashSet AniListIds { get; set; } public string Status { get; set; } public int? Runtime { get; set; } public TimeOfDayResource TimeOfDay { get; set; } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index f600057a7..c97c524dc 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -194,6 +194,8 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } series.ImdbId = show.ImdbId; + series.MalIds = show.MalIds; + series.AniListIds = show.AniListIds; series.Title = show.Title; series.CleanTitle = Parser.Parser.CleanSeriesTitle(show.Title); series.SortTitle = SeriesTitleNormalizer.Normalize(show.Title, show.TvdbId); diff --git a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs index b9dd08482..b5af90527 100644 --- a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs +++ b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs @@ -94,6 +94,8 @@ namespace NzbDrone.Core.Tv series.TvMazeId = seriesInfo.TvMazeId; series.TmdbId = seriesInfo.TmdbId; series.ImdbId = seriesInfo.ImdbId; + series.MalIds = seriesInfo.MalIds; + series.AniListIds = seriesInfo.AniListIds; series.AirTime = seriesInfo.AirTime; series.Overview = seriesInfo.Overview; series.OriginalLanguage = seriesInfo.OriginalLanguage; diff --git a/src/NzbDrone.Core/Tv/Series.cs b/src/NzbDrone.Core/Tv/Series.cs index 9819773a0..2464742b7 100644 --- a/src/NzbDrone.Core/Tv/Series.cs +++ b/src/NzbDrone.Core/Tv/Series.cs @@ -17,6 +17,8 @@ namespace NzbDrone.Core.Tv Seasons = new List(); Tags = new HashSet(); OriginalLanguage = Language.English; + MalIds = new HashSet(); + AniListIds = new HashSet(); } public int TvdbId { get; set; } @@ -24,6 +26,8 @@ namespace NzbDrone.Core.Tv public int TvMazeId { get; set; } public string ImdbId { get; set; } public int TmdbId { get; set; } + public HashSet MalIds { get; set; } + public HashSet AniListIds { get; set; } public string Title { get; set; } public string CleanTitle { get; set; } public string SortTitle { get; set; }