From dfbf12b711205cac79f6e24fc9500deae2a655b6 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 31 Mar 2025 19:24:55 -0700 Subject: [PATCH] Fixed: Delete orphaned extra and subtitle files during housekeeping Closes #7785 --- .../CleanupOrphanedExtraFilesFixture.cs | 121 +++++++++++++++++ .../CleanupOrphanedSubtitleFilesFixture.cs | 126 ++++++++++++++++++ .../Housekeepers/CleanupOrphanedExtraFiles.cs | 54 ++++++++ .../CleanupOrphanedSubtitleFiles.cs | 54 ++++++++ 4 files changed, 355 insertions(+) create mode 100644 src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedExtraFilesFixture.cs create mode 100644 src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedSubtitleFilesFixture.cs create mode 100644 src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedExtraFiles.cs create mode 100644 src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedSubtitleFiles.cs diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedExtraFilesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedExtraFilesFixture.cs new file mode 100644 index 000000000..3ea4c2488 --- /dev/null +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedExtraFilesFixture.cs @@ -0,0 +1,121 @@ +using System.Collections.Generic; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Extras.Others; +using NzbDrone.Core.Housekeeping.Housekeepers; +using NzbDrone.Core.Languages; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.Housekeeping.Housekeepers +{ + [TestFixture] + public class CleanupOrphanedExtraFilesFixture : DbTest + { + [Test] + public void should_delete_extra_files_that_dont_have_a_coresponding_series() + { + var episodeFile = Builder.CreateNew() + .With(h => h.Quality = new QualityModel()) + .With(h => h.Languages = new List { Language.English }) + .BuildNew(); + + Db.Insert(episodeFile); + + var extraFile = Builder.CreateNew() + .With(m => m.EpisodeFileId = episodeFile.Id) + .BuildNew(); + + Db.Insert(extraFile); + Subject.Clean(); + AllStoredModels.Should().BeEmpty(); + } + + [Test] + public void should_not_delete_extra_files_that_have_a_coresponding_series() + { + var series = Builder.CreateNew() + .BuildNew(); + + var episodeFile = Builder.CreateNew() + .With(h => h.Quality = new QualityModel()) + .With(h => h.Languages = new List { Language.English }) + .BuildNew(); + + Db.Insert(series); + Db.Insert(episodeFile); + + var extraFile = Builder.CreateNew() + .With(m => m.SeriesId = series.Id) + .With(m => m.EpisodeFileId = episodeFile.Id) + .BuildNew(); + + Db.Insert(extraFile); + Subject.Clean(); + AllStoredModels.Should().HaveCount(1); + } + + [Test] + public void should_delete_extra_files_that_dont_have_a_coresponding_episode_file() + { + var series = Builder.CreateNew() + .BuildNew(); + + Db.Insert(series); + + var extraFile = Builder.CreateNew() + .With(m => m.SeriesId = series.Id) + .With(m => m.EpisodeFileId = 10) + .BuildNew(); + + Db.Insert(extraFile); + Subject.Clean(); + AllStoredModels.Should().BeEmpty(); + } + + [Test] + public void should_not_delete_extra_files_that_have_a_coresponding_episode_file() + { + var series = Builder.CreateNew() + .BuildNew(); + + var episodeFile = Builder.CreateNew() + .With(h => h.Quality = new QualityModel()) + .With(h => h.Languages = new List { Language.English }) + .BuildNew(); + + Db.Insert(series); + Db.Insert(episodeFile); + + var extraFile = Builder.CreateNew() + .With(m => m.SeriesId = series.Id) + .With(m => m.EpisodeFileId = episodeFile.Id) + .BuildNew(); + + Db.Insert(extraFile); + Subject.Clean(); + AllStoredModels.Should().HaveCount(1); + } + + [Test] + public void should_delete_extra_files_that_have_episodefileid_of_zero() + { + var series = Builder.CreateNew() + .BuildNew(); + + Db.Insert(series); + + var extraFile = Builder.CreateNew() + .With(m => m.SeriesId = series.Id) + .With(m => m.EpisodeFileId = 0) + .BuildNew(); + + Db.Insert(extraFile); + Subject.Clean(); + AllStoredModels.Should().HaveCount(0); + } + } +} diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedSubtitleFilesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedSubtitleFilesFixture.cs new file mode 100644 index 000000000..660b34c49 --- /dev/null +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedSubtitleFilesFixture.cs @@ -0,0 +1,126 @@ +using System.Collections.Generic; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Extras.Subtitles; +using NzbDrone.Core.Housekeeping.Housekeepers; +using NzbDrone.Core.Languages; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.Housekeeping.Housekeepers +{ + [TestFixture] + public class CleanupOrphanedSubtitleFilesFixture : DbTest + { + [Test] + public void should_delete_subtitle_files_that_dont_have_a_coresponding_series() + { + var episodeFile = Builder.CreateNew() + .With(h => h.Quality = new QualityModel()) + .With(h => h.Languages = new List { Language.English }) + .BuildNew(); + + Db.Insert(episodeFile); + + var subtitleFile = Builder.CreateNew() + .With(m => m.EpisodeFileId = episodeFile.Id) + .With(m => m.Language = Language.English) + .BuildNew(); + + Db.Insert(subtitleFile); + Subject.Clean(); + AllStoredModels.Should().BeEmpty(); + } + + [Test] + public void should_not_delete_subtitle_files_that_have_a_coresponding_series() + { + var series = Builder.CreateNew() + .BuildNew(); + + var episodeFile = Builder.CreateNew() + .With(h => h.Quality = new QualityModel()) + .With(h => h.Languages = new List { Language.English }) + .BuildNew(); + + Db.Insert(series); + Db.Insert(episodeFile); + + var subtitleFile = Builder.CreateNew() + .With(m => m.SeriesId = series.Id) + .With(m => m.EpisodeFileId = episodeFile.Id) + .With(m => m.Language = Language.English) + .BuildNew(); + + Db.Insert(subtitleFile); + Subject.Clean(); + AllStoredModels.Should().HaveCount(1); + } + + [Test] + public void should_delete_subtitle_files_that_dont_have_a_coresponding_episode_file() + { + var series = Builder.CreateNew() + .BuildNew(); + + Db.Insert(series); + + var subtitleFile = Builder.CreateNew() + .With(m => m.SeriesId = series.Id) + .With(m => m.EpisodeFileId = 10) + .With(m => m.Language = Language.English) + .BuildNew(); + + Db.Insert(subtitleFile); + Subject.Clean(); + AllStoredModels.Should().BeEmpty(); + } + + [Test] + public void should_not_delete_subtitle_files_that_have_a_coresponding_episode_file() + { + var series = Builder.CreateNew() + .BuildNew(); + + var episodeFile = Builder.CreateNew() + .With(h => h.Quality = new QualityModel()) + .With(h => h.Languages = new List { Language.English }) + .BuildNew(); + + Db.Insert(series); + Db.Insert(episodeFile); + + var subtitleFile = Builder.CreateNew() + .With(m => m.SeriesId = series.Id) + .With(m => m.EpisodeFileId = episodeFile.Id) + .With(m => m.Language = Language.English) + .BuildNew(); + + Db.Insert(subtitleFile); + Subject.Clean(); + AllStoredModels.Should().HaveCount(1); + } + + [Test] + public void should_delete_subtitle_files_that_have_episodefileid_of_zero() + { + var series = Builder.CreateNew() + .BuildNew(); + + Db.Insert(series); + + var subtitleFile = Builder.CreateNew() + .With(m => m.SeriesId = series.Id) + .With(m => m.EpisodeFileId = 0) + .With(m => m.Language = Language.English) + .BuildNew(); + + Db.Insert(subtitleFile); + Subject.Clean(); + AllStoredModels.Should().HaveCount(0); + } + } +} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedExtraFiles.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedExtraFiles.cs new file mode 100644 index 000000000..e6e96f5bd --- /dev/null +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedExtraFiles.cs @@ -0,0 +1,54 @@ +using Dapper; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Housekeeping.Housekeepers +{ + public class CleanupOrphanedExtraFiles : IHousekeepingTask + { + private readonly IMainDatabase _database; + + public CleanupOrphanedExtraFiles(IMainDatabase database) + { + _database = database; + } + + public void Clean() + { + DeleteOrphanedBySeries(); + DeleteOrphanedByEpisodeFile(); + DeleteWhereEpisodeFileIsZero(); + } + + private void DeleteOrphanedBySeries() + { + using var mapper = _database.OpenConnection(); + mapper.Execute(@"DELETE FROM ""ExtraFiles"" + WHERE ""Id"" IN ( + SELECT ""ExtraFiles"".""Id"" FROM ""ExtraFiles"" + LEFT OUTER JOIN ""Series"" + ON ""ExtraFiles"".""SeriesId"" = ""Series"".""Id"" + WHERE ""Series"".""Id"" IS NULL)"); + } + + private void DeleteOrphanedByEpisodeFile() + { + using var mapper = _database.OpenConnection(); + mapper.Execute(@"DELETE FROM ""ExtraFiles"" + WHERE ""Id"" IN ( + SELECT ""ExtraFiles"".""Id"" FROM ""ExtraFiles"" + LEFT OUTER JOIN ""EpisodeFiles"" + ON ""ExtraFiles"".""EpisodeFileId"" = ""EpisodeFiles"".""Id"" + WHERE ""ExtraFiles"".""EpisodeFileId"" > 0 + AND ""EpisodeFiles"".""Id"" IS NULL)"); + } + + private void DeleteWhereEpisodeFileIsZero() + { + using var mapper = _database.OpenConnection(); + mapper.Execute(@"DELETE FROM ""ExtraFiles"" + WHERE ""Id"" IN ( + SELECT ""Id"" FROM ""ExtraFiles"" + WHERE ""EpisodeFileId"" = 0)"); + } + } +} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedSubtitleFiles.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedSubtitleFiles.cs new file mode 100644 index 000000000..929715907 --- /dev/null +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedSubtitleFiles.cs @@ -0,0 +1,54 @@ +using Dapper; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Housekeeping.Housekeepers +{ + public class CleanupOrphanedSubtitleFiles : IHousekeepingTask + { + private readonly IMainDatabase _database; + + public CleanupOrphanedSubtitleFiles(IMainDatabase database) + { + _database = database; + } + + public void Clean() + { + DeleteOrphanedBySeries(); + DeleteOrphanedByEpisodeFile(); + DeleteWhereEpisodeFileIsZero(); + } + + private void DeleteOrphanedBySeries() + { + using var mapper = _database.OpenConnection(); + mapper.Execute(@"DELETE FROM ""SubtitleFiles"" + WHERE ""Id"" IN ( + SELECT ""SubtitleFiles"".""Id"" FROM ""SubtitleFiles"" + LEFT OUTER JOIN ""Series"" + ON ""SubtitleFiles"".""SeriesId"" = ""Series"".""Id"" + WHERE ""Series"".""Id"" IS NULL)"); + } + + private void DeleteOrphanedByEpisodeFile() + { + using var mapper = _database.OpenConnection(); + mapper.Execute(@"DELETE FROM ""SubtitleFiles"" + WHERE ""Id"" IN ( + SELECT ""SubtitleFiles"".""Id"" FROM ""SubtitleFiles"" + LEFT OUTER JOIN ""EpisodeFiles"" + ON ""SubtitleFiles"".""EpisodeFileId"" = ""EpisodeFiles"".""Id"" + WHERE ""SubtitleFiles"".""EpisodeFileId"" > 0 + AND ""EpisodeFiles"".""Id"" IS NULL)"); + } + + private void DeleteWhereEpisodeFileIsZero() + { + using var mapper = _database.OpenConnection(); + mapper.Execute(@"DELETE FROM ""SubtitleFiles"" + WHERE ""Id"" IN ( + SELECT ""Id"" FROM ""SubtitleFiles"" + WHERE ""EpisodeFileId"" = 0)"); + } + } +}