From 8cd5cd603ade5fffbcef323f9c8963ad918e0ed8 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 26 Dec 2024 12:32:57 -0800 Subject: [PATCH] Fixed: Improve synchronization logic for import list items Closes #7511 --- .../ImportListItemServiceFixture.cs | 250 ++++++++++++++++-- .../ImportListItems/ImportListItemService.cs | 70 ++++- 2 files changed, 294 insertions(+), 26 deletions(-) diff --git a/src/NzbDrone.Core.Test/ImportListTests/ImportListItemServiceFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/ImportListItemServiceFixture.cs index 661e2b357..ff6ac9afa 100644 --- a/src/NzbDrone.Core.Test/ImportListTests/ImportListItemServiceFixture.cs +++ b/src/NzbDrone.Core.Test/ImportListTests/ImportListItemServiceFixture.cs @@ -12,46 +12,256 @@ namespace NzbDrone.Core.Test.ImportListTests { public class ImportListItemServiceFixture : CoreTest { - [SetUp] - public void SetUp() + private void GivenExisting(List existing) { - var existing = Builder.CreateListOfSize(3) - .TheFirst(1) - .With(s => s.TvdbId = 6) - .With(s => s.ImdbId = "6") - .TheNext(1) - .With(s => s.TvdbId = 7) - .With(s => s.ImdbId = "7") - .TheNext(1) - .With(s => s.TvdbId = 8) - .With(s => s.ImdbId = "8") - .Build().ToList(); Mocker.GetMock() .Setup(v => v.GetAllForLists(It.IsAny>())) .Returns(existing); } [Test] - public void should_insert_new_update_existing_and_delete_missing() + public void should_insert_new_update_existing_and_delete_missing_based_on_tvdb_id() { - var newItems = Builder.CreateListOfSize(3) + var existing = Builder.CreateListOfSize(2) + .All() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) .TheFirst(1) - .With(s => s.TvdbId = 5) - .TheNext(1) .With(s => s.TvdbId = 6) .TheNext(1) .With(s => s.TvdbId = 7) .Build().ToList(); + var newItem = Builder.CreateNew() + .With(s => s.TvdbId = 5) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) + .Build(); + + var updatedItem = Builder.CreateNew() + .With(s => s.TvdbId = 6) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) + .Build(); + + GivenExisting(existing); + var newItems = new List { newItem, updatedItem }; + var numDeleted = Subject.SyncSeriesForList(newItems, 1); numDeleted.Should().Be(1); + Mocker.GetMock() - .Verify(v => v.InsertMany(It.Is>(s => s.Count == 1 && s[0].TvdbId == 5)), Times.Once()); + .Verify(v => v.InsertMany(It.Is>(s => s.Count == 1 && s[0].TvdbId == newItem.TvdbId)), Times.Once()); + Mocker.GetMock() - .Verify(v => v.UpdateMany(It.Is>(s => s.Count == 2 && s[0].TvdbId == 6 && s[1].TvdbId == 7)), Times.Once()); + .Verify(v => v.UpdateMany(It.Is>(s => s.Count == 1 && s[0].TvdbId == updatedItem.TvdbId)), Times.Once()); + Mocker.GetMock() - .Verify(v => v.DeleteMany(It.Is>(s => s.Count == 1 && s[0].TvdbId == 8)), Times.Once()); + .Verify(v => v.DeleteMany(It.Is>(s => s.Count == 1 && s[0].TvdbId != newItem.TvdbId && s[0].TvdbId != updatedItem.TvdbId)), Times.Once()); + } + + [Test] + public void should_insert_new_update_existing_and_delete_missing_based_on_imdb_id() + { + var existing = Builder.CreateListOfSize(2) + .All() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) + .TheFirst(1) + .With(s => s.ImdbId = "6") + .TheNext(1) + .With(s => s.ImdbId = "7") + .Build().ToList(); + + var newItem = Builder.CreateNew() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = "5") + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) + .Build(); + + var updatedItem = Builder.CreateNew() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = "6") + .With(s => s.TmdbId = 6) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) + .Build(); + + GivenExisting(existing); + var newItems = new List { newItem, updatedItem }; + + var numDeleted = Subject.SyncSeriesForList(newItems, 1); + + numDeleted.Should().Be(1); + + Mocker.GetMock() + .Verify(v => v.InsertMany(It.Is>(s => s.Count == 1 && s[0].ImdbId == newItem.ImdbId)), Times.Once()); + + Mocker.GetMock() + .Verify(v => v.UpdateMany(It.Is>(s => s.Count == 1 && s[0].ImdbId == updatedItem.ImdbId)), Times.Once()); + + Mocker.GetMock() + .Verify(v => v.DeleteMany(It.Is>(s => s.Count == 1 && s[0].ImdbId != newItem.ImdbId && s[0].ImdbId != updatedItem.ImdbId)), Times.Once()); + } + + [Test] + public void should_insert_new_update_existing_and_delete_missing_based_on_tmdb_id() + { + var existing = Builder.CreateListOfSize(2) + .All() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) + .TheFirst(1) + .With(s => s.TmdbId = 6) + .TheNext(1) + .With(s => s.TmdbId = 7) + .Build().ToList(); + + var newItem = Builder.CreateNew() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 5) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) + .Build(); + + var updatedItem = Builder.CreateNew() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 6) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) + .Build(); + + GivenExisting(existing); + var newItems = new List { newItem, updatedItem }; + + var numDeleted = Subject.SyncSeriesForList(newItems, 1); + + numDeleted.Should().Be(1); + + Mocker.GetMock() + .Verify(v => v.InsertMany(It.Is>(s => s.Count == 1 && s[0].TmdbId == newItem.TmdbId)), Times.Once()); + + Mocker.GetMock() + .Verify(v => v.UpdateMany(It.Is>(s => s.Count == 1 && s[0].TmdbId == updatedItem.TmdbId)), Times.Once()); + + Mocker.GetMock() + .Verify(v => v.DeleteMany(It.Is>(s => s.Count == 1 && s[0].TmdbId != newItem.TmdbId && s[0].TmdbId != updatedItem.TmdbId)), Times.Once()); + } + + [Test] + public void should_insert_new_update_existing_and_delete_missing_based_on_mal_id() + { + var existing = Builder.CreateListOfSize(2) + .All() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) + .TheFirst(1) + .With(s => s.MalId = 6) + .TheNext(1) + .With(s => s.MalId = 7) + .Build().ToList(); + + var newItem = Builder.CreateNew() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 5) + .With(s => s.AniListId = 0) + .Build(); + + var updatedItem = Builder.CreateNew() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 6) + .With(s => s.AniListId = 0) + .Build(); + + GivenExisting(existing); + var newItems = new List { newItem, updatedItem }; + + var numDeleted = Subject.SyncSeriesForList(newItems, 1); + + numDeleted.Should().Be(1); + + Mocker.GetMock() + .Verify(v => v.InsertMany(It.Is>(s => s.Count == 1 && s[0].MalId == newItem.MalId)), Times.Once()); + + Mocker.GetMock() + .Verify(v => v.UpdateMany(It.Is>(s => s.Count == 1 && s[0].MalId == updatedItem.MalId)), Times.Once()); + + Mocker.GetMock() + .Verify(v => v.DeleteMany(It.Is>(s => s.Count == 1 && s[0].MalId != newItem.MalId && s[0].MalId != updatedItem.MalId)), Times.Once()); + } + + [Test] + public void should_insert_new_update_existing_and_delete_missing_based_on_anilist_id() + { + var existing = Builder.CreateListOfSize(2) + .All() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 0) + .TheFirst(1) + .With(s => s.AniListId = 6) + .TheNext(1) + .With(s => s.AniListId = 7) + .Build().ToList(); + + var newItem = Builder.CreateNew() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 5) + .Build(); + + var updatedItem = Builder.CreateNew() + .With(s => s.TvdbId = 0) + .With(s => s.ImdbId = null) + .With(s => s.TmdbId = 0) + .With(s => s.MalId = 0) + .With(s => s.AniListId = 6) + .Build(); + + GivenExisting(existing); + var newItems = new List { newItem, updatedItem }; + + var numDeleted = Subject.SyncSeriesForList(newItems, 1); + + numDeleted.Should().Be(1); + + Mocker.GetMock() + .Verify(v => v.InsertMany(It.Is>(s => s.Count == 1 && s[0].AniListId == newItem.AniListId)), Times.Once()); + + Mocker.GetMock() + .Verify(v => v.UpdateMany(It.Is>(s => s.Count == 1 && s[0].AniListId == updatedItem.AniListId)), Times.Once()); + + 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/ImportLists/ImportListItems/ImportListItemService.cs b/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemService.cs index 852a30ee5..793d6c548 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemService.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListItems/ImportListItemService.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider.Events; @@ -30,14 +31,38 @@ public int SyncSeriesForList(List listSeries, int listId) { var existingListSeries = GetAllForLists(new List { listId }); - listSeries.ForEach(l => l.Id = existingListSeries.FirstOrDefault(e => e.TvdbId == l.TvdbId)?.Id ?? 0); + var toAdd = new List(); + var toUpdate = new List(); - _importListSeriesRepository.InsertMany(listSeries.Where(l => l.Id == 0).ToList()); - _importListSeriesRepository.UpdateMany(listSeries.Where(l => l.Id > 0).ToList()); - var toDelete = existingListSeries.Where(l => !listSeries.Any(x => x.TvdbId == l.TvdbId)).ToList(); - _importListSeriesRepository.DeleteMany(toDelete); + listSeries.ForEach(item => + { + var existingItem = FindItem(existingListSeries, item); - return toDelete.Count; + if (existingItem == null) + { + toAdd.Add(item); + return; + } + + // Remove so we'll only be left with items to remove at the end + existingListSeries.Remove(existingItem); + toUpdate.Add(existingItem); + + existingItem.Title = item.Title; + existingItem.Year = item.Year; + existingItem.TvdbId = item.TvdbId; + existingItem.ImdbId = item.ImdbId; + existingItem.TmdbId = item.TmdbId; + existingItem.MalId = item.MalId; + existingItem.AniListId = item.AniListId; + existingItem.ReleaseDate = item.ReleaseDate; + }); + + _importListSeriesRepository.InsertMany(toAdd); + _importListSeriesRepository.UpdateMany(toUpdate); + _importListSeriesRepository.DeleteMany(existingListSeries); + + return existingListSeries.Count; } public List GetAllForLists(List listIds) @@ -55,5 +80,38 @@ public bool Exists(int tvdbId, string imdbId) { return _importListSeriesRepository.Exists(tvdbId, imdbId); } + + private ImportListItemInfo FindItem(List existingItems, ImportListItemInfo item) + { + return existingItems.FirstOrDefault(e => + { + if (e.TvdbId > 0 && item.TvdbId > 0 && e.TvdbId == item.TvdbId) + { + return true; + } + + if (e.ImdbId.IsNotNullOrWhiteSpace() && item.ImdbId.IsNotNullOrWhiteSpace() && e.ImdbId == item.ImdbId) + { + return true; + } + + if (e.TmdbId > 0 && item.TmdbId > 0 && e.TmdbId == item.TmdbId) + { + return true; + } + + if (e.MalId > 0 && item.MalId > 0 && e.MalId == item.MalId) + { + return true; + } + + if (e.AniListId > 0 && item.AniListId > 0 && e.AniListId == item.AniListId) + { + return true; + } + + return false; + }); + } } }