From bea07cb9928684114ac0bda02d1bb40195c895ca Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Mon, 15 Feb 2021 15:44:57 -0600 Subject: [PATCH] implement most scanning actions --- .../Commands/CreateMediaItemHandler.cs | 9 +- .../RefreshMediaItemMetadataHandler.cs | 5 +- ...sts.cs => LocalMediaSourcePlannerTests.cs} | 288 +++++++++--------- .../Metadata/ILocalMediaSourcePlanner.cs | 14 + .../Metadata/ILocalMetadataProvider.cs | 3 +- ErsatzTV.Core/Metadata/ActionPlan.cs | 4 + ErsatzTV.Core/Metadata/ItemScanningPlan.cs | 4 - .../Metadata/LocalMediaItemScanningPlan.cs | 8 - ErsatzTV.Core/Metadata/LocalMediaScanner.cs | 283 ++++++++++------- .../Metadata/LocalMediaSourcePlan.cs | 11 + ...aScanner.cs => LocalMediaSourcePlanner.cs} | 38 +-- .../Metadata/LocalMetadataProvider.cs | 17 +- ErsatzTV.Core/Metadata/LocalPosterProvider.cs | 2 +- ErsatzTV.Core/Metadata/ScanningAction.cs | 13 +- ErsatzTV/Services/WorkerService.cs | 2 +- 15 files changed, 400 insertions(+), 301 deletions(-) rename ErsatzTV.Core.Tests/Metadata/{TestMediaScannerTests.cs => LocalMediaSourcePlannerTests.cs} (70%) create mode 100644 ErsatzTV.Core/Interfaces/Metadata/ILocalMediaSourcePlanner.cs create mode 100644 ErsatzTV.Core/Metadata/ActionPlan.cs delete mode 100644 ErsatzTV.Core/Metadata/ItemScanningPlan.cs delete mode 100644 ErsatzTV.Core/Metadata/LocalMediaItemScanningPlan.cs create mode 100644 ErsatzTV.Core/Metadata/LocalMediaSourcePlan.cs rename ErsatzTV.Core/Metadata/{TestMediaScanner.cs => LocalMediaSourcePlanner.cs} (78%) diff --git a/ErsatzTV.Application/MediaItems/Commands/CreateMediaItemHandler.cs b/ErsatzTV.Application/MediaItems/Commands/CreateMediaItemHandler.cs index 53509737f..349ae3745 100644 --- a/ErsatzTV.Application/MediaItems/Commands/CreateMediaItemHandler.cs +++ b/ErsatzTV.Application/MediaItems/Commands/CreateMediaItemHandler.cs @@ -50,11 +50,12 @@ namespace ErsatzTV.Application.MediaItems.Commands private async Task PersistMediaItem(RequestParameters parameters) { await _mediaItemRepository.Add(parameters.MediaItem); - + await _localStatisticsProvider.RefreshStatistics(parameters.FFprobePath, parameters.MediaItem); - await _localMetadataProvider.RefreshMetadata(parameters.MediaItem); - await _localPosterProvider.RefreshPoster(parameters.MediaItem); - await _smartCollectionBuilder.RefreshSmartCollections(parameters.MediaItem); + // TODO: reimplement this + // await _localMetadataProvider.RefreshMetadata(parameters.MediaItem); + // await _localPosterProvider.RefreshPoster(parameters.MediaItem); + // await _smartCollectionBuilder.RefreshSmartCollections(parameters.MediaItem); return ProjectToViewModel(parameters.MediaItem); } diff --git a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemMetadataHandler.cs b/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemMetadataHandler.cs index 9a4a9a0fd..7bc3b695f 100644 --- a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemMetadataHandler.cs +++ b/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemMetadataHandler.cs @@ -46,7 +46,8 @@ namespace ErsatzTV.Application.MediaItems.Commands .Filter(item => File.Exists(item.Path)) .ToValidation($"[Path] '{mediaItem.Path}' does not exist on the file system"); - private Task RefreshMetadata(MediaItem mediaItem) => - _localMetadataProvider.RefreshMetadata(mediaItem).ToUnit(); + private Task RefreshMetadata(MediaItem mediaItem) => Task.CompletedTask.ToUnit(); + // TODO: reimplement this + // _localMetadataProvider.RefreshMetadata(mediaItem).ToUnit(); } } diff --git a/ErsatzTV.Core.Tests/Metadata/TestMediaScannerTests.cs b/ErsatzTV.Core.Tests/Metadata/LocalMediaSourcePlannerTests.cs similarity index 70% rename from ErsatzTV.Core.Tests/Metadata/TestMediaScannerTests.cs rename to ErsatzTV.Core.Tests/Metadata/LocalMediaSourcePlannerTests.cs index d817af976..21e97d31e 100644 --- a/ErsatzTV.Core.Tests/Metadata/TestMediaScannerTests.cs +++ b/ErsatzTV.Core.Tests/Metadata/LocalMediaSourcePlannerTests.cs @@ -11,7 +11,7 @@ using NUnit.Framework; namespace ErsatzTV.Core.Tests.Metadata { [TestFixture] - public class TestMediaScannerTests + public class LocalMediaSourcePlannerTests { private static readonly List VideoFileExtensions = new() { @@ -25,13 +25,13 @@ namespace ErsatzTV.Core.Tests.Metadata private static IEnumerable NewEntriesFor(params string[] fileNames) => fileNames.Map(f => new FakeFileSystemEntry(f, DateTime.MaxValue)); - private static TestMediaScanner ScannerForOldFiles(params string[] fileNames) + private static LocalMediaSourcePlanner ScannerForOldFiles(params string[] fileNames) => new(new FakeLocalFileSystem(OldEntriesFor(fileNames))); - private static TestMediaScanner ScannerForNewFiles(params string[] fileNames) + private static LocalMediaSourcePlanner ScannerForNewFiles(params string[] fileNames) => new(new FakeLocalFileSystem(NewEntriesFor(fileNames))); - private static TestMediaScanner ScannerFor(IEnumerable entries) + private static LocalMediaSourcePlanner ScannerFor(IEnumerable entries) => new(new FakeLocalFileSystem(entries)); [TestFixture] @@ -39,30 +39,31 @@ namespace ErsatzTV.Core.Tests.Metadata { [Test] public void WithoutNfo_WithoutPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension) { var movieFileName = $"/movies/test (2021)/test (2021).{extension}"; string[] fileNames = { movieFileName }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.Movie, Seq.Empty, fileNames.ToSeq()); result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); + (Either source, List itemScanningPlans) = result.Head(); source.IsLeft.Should().BeTrue(); source.LeftToSeq().Should().BeEquivalentTo(movieFileName); itemScanningPlans.Should().BeEquivalentTo( - new ItemScanningPlan(movieFileName, ScanningAction.Statistics), - new ItemScanningPlan(movieFileName, ScanningAction.FallbackMetadata), - new ItemScanningPlan(movieFileName, ScanningAction.Collections)); + new ActionPlan(movieFileName, ScanningAction.Add), + new ActionPlan(movieFileName, ScanningAction.Statistics), + new ActionPlan(movieFileName, ScanningAction.FallbackMetadata), + new ActionPlan(movieFileName, ScanningAction.Collections)); } [Test] public void WithNfo_WithoutPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension, [Values("test (2021).nfo", "movie.nfo")] string nfoFile) @@ -71,24 +72,25 @@ namespace ErsatzTV.Core.Tests.Metadata var nfoFileName = $"/movies/test (2021)/{nfoFile}"; string[] fileNames = { movieFileName, nfoFileName }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.Movie, Seq.Empty, fileNames.ToSeq()); result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); + (Either source, List itemScanningPlans) = result.Head(); source.IsLeft.Should().BeTrue(); source.LeftToSeq().Should().BeEquivalentTo(movieFileName); itemScanningPlans.Should().BeEquivalentTo( - new ItemScanningPlan(movieFileName, ScanningAction.Statistics), - new ItemScanningPlan(nfoFileName, ScanningAction.SidecarMetadata), - new ItemScanningPlan(nfoFileName, ScanningAction.Collections)); + new ActionPlan(movieFileName, ScanningAction.Add), + new ActionPlan(movieFileName, ScanningAction.Statistics), + new ActionPlan(nfoFileName, ScanningAction.SidecarMetadata), + new ActionPlan(nfoFileName, ScanningAction.Collections)); } [Test] public void WithoutNfo_WithPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension, [Values("", "test (2021)-")] string basePosterName, @@ -100,25 +102,26 @@ namespace ErsatzTV.Core.Tests.Metadata string[] fileNames = { movieFileName, posterFileName }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.Movie, Seq.Empty, fileNames.ToSeq()); result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); + (Either source, List itemScanningPlans) = result.Head(); source.IsLeft.Should().BeTrue(); source.LeftToSeq().Should().BeEquivalentTo(movieFileName); itemScanningPlans.Should().BeEquivalentTo( - new ItemScanningPlan(movieFileName, ScanningAction.Statistics), - new ItemScanningPlan(movieFileName, ScanningAction.FallbackMetadata), - new ItemScanningPlan(posterFileName, ScanningAction.Poster), - new ItemScanningPlan(movieFileName, ScanningAction.Collections)); + new ActionPlan(movieFileName, ScanningAction.Add), + new ActionPlan(movieFileName, ScanningAction.Statistics), + new ActionPlan(movieFileName, ScanningAction.FallbackMetadata), + new ActionPlan(posterFileName, ScanningAction.Poster), + new ActionPlan(movieFileName, ScanningAction.Collections)); } [Test] public void WithNfo_WithPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension, [Values("test (2021).nfo", "movie.nfo")] string nfoFile, @@ -133,20 +136,21 @@ namespace ErsatzTV.Core.Tests.Metadata string[] fileNames = { movieFileName, nfoFileName, posterFileName }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.Movie, Seq.Empty, fileNames.ToSeq()); result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); + (Either source, List itemScanningPlans) = result.Head(); source.IsLeft.Should().BeTrue(); source.LeftToSeq().Should().BeEquivalentTo(movieFileName); itemScanningPlans.Should().BeEquivalentTo( - new ItemScanningPlan(movieFileName, ScanningAction.Statistics), - new ItemScanningPlan(nfoFileName, ScanningAction.SidecarMetadata), - new ItemScanningPlan(posterFileName, ScanningAction.Poster), - new ItemScanningPlan(nfoFileName, ScanningAction.Collections)); + new ActionPlan(movieFileName, ScanningAction.Add), + new ActionPlan(movieFileName, ScanningAction.Statistics), + new ActionPlan(nfoFileName, ScanningAction.SidecarMetadata), + new ActionPlan(posterFileName, ScanningAction.Poster), + new ActionPlan(nfoFileName, ScanningAction.Collections)); } } @@ -155,7 +159,7 @@ namespace ErsatzTV.Core.Tests.Metadata { [Test] public void Old_File_Should_Do_Nothing( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension) { var movieMediaItem = new MediaItem @@ -166,7 +170,7 @@ namespace ErsatzTV.Core.Tests.Metadata string[] fileNames = { movieMediaItem.Path }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.Movie, Seq.create(movieMediaItem), fileNames.ToSeq()); @@ -176,7 +180,7 @@ namespace ErsatzTV.Core.Tests.Metadata [Test] public void Updated_File_Should_Refresh_Statistics( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension) { var movieMediaItem = new MediaItem @@ -187,22 +191,22 @@ namespace ErsatzTV.Core.Tests.Metadata string[] fileNames = { movieMediaItem.Path }; - Seq result = ScannerForNewFiles(fileNames).DetermineActions( + Seq result = ScannerForNewFiles(fileNames).DetermineActions( MediaType.Movie, Seq.create(movieMediaItem), fileNames.ToSeq()); result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); + (Either source, List itemScanningPlans) = result.Head(); source.IsRight.Should().BeTrue(); source.RightToSeq().Should().BeEquivalentTo(movieMediaItem); itemScanningPlans.Should().BeEquivalentTo( - new ItemScanningPlan(movieMediaItem.Path, ScanningAction.Statistics)); + new ActionPlan(movieMediaItem.Path, ScanningAction.Statistics)); } [Test] public void Fallback_WithNewNfo_WithoutPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension, [Values("test (2021).nfo", "movie.nfo")] string nfoFile) @@ -216,23 +220,23 @@ namespace ErsatzTV.Core.Tests.Metadata var nfoFileName = $"/movies/test (2021)/{nfoFile}"; string[] fileNames = { movieMediaItem.Path, nfoFileName }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.Movie, Seq.create(movieMediaItem), fileNames.ToSeq()); result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); + (Either source, List itemScanningPlans) = result.Head(); source.IsRight.Should().BeTrue(); source.RightToSeq().Should().BeEquivalentTo(movieMediaItem); itemScanningPlans.Should().BeEquivalentTo( - new ItemScanningPlan(nfoFileName, ScanningAction.SidecarMetadata), - new ItemScanningPlan(nfoFileName, ScanningAction.Collections)); + new ActionPlan(nfoFileName, ScanningAction.SidecarMetadata), + new ActionPlan(nfoFileName, ScanningAction.Collections)); } [Test] public void Sidecar_WithOldNfo_WithoutPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension, [Values("test (2021).nfo", "movie.nfo")] string nfoFile) @@ -246,7 +250,7 @@ namespace ErsatzTV.Core.Tests.Metadata var nfoFileName = $"/movies/test (2021)/{nfoFile}"; string[] fileNames = { movieMediaItem.Path, nfoFileName }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.Movie, Seq.create(movieMediaItem), fileNames.ToSeq()); @@ -256,7 +260,7 @@ namespace ErsatzTV.Core.Tests.Metadata [Test] public void Sidecar_WithUpdatedNfo_WithoutPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension, [Values("test (2021).nfo", "movie.nfo")] string nfoFile) @@ -270,7 +274,7 @@ namespace ErsatzTV.Core.Tests.Metadata var nfoFileName = $"/movies/test (2021)/{nfoFile}"; string[] fileNames = { movieMediaItem.Path, nfoFileName }; - Seq result = + Seq result = ScannerFor(OldEntriesFor(movieMediaItem.Path).Concat(NewEntriesFor(nfoFileName))) .DetermineActions( MediaType.Movie, @@ -278,17 +282,17 @@ namespace ErsatzTV.Core.Tests.Metadata fileNames.ToSeq()); result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); + (Either source, List itemScanningPlans) = result.Head(); source.IsRight.Should().BeTrue(); source.RightToSeq().Should().BeEquivalentTo(movieMediaItem); itemScanningPlans.Should().BeEquivalentTo( - new ItemScanningPlan(nfoFileName, ScanningAction.SidecarMetadata), - new ItemScanningPlan(nfoFileName, ScanningAction.Collections)); + new ActionPlan(nfoFileName, ScanningAction.SidecarMetadata), + new ActionPlan(nfoFileName, ScanningAction.Collections)); } [Test] public void WithoutNfo_WithNewPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension, [Values("", "test (2021)-")] string basePosterName, @@ -304,22 +308,22 @@ namespace ErsatzTV.Core.Tests.Metadata var posterFileName = $"/movies/test (2021)/{basePosterName}poster.{posterExtension}"; string[] fileNames = { movieMediaItem.Path, posterFileName }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.Movie, Seq.create(movieMediaItem), fileNames.ToSeq()); result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); + (Either source, List itemScanningPlans) = result.Head(); source.IsRight.Should().BeTrue(); source.RightToSeq().Should().BeEquivalentTo(movieMediaItem); itemScanningPlans.Should() - .BeEquivalentTo(new ItemScanningPlan(posterFileName, ScanningAction.Poster)); + .BeEquivalentTo(new ActionPlan(posterFileName, ScanningAction.Poster)); } [Test] public void WithoutNfo_WithOldPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension, [Values("", "test (2021)-")] string basePosterName, @@ -337,7 +341,7 @@ namespace ErsatzTV.Core.Tests.Metadata var posterFileName = $"/movies/test (2021)/{basePosterName}poster.{posterExtension}"; string[] fileNames = { movieMediaItem.Path, posterFileName }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.Movie, Seq.create(movieMediaItem), fileNames.ToSeq()); @@ -347,7 +351,7 @@ namespace ErsatzTV.Core.Tests.Metadata [Test] public void WithoutNfo_WithUpdatedPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension, [Values("", "test (2021)-")] string basePosterName, @@ -365,7 +369,7 @@ namespace ErsatzTV.Core.Tests.Metadata var posterFileName = $"/movies/test (2021)/{basePosterName}poster.{posterExtension}"; string[] fileNames = { movieMediaItem.Path, posterFileName }; - Seq result = + Seq result = ScannerFor(OldEntriesFor(movieMediaItem.Path).Concat(NewEntriesFor(posterFileName))) .DetermineActions( MediaType.Movie, @@ -373,16 +377,16 @@ namespace ErsatzTV.Core.Tests.Metadata fileNames.ToSeq()); result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); + (Either source, List itemScanningPlans) = result.Head(); source.IsRight.Should().BeTrue(); source.RightToSeq().Should().BeEquivalentTo(movieMediaItem); itemScanningPlans.Should() - .BeEquivalentTo(new ItemScanningPlan(posterFileName, ScanningAction.Poster)); + .BeEquivalentTo(new ActionPlan(posterFileName, ScanningAction.Poster)); } [Test] public void WithNewNfo_WithNewPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension, [Values("test (2021).nfo", "movie.nfo")] string nfoFile, @@ -401,19 +405,19 @@ namespace ErsatzTV.Core.Tests.Metadata var posterFileName = $"/movies/test (2021)/{basePosterName}poster.{posterExtension}"; string[] fileNames = { movieMediaItem.Path, nfoFileName, posterFileName }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.Movie, Seq.create(movieMediaItem), fileNames.ToSeq()); result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); + (Either source, List itemScanningPlans) = result.Head(); source.IsRight.Should().BeTrue(); source.RightToSeq().Should().BeEquivalentTo(movieMediaItem); itemScanningPlans.Should().BeEquivalentTo( - new ItemScanningPlan(nfoFileName, ScanningAction.SidecarMetadata), - new ItemScanningPlan(posterFileName, ScanningAction.Poster), - new ItemScanningPlan(nfoFileName, ScanningAction.Collections)); + new ActionPlan(nfoFileName, ScanningAction.SidecarMetadata), + new ActionPlan(posterFileName, ScanningAction.Poster), + new ActionPlan(nfoFileName, ScanningAction.Collections)); } } @@ -422,54 +426,56 @@ namespace ErsatzTV.Core.Tests.Metadata { [Test] public void WithoutNfo_WithoutPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension) { var episodeFileName = $"/tv/test (2021)/season 01/test (2021) - s01e03.{extension}"; string[] fileNames = { episodeFileName }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.TvShow, Seq.Empty, fileNames.ToSeq()); result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); + (Either source, List itemScanningPlans) = result.Head(); source.IsLeft.Should().BeTrue(); source.LeftToSeq().Should().BeEquivalentTo(episodeFileName); itemScanningPlans.Should().BeEquivalentTo( - new ItemScanningPlan(episodeFileName, ScanningAction.Statistics), - new ItemScanningPlan(episodeFileName, ScanningAction.FallbackMetadata), - new ItemScanningPlan(episodeFileName, ScanningAction.Collections)); + new ActionPlan(episodeFileName, ScanningAction.Add), + new ActionPlan(episodeFileName, ScanningAction.Statistics), + new ActionPlan(episodeFileName, ScanningAction.FallbackMetadata), + new ActionPlan(episodeFileName, ScanningAction.Collections)); } [Test] public void WithNfo_WithoutPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension) { var episodeFileName = $"/tv/test (2021)/season 01/test (2021) - s01e03.{extension}"; var nfoFileName = "/tv/test (2021)/season 01/test (2021) - s01e03.nfo"; string[] fileNames = { episodeFileName, nfoFileName }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.TvShow, Seq.Empty, fileNames.ToSeq()); result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); + (Either source, List itemScanningPlans) = result.Head(); source.IsLeft.Should().BeTrue(); source.LeftToSeq().Should().BeEquivalentTo(episodeFileName); itemScanningPlans.Should().BeEquivalentTo( - new ItemScanningPlan(episodeFileName, ScanningAction.Statistics), - new ItemScanningPlan(nfoFileName, ScanningAction.SidecarMetadata), - new ItemScanningPlan(nfoFileName, ScanningAction.Collections)); + new ActionPlan(episodeFileName, ScanningAction.Add), + new ActionPlan(episodeFileName, ScanningAction.Statistics), + new ActionPlan(nfoFileName, ScanningAction.SidecarMetadata), + new ActionPlan(nfoFileName, ScanningAction.Collections)); } [Test] public void WithoutNfo_WithPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension, [Values("jpg", "jpeg", "png", "gif", "tbn")] string posterExtension) @@ -479,25 +485,26 @@ namespace ErsatzTV.Core.Tests.Metadata string[] fileNames = { episodeFileName, posterFileName }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.TvShow, Seq.Empty, fileNames.ToSeq()); result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); + (Either source, List itemScanningPlans) = result.Head(); source.IsLeft.Should().BeTrue(); source.LeftToSeq().Should().BeEquivalentTo(episodeFileName); itemScanningPlans.Should().BeEquivalentTo( - new ItemScanningPlan(episodeFileName, ScanningAction.Statistics), - new ItemScanningPlan(episodeFileName, ScanningAction.FallbackMetadata), - new ItemScanningPlan(posterFileName, ScanningAction.Poster), - new ItemScanningPlan(episodeFileName, ScanningAction.Collections)); + new ActionPlan(episodeFileName, ScanningAction.Add), + new ActionPlan(episodeFileName, ScanningAction.Statistics), + new ActionPlan(episodeFileName, ScanningAction.FallbackMetadata), + new ActionPlan(posterFileName, ScanningAction.Poster), + new ActionPlan(episodeFileName, ScanningAction.Collections)); } [Test] public void WithNfo_WithPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension, [Values("jpg", "jpeg", "png", "gif", "tbn")] string posterExtension) @@ -508,20 +515,21 @@ namespace ErsatzTV.Core.Tests.Metadata string[] fileNames = { episodeFileName, nfoFileName, posterFileName }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.TvShow, Seq.Empty, fileNames.ToSeq()); result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); + (Either source, List itemScanningPlans) = result.Head(); source.IsLeft.Should().BeTrue(); source.LeftToSeq().Should().BeEquivalentTo(episodeFileName); itemScanningPlans.Should().BeEquivalentTo( - new ItemScanningPlan(episodeFileName, ScanningAction.Statistics), - new ItemScanningPlan(nfoFileName, ScanningAction.SidecarMetadata), - new ItemScanningPlan(posterFileName, ScanningAction.Poster), - new ItemScanningPlan(nfoFileName, ScanningAction.Collections)); + new ActionPlan(episodeFileName, ScanningAction.Add), + new ActionPlan(episodeFileName, ScanningAction.Statistics), + new ActionPlan(nfoFileName, ScanningAction.SidecarMetadata), + new ActionPlan(posterFileName, ScanningAction.Poster), + new ActionPlan(nfoFileName, ScanningAction.Collections)); } } @@ -530,7 +538,7 @@ namespace ErsatzTV.Core.Tests.Metadata { [Test] public void Old_File_Should_Do_Nothing( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension) { var episodeMediaItem = new MediaItem @@ -541,7 +549,7 @@ namespace ErsatzTV.Core.Tests.Metadata string[] fileNames = { episodeMediaItem.Path }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.TvShow, Seq.create(episodeMediaItem), fileNames.ToSeq()); @@ -551,7 +559,7 @@ namespace ErsatzTV.Core.Tests.Metadata [Test] public void Updated_File_Should_Refresh_Statistics( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension) { var episodeMediaItem = new MediaItem @@ -562,22 +570,22 @@ namespace ErsatzTV.Core.Tests.Metadata string[] fileNames = { episodeMediaItem.Path }; - Seq result = ScannerForNewFiles(fileNames).DetermineActions( + Seq result = ScannerForNewFiles(fileNames).DetermineActions( MediaType.TvShow, Seq.create(episodeMediaItem), fileNames.ToSeq()); result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); + (Either source, List itemScanningPlans) = result.Head(); source.IsRight.Should().BeTrue(); source.RightToSeq().Should().BeEquivalentTo(episodeMediaItem); itemScanningPlans.Should().BeEquivalentTo( - new ItemScanningPlan(episodeMediaItem.Path, ScanningAction.Statistics)); + new ActionPlan(episodeMediaItem.Path, ScanningAction.Statistics)); } [Test] public void Fallback_WithNewNfo_WithoutPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension) { var episodeMediaItem = new MediaItem @@ -589,23 +597,23 @@ namespace ErsatzTV.Core.Tests.Metadata var nfoFileName = "/tv/test (2021)/season 01/test (2021) - s01e03.nfo"; string[] fileNames = { episodeMediaItem.Path, nfoFileName }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.TvShow, Seq.create(episodeMediaItem), fileNames.ToSeq()); result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); + (Either source, List itemScanningPlans) = result.Head(); source.IsRight.Should().BeTrue(); source.RightToSeq().Should().BeEquivalentTo(episodeMediaItem); itemScanningPlans.Should().BeEquivalentTo( - new ItemScanningPlan(nfoFileName, ScanningAction.SidecarMetadata), - new ItemScanningPlan(nfoFileName, ScanningAction.Collections)); + new ActionPlan(nfoFileName, ScanningAction.SidecarMetadata), + new ActionPlan(nfoFileName, ScanningAction.Collections)); } [Test] public void Sidecar_WithOldNfo_WithoutPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension) { var episodeMediaItem = new MediaItem @@ -617,7 +625,7 @@ namespace ErsatzTV.Core.Tests.Metadata var nfoFileName = "/tv/test (2021)/season 01/test (2021) - s01e03.nfo"; string[] fileNames = { episodeMediaItem.Path, nfoFileName }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.TvShow, Seq.create(episodeMediaItem), fileNames.ToSeq()); @@ -627,7 +635,7 @@ namespace ErsatzTV.Core.Tests.Metadata [Test] public void Sidecar_WithUpdatedNfo_WithoutPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension) { var episodeMediaItem = new MediaItem @@ -639,7 +647,7 @@ namespace ErsatzTV.Core.Tests.Metadata var nfoFileName = "/tv/test (2021)/season 01/test (2021) - s01e03.nfo"; string[] fileNames = { episodeMediaItem.Path, nfoFileName }; - Seq result = + Seq result = ScannerFor(OldEntriesFor(episodeMediaItem.Path).Concat(NewEntriesFor(nfoFileName))) .DetermineActions( MediaType.TvShow, @@ -647,17 +655,17 @@ namespace ErsatzTV.Core.Tests.Metadata fileNames.ToSeq()); result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); + (Either source, List itemScanningPlans) = result.Head(); source.IsRight.Should().BeTrue(); source.RightToSeq().Should().BeEquivalentTo(episodeMediaItem); itemScanningPlans.Should().BeEquivalentTo( - new ItemScanningPlan(nfoFileName, ScanningAction.SidecarMetadata), - new ItemScanningPlan(nfoFileName, ScanningAction.Collections)); + new ActionPlan(nfoFileName, ScanningAction.SidecarMetadata), + new ActionPlan(nfoFileName, ScanningAction.Collections)); } [Test] public void WithoutNfo_WithNewPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension, [Values("jpg", "jpeg", "png", "gif", "tbn")] string posterExtension) @@ -671,22 +679,22 @@ namespace ErsatzTV.Core.Tests.Metadata var posterFileName = $"/tv/test (2021)/poster.{posterExtension}"; string[] fileNames = { episodeMediaItem.Path, posterFileName }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.TvShow, Seq.create(episodeMediaItem), fileNames.ToSeq()); result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); + (Either source, List itemScanningPlans) = result.Head(); source.IsRight.Should().BeTrue(); source.RightToSeq().Should().BeEquivalentTo(episodeMediaItem); itemScanningPlans.Should() - .BeEquivalentTo(new ItemScanningPlan(posterFileName, ScanningAction.Poster)); + .BeEquivalentTo(new ActionPlan(posterFileName, ScanningAction.Poster)); } [Test] public void WithoutNfo_WithOldPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension, [Values("jpg", "jpeg", "png", "gif", "tbn")] string posterExtension) @@ -702,7 +710,7 @@ namespace ErsatzTV.Core.Tests.Metadata var posterFileName = $"/tv/test (2021)/poster.{posterExtension}"; string[] fileNames = { episodeMediaItem.Path, posterFileName }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.TvShow, Seq.create(episodeMediaItem), fileNames.ToSeq()); @@ -712,7 +720,7 @@ namespace ErsatzTV.Core.Tests.Metadata [Test] public void WithoutNfo_WithUpdatedPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension, [Values("jpg", "jpeg", "png", "gif", "tbn")] string posterExtension) @@ -728,7 +736,7 @@ namespace ErsatzTV.Core.Tests.Metadata var posterFileName = $"/tv/test (2021)/poster.{posterExtension}"; string[] fileNames = { episodeMediaItem.Path, posterFileName }; - Seq result = + Seq result = ScannerFor(OldEntriesFor(episodeMediaItem.Path).Concat(NewEntriesFor(posterFileName))) .DetermineActions( MediaType.TvShow, @@ -736,16 +744,16 @@ namespace ErsatzTV.Core.Tests.Metadata fileNames.ToSeq()); result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); + (Either source, List itemScanningPlans) = result.Head(); source.IsRight.Should().BeTrue(); source.RightToSeq().Should().BeEquivalentTo(episodeMediaItem); itemScanningPlans.Should() - .BeEquivalentTo(new ItemScanningPlan(posterFileName, ScanningAction.Poster)); + .BeEquivalentTo(new ActionPlan(posterFileName, ScanningAction.Poster)); } [Test] public void WithNewNfo_WithNewPoster( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension, [Values("jpg", "jpeg", "png", "gif", "tbn")] string posterExtension) @@ -760,19 +768,19 @@ namespace ErsatzTV.Core.Tests.Metadata var posterFileName = $"/tv/test (2021)/poster.{posterExtension}"; string[] fileNames = { episodeMediaItem.Path, nfoFileName, posterFileName }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.TvShow, Seq.create(episodeMediaItem), fileNames.ToSeq()); result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); + (Either source, List itemScanningPlans) = result.Head(); source.IsRight.Should().BeTrue(); source.RightToSeq().Should().BeEquivalentTo(episodeMediaItem); itemScanningPlans.Should().BeEquivalentTo( - new ItemScanningPlan(nfoFileName, ScanningAction.SidecarMetadata), - new ItemScanningPlan(posterFileName, ScanningAction.Poster), - new ItemScanningPlan(nfoFileName, ScanningAction.Collections)); + new ActionPlan(nfoFileName, ScanningAction.SidecarMetadata), + new ActionPlan(posterFileName, ScanningAction.Poster), + new ActionPlan(nfoFileName, ScanningAction.Collections)); } } @@ -790,12 +798,12 @@ namespace ErsatzTV.Core.Tests.Metadata "/movies/test (2021)/Extras/test (2021)", "/movies/test (2021)/Specials/test (2021)")] string baseFileName, - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension) { string[] fileNames = { $"{baseFileName}.{extension}" }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.Movie, Seq.Empty, fileNames.ToSeq()); @@ -815,12 +823,12 @@ namespace ErsatzTV.Core.Tests.Metadata "/movies/test (2021)/test (2021)-trailer", "/movies/test (2021)/test (2021)-other")] string baseFileName, - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension) { string[] fileNames = { $"{baseFileName}.{extension}" }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.Movie, Seq.Empty, fileNames.ToSeq()); @@ -830,7 +838,7 @@ namespace ErsatzTV.Core.Tests.Metadata [Test] public void Movies_Should_Remove_Missing_MediaItems( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension) { var movieMediaItem = new MediaItem @@ -847,29 +855,29 @@ namespace ErsatzTV.Core.Tests.Metadata string[] fileNames = { "anything" }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.Movie, Seq.create(movieMediaItem, movieMediaItem2), fileNames.ToSeq()); result.Count.Should().Be(2); - (Either source1, List itemScanningPlans1) = result.Head(); + (Either source1, List itemScanningPlans1) = result.Head(); source1.IsRight.Should().BeTrue(); source1.RightToSeq().Should().BeEquivalentTo(movieMediaItem); itemScanningPlans1.Should().BeEquivalentTo( - new ItemScanningPlan(movieMediaItem.Path, ScanningAction.Remove)); + new ActionPlan(movieMediaItem.Path, ScanningAction.Remove)); - (Either source2, List itemScanningPlans2) = result.Last(); + (Either source2, List itemScanningPlans2) = result.Last(); source2.IsRight.Should().BeTrue(); source2.RightToSeq().Should().BeEquivalentTo(movieMediaItem2); itemScanningPlans2.Should().BeEquivalentTo( - new ItemScanningPlan(movieMediaItem2.Path, ScanningAction.Remove)); + new ActionPlan(movieMediaItem2.Path, ScanningAction.Remove)); } [Test] public void Episodes_Should_Remove_Missing_MediaItems( - [ValueSource(typeof(TestMediaScannerTests), nameof(VideoFileExtensions))] + [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] string extension) { var movieMediaItem = new MediaItem @@ -886,24 +894,24 @@ namespace ErsatzTV.Core.Tests.Metadata string[] fileNames = { "anything" }; - Seq result = ScannerForOldFiles(fileNames).DetermineActions( + Seq result = ScannerForOldFiles(fileNames).DetermineActions( MediaType.TvShow, Seq.create(movieMediaItem, movieMediaItem2), fileNames.ToSeq()); result.Count.Should().Be(2); - (Either source1, List itemScanningPlans1) = result.Head(); + (Either source1, List itemScanningPlans1) = result.Head(); source1.IsRight.Should().BeTrue(); source1.RightToSeq().Should().BeEquivalentTo(movieMediaItem); itemScanningPlans1.Should().BeEquivalentTo( - new ItemScanningPlan(movieMediaItem.Path, ScanningAction.Remove)); + new ActionPlan(movieMediaItem.Path, ScanningAction.Remove)); - (Either source2, List itemScanningPlans2) = result.Last(); + (Either source2, List itemScanningPlans2) = result.Last(); source2.IsRight.Should().BeTrue(); source2.RightToSeq().Should().BeEquivalentTo(movieMediaItem2); itemScanningPlans2.Should().BeEquivalentTo( - new ItemScanningPlan(movieMediaItem2.Path, ScanningAction.Remove)); + new ActionPlan(movieMediaItem2.Path, ScanningAction.Remove)); } private class FakeLocalFileSystem : ILocalFileSystem diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalMediaSourcePlanner.cs b/ErsatzTV.Core/Interfaces/Metadata/ILocalMediaSourcePlanner.cs new file mode 100644 index 000000000..555463b53 --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Metadata/ILocalMediaSourcePlanner.cs @@ -0,0 +1,14 @@ +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Metadata; +using LanguageExt; + +namespace ErsatzTV.Core.Interfaces.Metadata +{ + public interface ILocalMediaSourcePlanner + { + public Seq DetermineActions( + MediaType mediaType, + Seq mediaItems, + Seq files); + } +} diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs b/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs index eea22c7af..8e413e1a4 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs @@ -5,6 +5,7 @@ namespace ErsatzTV.Core.Interfaces.Metadata { public interface ILocalMetadataProvider { - Task RefreshMetadata(MediaItem mediaItem); + Task RefreshSidecarMetadata(MediaItem mediaItem, string path); + Task RefreshFallbackMetadata(MediaItem mediaItem); } } diff --git a/ErsatzTV.Core/Metadata/ActionPlan.cs b/ErsatzTV.Core/Metadata/ActionPlan.cs new file mode 100644 index 000000000..b10ab95fd --- /dev/null +++ b/ErsatzTV.Core/Metadata/ActionPlan.cs @@ -0,0 +1,4 @@ +namespace ErsatzTV.Core.Metadata +{ + public record ActionPlan(string TargetPath, ScanningAction TargetAction); +} diff --git a/ErsatzTV.Core/Metadata/ItemScanningPlan.cs b/ErsatzTV.Core/Metadata/ItemScanningPlan.cs deleted file mode 100644 index 193b5fe87..000000000 --- a/ErsatzTV.Core/Metadata/ItemScanningPlan.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace ErsatzTV.Core.Metadata -{ - public record ItemScanningPlan(string TargetPath, ScanningAction TargetAction); -} diff --git a/ErsatzTV.Core/Metadata/LocalMediaItemScanningPlan.cs b/ErsatzTV.Core/Metadata/LocalMediaItemScanningPlan.cs deleted file mode 100644 index 2e8d0656d..000000000 --- a/ErsatzTV.Core/Metadata/LocalMediaItemScanningPlan.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Collections.Generic; -using ErsatzTV.Core.Domain; -using LanguageExt; - -namespace ErsatzTV.Core.Metadata -{ - public record LocalMediaItemScanningPlan(Either Source, List ActionPlans); -} diff --git a/ErsatzTV.Core/Metadata/LocalMediaScanner.cs b/ErsatzTV.Core/Metadata/LocalMediaScanner.cs index 6be272032..b07a042fe 100644 --- a/ErsatzTV.Core/Metadata/LocalMediaScanner.cs +++ b/ErsatzTV.Core/Metadata/LocalMediaScanner.cs @@ -1,22 +1,26 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Scheduling; using LanguageExt; using Microsoft.Extensions.Logging; using static LanguageExt.Prelude; +using Seq = LanguageExt.Seq; namespace ErsatzTV.Core.Metadata { public class LocalMediaScanner : ILocalMediaScanner { + private readonly IImageCache _imageCache; private readonly ILocalFileSystem _localFileSystem; + private readonly ILocalMediaSourcePlanner _localMediaSourcePlanner; private readonly ILocalMetadataProvider _localMetadataProvider; - private readonly ILocalPosterProvider _localPosterProvider; private readonly ILocalStatisticsProvider _localStatisticsProvider; private readonly ILogger _logger; private readonly IMediaItemRepository _mediaItemRepository; @@ -29,21 +33,23 @@ namespace ErsatzTV.Core.Metadata IPlayoutRepository playoutRepository, ILocalStatisticsProvider localStatisticsProvider, ILocalMetadataProvider localMetadataProvider, - ILocalPosterProvider localPosterProvider, ISmartCollectionBuilder smartCollectionBuilder, IPlayoutBuilder playoutBuilder, - ILogger logger, - ILocalFileSystem localFileSystem) + ILocalMediaSourcePlanner localMediaSourcePlanner, + ILocalFileSystem localFileSystem, + IImageCache imageCache, + ILogger logger) { _mediaItemRepository = mediaItemRepository; _playoutRepository = playoutRepository; _localStatisticsProvider = localStatisticsProvider; _localMetadataProvider = localMetadataProvider; - _localPosterProvider = localPosterProvider; _smartCollectionBuilder = smartCollectionBuilder; _playoutBuilder = playoutBuilder; - _logger = logger; + _localMediaSourcePlanner = localMediaSourcePlanner; _localFileSystem = localFileSystem; + _imageCache = imageCache; + _logger = logger; } public async Task ScanLocalMediaSource( @@ -62,85 +68,93 @@ namespace ErsatzTV.Core.Metadata List knownMediaItems = await _mediaItemRepository.GetAllByMediaSourceId(localMediaSource.Id); var modifiedPlayoutIds = new List(); - Seq allFiles = _localFileSystem.FindRelevantVideos(localMediaSource); - - var testScanner = new TestMediaScanner(_localFileSystem); - var actions = testScanner.DetermineActions( + Seq actions = _localMediaSourcePlanner.DetermineActions( localMediaSource.MediaType, knownMediaItems.ToSeq(), FindAllFiles(localMediaSource)); - foreach (LocalMediaItemScanningPlan action in actions) + foreach (LocalMediaSourcePlan action in actions) { - foreach (ItemScanningPlan plan in action.ActionPlans) + Option maybeAddPlan = + action.ActionPlans.SingleOrDefault(plan => plan.TargetAction == ScanningAction.Add); + await maybeAddPlan.IfSomeAsync( + async plan => + { + Option maybeMediaItem = await AddMediaItem(localMediaSource, plan.TargetPath); + + // any actions other than "add" need to operate on a media item + maybeMediaItem.IfSome(mediaItem => action.Source = mediaItem); + }); + + foreach (ActionPlan plan in action.ActionPlans.OrderBy(plan => (int) plan.TargetAction)) { string sourcePath = action.Source.Match( - Right: mediaItem => mediaItem.Path, - Left: path => path); - + mediaItem => mediaItem.Path, + path => path); + _logger.LogDebug( - "Action Plan: {Source} / {File} => {Action}", + "{Source}: {Action} with {File}", Path.GetFileName(sourcePath), - Path.GetRelativePath(Path.GetDirectoryName(sourcePath) ?? string.Empty, plan.TargetPath), - plan.TargetAction); + plan.TargetAction, + Path.GetRelativePath(Path.GetDirectoryName(sourcePath) ?? string.Empty, plan.TargetPath)); + + await action.Source.Match( + async mediaItem => + { + var changed = false; + + switch (plan.TargetAction) + { + case ScanningAction.Remove: + await RemoveMissingItem(mediaItem); + break; + case ScanningAction.Poster: + await SavePosterForItem(mediaItem, plan.TargetPath); + break; + case ScanningAction.FallbackMetadata: + await RefreshFallbackMetadataForItem(mediaItem); + break; + case ScanningAction.SidecarMetadata: + await RefreshSidecarMetadataForItem(mediaItem, plan.TargetPath); + break; + case ScanningAction.Statistics: + changed = await RefreshStatisticsForItem(mediaItem, ffprobePath); + break; + case ScanningAction.Collections: + changed = await RefreshCollectionsForItem(mediaItem); + break; + } + + if (changed) + { + List ids = + await _playoutRepository.GetPlayoutIdsForMediaItems(Seq.create(mediaItem)); + modifiedPlayoutIds.AddRange(ids); + } + }, + path => + { + _logger.LogError("This is a bug, something went wrong processing {Path}", path); + return Task.CompletedTask; + }); } } - // // check if the media item exists - // (Seq newFiles, Seq existingMediaItems) = allFiles.Map( - // s => Optional(knownMediaItems.Find(i => i.Path == s)).ToEither(s)) - // .Partition(); - // - // // remove media items that no longer exist - // var missingMediaItems = knownMediaItems.Filter(i => !allFiles.Contains(i.Path)).ToSeq(); - // await RemoveMissingItems(missingMediaItems); - // modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(missingMediaItems)); - // - // Seq staleMetadataMediaItems = scanningMode == ScanningMode.RescanAll - // ? existingMediaItems - // : existingMediaItems.Filter(i => _localFileSystem.ShouldRefreshMetadata(localMediaSource, i)); - // Seq modifiedMediaItems = await RefreshMetadataForItems(ffprobePath, staleMetadataMediaItems); - // modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(modifiedMediaItems)); - // - // // if new, add and store mtime, refresh metadata - // var addedMediaItems = new List(); - // foreach (string path in newFiles) - // { - // _logger.LogDebug("Adding new media item {MediaItem}", path); - // var mediaItem = new MediaItem - // { - // MediaSourceId = localMediaSource.Id, - // Path = path, - // LastWriteTime = File.GetLastWriteTimeUtc(path) - // }; - // - // await _mediaItemRepository.Add(mediaItem); - // await RefreshMetadata(mediaItem, ffprobePath); - // addedMediaItems.Add(mediaItem); - // } - // - // modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(addedMediaItems.ToSeq())); - // - // Seq stalePosterMediaItems = existingMediaItems - // .Filter(_localFileSystem.ShouldRefreshPoster) - // .Concat(addedMediaItems); - // await RefreshPosterForItems(stalePosterMediaItems); - // - // foreach (int playoutId in modifiedPlayoutIds.Distinct()) - // { - // Option maybePlayout = await _playoutRepository.GetFull(playoutId); - // await maybePlayout.Match( - // async playout => - // { - // Playout result = await _playoutBuilder.BuildPlayoutItems(playout, true); - // await _playoutRepository.Update(result); - // }, - // Task.CompletedTask); - // } + foreach (int playoutId in modifiedPlayoutIds.Distinct()) + { + Option maybePlayout = await _playoutRepository.GetFull(playoutId); + await maybePlayout.Match( + async playout => + { + Playout result = await _playoutBuilder.BuildPlayoutItems(playout, true); + await _playoutRepository.Update(result); + }, + Task.CompletedTask); + } return unit; } - + private Seq FindAllFiles(LocalMediaSource localMediaSource) { Seq allDirectories = Directory @@ -152,7 +166,7 @@ namespace ErsatzTV.Core.Metadata Seq excluded = allDirectories.Filter(path => File.Exists(Path.Combine(path, ".etvignore"))); Seq relevantDirectories = allDirectories .Filter(d => !excluded.Any(d.StartsWith)); - // .Filter(d => localMediaSource.MediaType == MediaType.Other || !IsExtrasFolder(d)); + // .Filter(d => localMediaSource.MediaType == MediaType.Other || !IsExtrasFolder(d)); return relevantDirectories .Collect(d => Directory.GetFiles(d, "*", SearchOption.TopDirectoryOnly)) @@ -160,65 +174,116 @@ namespace ErsatzTV.Core.Metadata .ToSeq(); } - private async Task> RefreshMetadataForItems( - string ffprobePath, - Seq staleMetadataMediaItems) + private async Task> AddMediaItem(MediaSource mediaSource, string path) { - var modifiedMediaItems = new List(); - foreach (MediaItem mediaItem in staleMetadataMediaItems) + try { - _logger.LogDebug("Refreshing metadata for media item {MediaItem}", mediaItem.Path); - if (await RefreshMetadata(mediaItem, ffprobePath)) + var mediaItem = new MediaItem { - // only queue playout rebuilds for media items - // where the duration or collections have changed - modifiedMediaItems.Add(mediaItem); - } - } + MediaSourceId = mediaSource.Id, + Path = path, + LastWriteTime = File.GetLastWriteTimeUtc(path) + }; - return modifiedMediaItems.ToSeq(); + await _mediaItemRepository.Add(mediaItem); + + return mediaItem; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to add media item for {Path}", path); + return None; + } } - private async Task RefreshPosterForItems(Seq stalePosterMediaItems) + private async Task RemoveMissingItem(MediaItem mediaItem) { - (Seq movies, Seq episodes) = stalePosterMediaItems - .Map(i => Optional(i).Filter(i2 => i2.Metadata?.MediaType == MediaType.TvShow).ToEither(i)) - .Partition(); + try + { + await _mediaItemRepository.Delete(mediaItem.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to remove missing local media item {MediaItem}", mediaItem.Path); + } + } - // there's a 1:1 movie:poster, so refresh all - foreach (MediaItem movie in movies) + private async Task SavePosterForItem(MediaItem mediaItem, string posterPath) + { + try { - _logger.LogDebug("Refreshing poster for media item {MediaItem}", movie.Path); - await _localPosterProvider.RefreshPoster(movie); + byte[] originalBytes = await File.ReadAllBytesAsync(posterPath); + Either maybeHash = await _imageCache.ResizeAndSaveImage(originalBytes, 220, null); + await maybeHash.Match( + hash => + { + mediaItem.Poster = hash; + mediaItem.PosterLastWriteTime = File.GetLastWriteTimeUtc(posterPath); + return _mediaItemRepository.Update(mediaItem); + }, + error => + { + _logger.LogWarning( + "Unable to save poster to disk from {Path}: {Error}", + posterPath, + error.Value); + return Task.CompletedTask; + }); } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to refresh poster for media item {MediaItem}", mediaItem.Path); + } + } - // we currently have 1 poster per series, so pick the first from each group - IEnumerable episodesToRefresh = episodes.GroupBy(e => e.Metadata.Title) - .SelectMany(g => (Option) g.FirstOrDefault()); + private async Task RefreshStatisticsForItem(MediaItem mediaItem, string ffprobePath) + { + try + { + return await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to refresh statistics for media item {MediaItem}", mediaItem.Path); + return false; + } + } - foreach (MediaItem episode in episodesToRefresh) + private async Task RefreshCollectionsForItem(MediaItem mediaItem) + { + try + { + return await _smartCollectionBuilder.RefreshSmartCollections(mediaItem); + } + catch (Exception ex) { - _logger.LogDebug("Refreshing poster for media item {MediaItem}", episode.Path); - await _localPosterProvider.RefreshPoster(episode); + _logger.LogError(ex, "Failed to refresh collections for media item {MediaItem}", mediaItem.Path); + return false; } } - private async Task RemoveMissingItems(Seq removedMediaItems) + private async Task RefreshSidecarMetadataForItem(MediaItem mediaItem, string path) { - // TODO: flag as missing? delete after some period of time? - foreach (MediaItem mediaItem in removedMediaItems) + try { - _logger.LogDebug("Removing missing local media item {MediaItem}", mediaItem.Path); - await _mediaItemRepository.Delete(mediaItem.Id); + await _localMetadataProvider.RefreshSidecarMetadata(mediaItem, path); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to refresh nfo metadata for media item {MediaItem}", mediaItem.Path); } } - private async Task RefreshMetadata(MediaItem mediaItem, string ffprobePath) + private async Task RefreshFallbackMetadataForItem(MediaItem mediaItem) { - bool durationChange = await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem); - await _localMetadataProvider.RefreshMetadata(mediaItem); - bool collectionChange = await _smartCollectionBuilder.RefreshSmartCollections(mediaItem); - return durationChange || collectionChange; + try + { + await _localMetadataProvider.RefreshFallbackMetadata(mediaItem); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to refresh fallback metadata for media item {MediaItem}", mediaItem.Path); + } } } } diff --git a/ErsatzTV.Core/Metadata/LocalMediaSourcePlan.cs b/ErsatzTV.Core/Metadata/LocalMediaSourcePlan.cs new file mode 100644 index 000000000..2ede96786 --- /dev/null +++ b/ErsatzTV.Core/Metadata/LocalMediaSourcePlan.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using ErsatzTV.Core.Domain; +using LanguageExt; + +namespace ErsatzTV.Core.Metadata +{ + public record LocalMediaSourcePlan(Either Source, List ActionPlans) + { + public Either Source { get; set; } = Source; + } +} diff --git a/ErsatzTV.Core/Metadata/TestMediaScanner.cs b/ErsatzTV.Core/Metadata/LocalMediaSourcePlanner.cs similarity index 78% rename from ErsatzTV.Core/Metadata/TestMediaScanner.cs rename to ErsatzTV.Core/Metadata/LocalMediaSourcePlanner.cs index 7b5adc844..91cfffd1f 100644 --- a/ErsatzTV.Core/Metadata/TestMediaScanner.cs +++ b/ErsatzTV.Core/Metadata/LocalMediaSourcePlanner.cs @@ -9,14 +9,15 @@ using static LanguageExt.Prelude; namespace ErsatzTV.Core.Metadata { - public class TestMediaScanner + // TODO: this needs a better name + public class LocalMediaSourcePlanner : ILocalMediaSourcePlanner { private static readonly Seq ImageFileExtensions = Seq("jpg", "jpeg", "png", "gif", "tbn"); private readonly ILocalFileSystem _localFileSystem; - public TestMediaScanner(ILocalFileSystem localFileSystem) => _localFileSystem = localFileSystem; + public LocalMediaSourcePlanner(ILocalFileSystem localFileSystem) => _localFileSystem = localFileSystem; - public Seq DetermineActions( + public Seq DetermineActions( MediaType mediaType, Seq mediaItems, Seq files) @@ -32,24 +33,25 @@ namespace ErsatzTV.Core.Metadata // new files foreach (string file in newFiles) { - results.Add(file, new ItemScanningPlan(file, ScanningAction.Statistics)); + results.Add(file, new ActionPlan(file, ScanningAction.Add)); + results.Add(file, new ActionPlan(file, ScanningAction.Statistics)); Option maybeNfoFile = LocateNfoFile(mediaType, files, file); maybeNfoFile.BiIter( nfoFile => { - results.Add(file, new ItemScanningPlan(nfoFile, ScanningAction.SidecarMetadata)); - results.Add(file, new ItemScanningPlan(nfoFile, ScanningAction.Collections)); + results.Add(file, new ActionPlan(nfoFile, ScanningAction.SidecarMetadata)); + results.Add(file, new ActionPlan(nfoFile, ScanningAction.Collections)); }, () => { - results.Add(file, new ItemScanningPlan(file, ScanningAction.FallbackMetadata)); - results.Add(file, new ItemScanningPlan(file, ScanningAction.Collections)); + results.Add(file, new ActionPlan(file, ScanningAction.FallbackMetadata)); + results.Add(file, new ActionPlan(file, ScanningAction.Collections)); }); Option maybePoster = LocatePoster(mediaType, files, file); maybePoster.IfSome( - posterFile => results.Add(file, new ItemScanningPlan(posterFile, ScanningAction.Poster))); + posterFile => results.Add(file, new ActionPlan(posterFile, ScanningAction.Poster))); } // existing media items @@ -57,7 +59,7 @@ namespace ErsatzTV.Core.Metadata { if ((mediaItem.LastWriteTime ?? DateTime.MinValue) < _localFileSystem.GetLastWriteTime(mediaItem.Path)) { - results.Add(mediaItem, new ItemScanningPlan(mediaItem.Path, ScanningAction.Statistics)); + results.Add(mediaItem, new ActionPlan(mediaItem.Path, ScanningAction.Statistics)); } Option maybeNfoFile = LocateNfoFile(mediaType, files, mediaItem.Path); @@ -68,8 +70,8 @@ namespace ErsatzTV.Core.Metadata (mediaItem.Metadata.LastWriteTime ?? DateTime.MinValue) < _localFileSystem.GetLastWriteTime(nfoFile)) { - results.Add(mediaItem, new ItemScanningPlan(nfoFile, ScanningAction.SidecarMetadata)); - results.Add(mediaItem, new ItemScanningPlan(nfoFile, ScanningAction.Collections)); + results.Add(mediaItem, new ActionPlan(nfoFile, ScanningAction.SidecarMetadata)); + results.Add(mediaItem, new ActionPlan(nfoFile, ScanningAction.Collections)); } }); @@ -81,7 +83,7 @@ namespace ErsatzTV.Core.Metadata (mediaItem.PosterLastWriteTime ?? DateTime.MinValue) < _localFileSystem.GetLastWriteTime(posterFile)) { - results.Add(mediaItem, new ItemScanningPlan(posterFile, ScanningAction.Poster)); + results.Add(mediaItem, new ActionPlan(posterFile, ScanningAction.Poster)); } }); } @@ -89,7 +91,7 @@ namespace ErsatzTV.Core.Metadata // missing media items foreach (MediaItem mediaItem in mediaItems.Where(i => !files.Contains(i.Path))) { - results.Add(mediaItem, new ItemScanningPlan(mediaItem.Path, ScanningAction.Remove)); + results.Add(mediaItem, new ActionPlan(mediaItem.Path, ScanningAction.Remove)); } return results.Summarize(); @@ -147,15 +149,15 @@ namespace ErsatzTV.Core.Metadata private class IntermediateResults { - private readonly List, ItemScanningPlan>> _rawResults = new(); + private readonly List, ActionPlan>> _rawResults = new(); - public void Add(Either source, ItemScanningPlan plan) => + public void Add(Either source, ActionPlan plan) => _rawResults.Add(Tuple(source, plan)); - public Seq Summarize() => + public Seq Summarize() => _rawResults .GroupBy(t => t.Item1) - .Select(g => new LocalMediaItemScanningPlan(g.Key, g.Select(g2 => g2.Item2).ToList())) + .Select(g => new LocalMediaSourcePlan(g.Key, g.Select(g2 => g2.Item2).ToList())) .ToSeq(); } diff --git a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs index fe96042b7..c9d480757 100644 --- a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs @@ -25,14 +25,15 @@ namespace ErsatzTV.Core.Metadata _logger = logger; } - public async Task RefreshMetadata(MediaItem mediaItem) + public async Task RefreshSidecarMetadata(MediaItem mediaItem, string path) { - Option maybeMetadata = await LoadMetadata(mediaItem); - MediaMetadata metadata = - maybeMetadata.IfNone(() => FallbackMetadataProvider.GetFallbackMetadata(mediaItem)); - await ApplyMetadataUpdate(mediaItem, metadata); + Option maybeMetadata = await LoadMetadata(mediaItem, path); + await maybeMetadata.IfSomeAsync(metadata => ApplyMetadataUpdate(mediaItem, metadata)); } + public Task RefreshFallbackMetadata(MediaItem mediaItem) => + ApplyMetadataUpdate(mediaItem, FallbackMetadataProvider.GetFallbackMetadata(mediaItem)); + private async Task ApplyMetadataUpdate(MediaItem mediaItem, MediaMetadata metadata) { if (mediaItem.Metadata == null) @@ -41,6 +42,7 @@ namespace ErsatzTV.Core.Metadata } mediaItem.Metadata.Source = metadata.Source; + mediaItem.Metadata.LastWriteTime = metadata.LastWriteTime; mediaItem.Metadata.MediaType = metadata.MediaType; mediaItem.Metadata.Title = metadata.Title; mediaItem.Metadata.Subtitle = metadata.Subtitle; @@ -57,9 +59,8 @@ namespace ErsatzTV.Core.Metadata await _mediaItemRepository.Update(mediaItem); } - private async Task> LoadMetadata(MediaItem mediaItem) + private async Task> LoadMetadata(MediaItem mediaItem, string nfoFileName) { - string nfoFileName = Path.ChangeExtension(mediaItem.Path, "nfo"); if (nfoFileName == null || !File.Exists(nfoFileName)) { _logger.LogDebug("NFO file does not exist at {Path}", nfoFileName); @@ -90,6 +91,7 @@ namespace ErsatzTV.Core.Metadata nfo => new MediaMetadata { Source = MetadataSource.Sidecar, + LastWriteTime = File.GetLastWriteTimeUtc(nfoFileName), MediaType = MediaType.TvShow, Title = nfo.ShowTitle, Subtitle = nfo.Title, @@ -117,6 +119,7 @@ namespace ErsatzTV.Core.Metadata nfo => new MediaMetadata { Source = MetadataSource.Sidecar, + LastWriteTime = File.GetLastWriteTimeUtc(nfoFileName), MediaType = MediaType.Movie, Title = nfo.Title, Description = nfo.Outline, diff --git a/ErsatzTV.Core/Metadata/LocalPosterProvider.cs b/ErsatzTV.Core/Metadata/LocalPosterProvider.cs index 1e5012ad3..a90d79ae6 100644 --- a/ErsatzTV.Core/Metadata/LocalPosterProvider.cs +++ b/ErsatzTV.Core/Metadata/LocalPosterProvider.cs @@ -73,7 +73,7 @@ namespace ErsatzTV.Core.Metadata return None; } - private async Task SavePosterToDisk(MediaItem mediaItem, string posterPath) + public async Task SavePosterToDisk(MediaItem mediaItem, string posterPath) { byte[] originalBytes = await File.ReadAllBytesAsync(posterPath); Either maybeHash = await _imageCache.ResizeAndSaveImage(originalBytes, 220, null); diff --git a/ErsatzTV.Core/Metadata/ScanningAction.cs b/ErsatzTV.Core/Metadata/ScanningAction.cs index cbfca37b1..45e04fba8 100644 --- a/ErsatzTV.Core/Metadata/ScanningAction.cs +++ b/ErsatzTV.Core/Metadata/ScanningAction.cs @@ -3,11 +3,12 @@ public enum ScanningAction { None = 0, - Statistics = 1, - SidecarMetadata = 2, - FallbackMetadata = 3, - Collections = 4, - Poster = 5, - Remove = 6 + Add = 1, + Remove = 2, + Statistics = 3, + SidecarMetadata = 4, + FallbackMetadata = 5, + Collections = 6, + Poster = 7, } } diff --git a/ErsatzTV/Services/WorkerService.cs b/ErsatzTV/Services/WorkerService.cs index a7f1bbb19..75538af40 100644 --- a/ErsatzTV/Services/WorkerService.cs +++ b/ErsatzTV/Services/WorkerService.cs @@ -59,7 +59,7 @@ namespace ErsatzTV.Services case RefreshMediaItem refreshMediaItem: string type = refreshMediaItem switch { - RefreshMediaItemMetadata => "metadata", + // RefreshMediaItemMetadata => "metadata", RefreshMediaItemStatistics => "statistics", RefreshMediaItemCollections => "collections", RefreshMediaItemPoster => "poster",