diff --git a/ErsatzTV.Core.Tests/Metadata/TestMediaScannerTests.cs b/ErsatzTV.Core.Tests/Metadata/TestMediaScannerTests.cs index d1f598fd7..f66992448 100644 --- a/ErsatzTV.Core.Tests/Metadata/TestMediaScannerTests.cs +++ b/ErsatzTV.Core.Tests/Metadata/TestMediaScannerTests.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Metadata; using FluentAssertions; using LanguageExt; @@ -10,18 +12,30 @@ namespace ErsatzTV.Core.Tests.Metadata [TestFixture] public class TestMediaScannerTests { + private static TestMediaScanner ScannerForFiles(IEnumerable fileNames) + { + var localFileSystem = new FakeLocalFileSystem(fileNames); + return new TestMediaScanner(localFileSystem); + } + + private static TestMediaScanner ScannerForNewFiles(IEnumerable fileNames) + { + IEnumerable fakeFiles = + fileNames.Map(f => new FakeFileSystemEntry(f, DateTime.MaxValue)); + var localFileSystem = new FakeLocalFileSystem(fakeFiles); + return new TestMediaScanner(localFileSystem); + } + [TestFixture] public class NewMovieTests { - private readonly TestMediaScanner _scanner = new(); - [Test] public void WithoutNfo_WithoutPoster() { var movieFileName = "/movies/test (2021)/test (2021).mkv"; string[] fileNames = { movieFileName }; - Seq result = _scanner.DetermineActions( + Seq result = ScannerForFiles(fileNames).DetermineActions( MediaType.Movie, Seq.Empty, fileNames.ToSeq()); @@ -45,7 +59,7 @@ namespace ErsatzTV.Core.Tests.Metadata var nfoFileName = $"/movies/test (2021)/{nfoFile}"; string[] fileNames = { movieFileName, nfoFileName }; - Seq result = _scanner.DetermineActions( + Seq result = ScannerForFiles(fileNames).DetermineActions( MediaType.Movie, Seq.Empty, fileNames.ToSeq()); @@ -72,7 +86,7 @@ namespace ErsatzTV.Core.Tests.Metadata string[] fileNames = { movieFileName, posterFileName }; - Seq result = _scanner.DetermineActions( + Seq result = ScannerForFiles(fileNames).DetermineActions( MediaType.Movie, Seq.Empty, fileNames.ToSeq()); @@ -103,7 +117,7 @@ namespace ErsatzTV.Core.Tests.Metadata string[] fileNames = { movieFileName, nfoFileName, posterFileName }; - Seq result = _scanner.DetermineActions( + Seq result = ScannerForFiles(fileNames).DetermineActions( MediaType.Movie, Seq.Empty, fileNames.ToSeq()); @@ -123,11 +137,8 @@ namespace ErsatzTV.Core.Tests.Metadata [TestFixture] public class ExistingMovieTests { - private readonly TestMediaScanner _scanner = new(); - - // TODO: mtime should affect this [Test] - public void WithNewNfo_WithoutPoster( + public void Fallback_WithNewNfo_WithoutPoster( [Values("test (2021).nfo", "movie.nfo")] string nfoFile) { @@ -140,7 +151,57 @@ namespace ErsatzTV.Core.Tests.Metadata var nfoFileName = $"/movies/test (2021)/{nfoFile}"; string[] fileNames = { movieMediaItem.Path, nfoFileName }; - Seq result = _scanner.DetermineActions( + Seq result = ScannerForFiles(fileNames).DetermineActions( + MediaType.Movie, + Seq.create(movieMediaItem), + fileNames.ToSeq()); + + result.Count.Should().Be(1); + (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(movieMediaItem.Path, ScanningAction.Collections)); + } + + [Test] + public void Sidecar_WithOldNfo_WithoutPoster( + [Values("test (2021).nfo", "movie.nfo")] + string nfoFile) + { + var movieMediaItem = new MediaItem + { + Metadata = new MediaMetadata { Source = MetadataSource.Sidecar }, + Path = "/movies/test (2021)/test (2021).mkv" + }; + + var nfoFileName = $"/movies/test (2021)/{nfoFile}"; + string[] fileNames = { movieMediaItem.Path, nfoFileName }; + + Seq result = ScannerForFiles(fileNames).DetermineActions( + MediaType.Movie, + Seq.create(movieMediaItem), + fileNames.ToSeq()); + + result.Count.Should().Be(0); + } + + [Test] + public void Sidecar_WithUpdatedNfo_WithoutPoster( + [Values("test (2021).nfo", "movie.nfo")] + string nfoFile) + { + var movieMediaItem = new MediaItem + { + Metadata = new MediaMetadata { Source = MetadataSource.Sidecar }, + Path = "/movies/test (2021)/test (2021).mkv" + }; + + var nfoFileName = $"/movies/test (2021)/{nfoFile}"; + string[] fileNames = { movieMediaItem.Path, nfoFileName }; + + Seq result = ScannerForNewFiles(fileNames).DetermineActions( MediaType.Movie, Seq.create(movieMediaItem), fileNames.ToSeq()); @@ -170,7 +231,64 @@ namespace ErsatzTV.Core.Tests.Metadata var posterFileName = $"/movies/test (2021)/{basePosterName}poster.{posterExtension}"; string[] fileNames = { movieMediaItem.Path, posterFileName }; - Seq result = _scanner.DetermineActions( + Seq result = ScannerForFiles(fileNames).DetermineActions( + MediaType.Movie, + Seq.create(movieMediaItem), + fileNames.ToSeq()); + + result.Count.Should().Be(1); + (Either source, List itemScanningPlans) = result.Head(); + source.IsRight.Should().BeTrue(); + source.RightToSeq().Should().BeEquivalentTo(movieMediaItem); + itemScanningPlans.Should() + .BeEquivalentTo(new ItemScanningPlan(posterFileName, ScanningAction.Poster)); + } + + [Test] + public void WithoutNfo_WithOldPoster( + [Values("", "test (2021)-")] + string basePosterName, + [Values("jpg", "jpeg", "png", "gif", "tbn")] + string posterExtension) + { + var movieMediaItem = new MediaItem + { + Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, + Path = "/movies/test (2021)/test (2021).mkv", + Poster = "anything", + PosterLastWriteTime = DateTime.UtcNow + }; + + var posterFileName = $"/movies/test (2021)/{basePosterName}poster.{posterExtension}"; + string[] fileNames = { movieMediaItem.Path, posterFileName }; + + Seq result = ScannerForFiles(fileNames).DetermineActions( + MediaType.Movie, + Seq.create(movieMediaItem), + fileNames.ToSeq()); + + result.Count.Should().Be(0); + } + + [Test] + public void WithoutNfo_WithUpdatedPoster( + [Values("", "test (2021)-")] + string basePosterName, + [Values("jpg", "jpeg", "png", "gif", "tbn")] + string posterExtension) + { + var movieMediaItem = new MediaItem + { + Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, + Path = "/movies/test (2021)/test (2021).mkv", + Poster = "anything", + PosterLastWriteTime = DateTime.UtcNow + }; + + var posterFileName = $"/movies/test (2021)/{basePosterName}poster.{posterExtension}"; + string[] fileNames = { movieMediaItem.Path, posterFileName }; + + Seq result = ScannerForNewFiles(fileNames).DetermineActions( MediaType.Movie, Seq.create(movieMediaItem), fileNames.ToSeq()); @@ -202,7 +320,7 @@ namespace ErsatzTV.Core.Tests.Metadata var posterFileName = $"/movies/test (2021)/{basePosterName}poster.{posterExtension}"; string[] fileNames = { movieMediaItem.Path, nfoFileName, posterFileName }; - Seq result = _scanner.DetermineActions( + Seq result = ScannerForFiles(fileNames).DetermineActions( MediaType.Movie, Seq.create(movieMediaItem), fileNames.ToSeq()); @@ -221,15 +339,13 @@ namespace ErsatzTV.Core.Tests.Metadata [TestFixture] public class NewEpisodeTests { - private readonly TestMediaScanner _scanner = new(); - [Test] public void WithoutNfo_WithoutPoster() { var episodeFileName = "/tv/test (2021)/season 01/test (2021) - s01e03.mkv"; string[] fileNames = { episodeFileName }; - Seq result = _scanner.DetermineActions( + Seq result = ScannerForFiles(fileNames).DetermineActions( MediaType.TvShow, Seq.Empty, fileNames.ToSeq()); @@ -251,7 +367,7 @@ namespace ErsatzTV.Core.Tests.Metadata var nfoFileName = "/tv/test (2021)/season 01/test (2021) - s01e03.nfo"; string[] fileNames = { episodeFileName, nfoFileName }; - Seq result = _scanner.DetermineActions( + Seq result = ScannerForFiles(fileNames).DetermineActions( MediaType.TvShow, Seq.Empty, fileNames.ToSeq()); @@ -276,7 +392,7 @@ namespace ErsatzTV.Core.Tests.Metadata string[] fileNames = { episodeFileName, posterFileName }; - Seq result = _scanner.DetermineActions( + Seq result = ScannerForFiles(fileNames).DetermineActions( MediaType.TvShow, Seq.Empty, fileNames.ToSeq()); @@ -303,7 +419,7 @@ namespace ErsatzTV.Core.Tests.Metadata string[] fileNames = { episodeFileName, nfoFileName, posterFileName }; - Seq result = _scanner.DetermineActions( + Seq result = ScannerForFiles(fileNames).DetermineActions( MediaType.TvShow, Seq.Empty, fileNames.ToSeq()); @@ -319,15 +435,12 @@ namespace ErsatzTV.Core.Tests.Metadata new ItemScanningPlan(episodeFileName, ScanningAction.Collections)); } } - + [TestFixture] public class ExistingEpisodeTests { - private readonly TestMediaScanner _scanner = new(); - - // TODO: mtime should affect this [Test] - public void WithNewNfo_WithoutPoster() + public void Fallback_WithNewNfo_WithoutPoster() { var episodeMediaItem = new MediaItem { @@ -338,7 +451,53 @@ namespace ErsatzTV.Core.Tests.Metadata var nfoFileName = "/tv/test (2021)/season 01/test (2021) - s01e03.nfo"; string[] fileNames = { episodeMediaItem.Path, nfoFileName }; - Seq result = _scanner.DetermineActions( + Seq result = ScannerForFiles(fileNames).DetermineActions( + MediaType.TvShow, + Seq.create(episodeMediaItem), + fileNames.ToSeq()); + + result.Count.Should().Be(1); + (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(episodeMediaItem.Path, ScanningAction.Collections)); + } + + [Test] + public void Sidecar_WithOldNfo_WithoutPoster() + { + var episodeMediaItem = new MediaItem + { + Metadata = new MediaMetadata { Source = MetadataSource.Sidecar }, + Path = "/tv/test (2021)/season 01/test (2021) - s01e03.mkv" + }; + + var nfoFileName = "/tv/test (2021)/season 01/test (2021) - s01e03.nfo"; + string[] fileNames = { episodeMediaItem.Path, nfoFileName }; + + Seq result = ScannerForFiles(fileNames).DetermineActions( + MediaType.TvShow, + Seq.create(episodeMediaItem), + fileNames.ToSeq()); + + result.Count.Should().Be(0); + } + + [Test] + public void Sidecar_WithUpdatedNfo_WithoutPoster() + { + var episodeMediaItem = new MediaItem + { + Metadata = new MediaMetadata { Source = MetadataSource.Sidecar }, + Path = "/tv/test (2021)/season 01/test (2021) - s01e03.mkv" + }; + + var nfoFileName = "/tv/test (2021)/season 01/test (2021) - s01e03.nfo"; + string[] fileNames = { episodeMediaItem.Path, nfoFileName }; + + Seq result = ScannerForNewFiles(fileNames).DetermineActions( MediaType.TvShow, Seq.create(episodeMediaItem), fileNames.ToSeq()); @@ -366,7 +525,60 @@ namespace ErsatzTV.Core.Tests.Metadata var posterFileName = $"/tv/test (2021)/poster.{posterExtension}"; string[] fileNames = { episodeMediaItem.Path, posterFileName }; - Seq result = _scanner.DetermineActions( + Seq result = ScannerForFiles(fileNames).DetermineActions( + MediaType.TvShow, + Seq.create(episodeMediaItem), + fileNames.ToSeq()); + + result.Count.Should().Be(1); + (Either source, List itemScanningPlans) = result.Head(); + source.IsRight.Should().BeTrue(); + source.RightToSeq().Should().BeEquivalentTo(episodeMediaItem); + itemScanningPlans.Should() + .BeEquivalentTo(new ItemScanningPlan(posterFileName, ScanningAction.Poster)); + } + + [Test] + public void WithoutNfo_WithOldPoster( + [Values("jpg", "jpeg", "png", "gif", "tbn")] + string posterExtension) + { + var episodeMediaItem = new MediaItem + { + Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, + Path = "/tv/test (2021)/season 01/test (2021) - s01e03.mkv", + Poster = "anything", + PosterLastWriteTime = DateTime.UtcNow + }; + + var posterFileName = $"/tv/test (2021)/poster.{posterExtension}"; + string[] fileNames = { episodeMediaItem.Path, posterFileName }; + + Seq result = ScannerForFiles(fileNames).DetermineActions( + MediaType.TvShow, + Seq.create(episodeMediaItem), + fileNames.ToSeq()); + + result.Count.Should().Be(0); + } + + [Test] + public void WithoutNfo_WithUpdatedPoster( + [Values("jpg", "jpeg", "png", "gif", "tbn")] + string posterExtension) + { + var episodeMediaItem = new MediaItem + { + Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, + Path = "/tv/test (2021)/season 01/test (2021) - s01e03.mkv", + Poster = "anything", + PosterLastWriteTime = DateTime.UtcNow + }; + + var posterFileName = $"/tv/test (2021)/poster.{posterExtension}"; + string[] fileNames = { episodeMediaItem.Path, posterFileName }; + + Seq result = ScannerForNewFiles(fileNames).DetermineActions( MediaType.TvShow, Seq.create(episodeMediaItem), fileNames.ToSeq()); @@ -394,7 +606,7 @@ namespace ErsatzTV.Core.Tests.Metadata var posterFileName = $"/tv/test (2021)/poster.{posterExtension}"; string[] fileNames = { episodeMediaItem.Path, nfoFileName, posterFileName }; - Seq result = _scanner.DetermineActions( + Seq result = ScannerForFiles(fileNames).DetermineActions( MediaType.TvShow, Seq.create(episodeMediaItem), fileNames.ToSeq()); @@ -409,5 +621,84 @@ namespace ErsatzTV.Core.Tests.Metadata new ItemScanningPlan(episodeMediaItem.Path, ScanningAction.Collections)); } } + + [Test] + [TestCase("/movies/test (2021)/Behind The Scenes/test (2021).mkv")] + [TestCase("/movies/test (2021)/Deleted Scenes/test (2021).mkv")] + [TestCase("/movies/test (2021)/Featurettes/test (2021).mkv")] + [TestCase("/movies/test (2021)/Interviews/test (2021).mkv")] + [TestCase("/movies/test (2021)/Scenes/test (2021).mkv")] + [TestCase("/movies/test (2021)/Shorts/test (2021).mkv")] + [TestCase("/movies/test (2021)/Trailers/test (2021).mkv")] + [TestCase("/movies/test (2021)/Other/test (2021).mkv")] + public void Movies_Should_Ignore_ExtraFolders(string fileName) + { + string[] fileNames = { fileName }; + + Seq result = ScannerForFiles(fileNames).DetermineActions( + MediaType.Movie, + Seq.Empty, + fileNames.ToSeq()); + + result.Count.Should().Be(0); + } + + [Test] + [TestCase("/movies/test (2021)/test (2021)-behindthescenes.mkv")] + [TestCase("/movies/test (2021)/test (2021)-deleted.mkv")] + [TestCase("/movies/test (2021)/test (2021)-featurette.mkv")] + [TestCase("/movies/test (2021)/test (2021)-interview.mkv")] + [TestCase("/movies/test (2021)/test (2021)-scene.mkv")] + [TestCase("/movies/test (2021)/test (2021)-short.mkv")] + [TestCase("/movies/test (2021)/test (2021)-trailer.mkv")] + [TestCase("/movies/test (2021)/test (2021)-other.mkv")] + public void Movies_Should_Ignore_ExtraFiles(string fileName) + { + string[] fileNames = { fileName }; + + Seq result = ScannerForFiles(fileNames).DetermineActions( + MediaType.Movie, + Seq.Empty, + fileNames.ToSeq()); + + result.Count.Should().Be(0); + } + + private class FakeLocalFileSystem : ILocalFileSystem + { + private readonly Dictionary _files = new(); + + public FakeLocalFileSystem(IEnumerable files) + { + // default to being old/unmodified + foreach (string file in files) + { + _files.Add(file, DateTime.MinValue); + } + } + + public FakeLocalFileSystem(IEnumerable entries) + { + foreach ((string path, DateTime modifyTime) in entries) + { + _files.Add(path, modifyTime); + } + } + + public DateTime GetLastWriteTime(string path) => + _files.ContainsKey(path) ? _files[path] : DateTime.MinValue; + + public bool IsMediaSourceAccessible(LocalMediaSource localMediaSource) => throw new NotSupportedException(); + + public Seq FindRelevantVideos(LocalMediaSource localMediaSource) => + throw new NotSupportedException(); + + public bool ShouldRefreshMetadata(LocalMediaSource localMediaSource, MediaItem mediaItem) => + throw new NotSupportedException(); + + public bool ShouldRefreshPoster(MediaItem mediaItem) => throw new NotSupportedException(); + } + + private record FakeFileSystemEntry(string Path, DateTime ModifyTime); } } diff --git a/ErsatzTV.Core/Domain/MediaItem.cs b/ErsatzTV.Core/Domain/MediaItem.cs index d5e8fd7b7..5136d3b8e 100644 --- a/ErsatzTV.Core/Domain/MediaItem.cs +++ b/ErsatzTV.Core/Domain/MediaItem.cs @@ -10,6 +10,7 @@ namespace ErsatzTV.Core.Domain public MediaSource Source { get; set; } public string Path { get; set; } public string Poster { get; set; } + public DateTime? PosterLastWriteTime { get; set; } public MediaMetadata Metadata { get; set; } public DateTime? LastWriteTime { get; set; } public IList SimpleMediaCollections { get; set; } diff --git a/ErsatzTV.Core/Domain/MediaMetadata.cs b/ErsatzTV.Core/Domain/MediaMetadata.cs index 71421f165..7e725d0bb 100644 --- a/ErsatzTV.Core/Domain/MediaMetadata.cs +++ b/ErsatzTV.Core/Domain/MediaMetadata.cs @@ -6,6 +6,7 @@ namespace ErsatzTV.Core.Domain public record MediaMetadata : IDisplaySize { public MetadataSource Source { get; set; } + public DateTime? LastWriteTime { get; set; } public TimeSpan Duration { get; set; } public string SampleAspectRatio { get; set; } public string DisplayAspectRatio { get; set; } diff --git a/ErsatzTV.Core/Domain/MetadataSource.cs b/ErsatzTV.Core/Domain/MetadataSource.cs index 1c3ecfc0e..3cb073041 100644 --- a/ErsatzTV.Core/Domain/MetadataSource.cs +++ b/ErsatzTV.Core/Domain/MetadataSource.cs @@ -2,7 +2,7 @@ { public enum MetadataSource { - Sidecar = 1, - Fallback = 2 + Fallback = 0, + Sidecar = 1 } } diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs b/ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs index 10de3a11a..d2d0bb575 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs @@ -1,10 +1,12 @@ -using ErsatzTV.Core.Domain; +using System; +using ErsatzTV.Core.Domain; using LanguageExt; namespace ErsatzTV.Core.Interfaces.Metadata { public interface ILocalFileSystem { + public DateTime GetLastWriteTime(string path); public bool IsMediaSourceAccessible(LocalMediaSource localMediaSource); public Seq FindRelevantVideos(LocalMediaSource localMediaSource); public bool ShouldRefreshMetadata(LocalMediaSource localMediaSource, MediaItem mediaItem); diff --git a/ErsatzTV.Core/Metadata/LocalFileSystem.cs b/ErsatzTV.Core/Metadata/LocalFileSystem.cs index 74153e6bb..dd9adaacd 100644 --- a/ErsatzTV.Core/Metadata/LocalFileSystem.cs +++ b/ErsatzTV.Core/Metadata/LocalFileSystem.cs @@ -10,6 +10,9 @@ namespace ErsatzTV.Core.Metadata { public class LocalFileSystem : ILocalFileSystem { + public DateTime GetLastWriteTime(string path) => + File.GetLastWriteTimeUtc(path); + public bool IsMediaSourceAccessible(LocalMediaSource localMediaSource) => Directory.Exists(localMediaSource.Folder); diff --git a/ErsatzTV.Core/Metadata/LocalMediaScanner.cs b/ErsatzTV.Core/Metadata/LocalMediaScanner.cs index 448324924..1aa2b5745 100644 --- a/ErsatzTV.Core/Metadata/LocalMediaScanner.cs +++ b/ErsatzTV.Core/Metadata/LocalMediaScanner.cs @@ -64,60 +64,99 @@ namespace ErsatzTV.Core.Metadata Seq allFiles = _localFileSystem.FindRelevantVideos(localMediaSource); - // 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)); + var testScanner = new TestMediaScanner(_localFileSystem); + var actions = testScanner.DetermineActions( + localMediaSource.MediaType, + knownMediaItems.ToSeq(), + FindAllFiles(localMediaSource)); - // if new, add and store mtime, refresh metadata - var addedMediaItems = new List(); - foreach (string path in newFiles) + foreach (var action in actions) { - _logger.LogDebug("Adding new media item {MediaItem}", path); - var mediaItem = new MediaItem + foreach (var plan in action.ActionPlans) { - MediaSourceId = localMediaSource.Id, - Path = path, - LastWriteTime = File.GetLastWriteTimeUtc(path) - }; - - await _mediaItemRepository.Add(mediaItem); - await RefreshMetadata(mediaItem, ffprobePath); - addedMediaItems.Add(mediaItem); + _logger.LogDebug( + "Action Plan: {Source} / {File} => {Action}", + action.Source.Match( + Right: mediaItem => Path.GetFileName(mediaItem.Path), + Left: Path.GetFileName), + plan.TargetPath, + plan.TargetAction); + } } - 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); - } + // // 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); + // } return unit; } + + private Seq FindAllFiles(LocalMediaSource localMediaSource) + { + Seq allDirectories = Directory + .GetDirectories(localMediaSource.Folder, "*", SearchOption.AllDirectories) + .ToSeq() + .Add(localMediaSource.Folder); + + // remove any directories with an .etvignore file locally, or in any parent directory + 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)); + + return relevantDirectories + .Collect(d => Directory.GetFiles(d, "*", SearchOption.TopDirectoryOnly)) + .OrderBy(identity) + .ToSeq(); + } private async Task> RefreshMetadataForItems( string ffprobePath, diff --git a/ErsatzTV.Core/Metadata/TestMediaScanner.cs b/ErsatzTV.Core/Metadata/TestMediaScanner.cs index 3499435d0..3e6521613 100644 --- a/ErsatzTV.Core/Metadata/TestMediaScanner.cs +++ b/ErsatzTV.Core/Metadata/TestMediaScanner.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Metadata; using LanguageExt; using static LanguageExt.Prelude; @@ -10,13 +11,10 @@ namespace ErsatzTV.Core.Metadata { public class TestMediaScanner { - // @formatter:off - private static readonly Seq VideoFileExtensions = Seq( - ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".mp4", - ".m4p", ".m4v", ".avi", ".wmv", ".mov", ".mkv", ".ts"); - // @formatter:on - private static readonly Seq ImageFileExtensions = Seq("jpg", "jpeg", "png", "gif", "tbn"); + private readonly ILocalFileSystem _localFileSystem; + + public TestMediaScanner(ILocalFileSystem localFileSystem) => _localFileSystem = localFileSystem; public Seq DetermineActions( MediaType mediaType, @@ -24,7 +22,8 @@ namespace ErsatzTV.Core.Metadata Seq files) { var results = new IntermediateResults(); - Seq videoFiles = files.Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))); + Seq videoFiles = files.Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))) + .Filter(f => !IsExtra(f)); (Seq newFiles, Seq existingMediaItems) = videoFiles.Map( s => mediaItems.Find(i => i.Path == s).ToEither(s)) @@ -49,28 +48,43 @@ namespace ErsatzTV.Core.Metadata // existing media items foreach (MediaItem mediaItem in existingMediaItems) { - if (mediaItem.Metadata == null || mediaItem.Metadata.Source == MetadataSource.Fallback) - { - Option maybeNfoFile = LocateNfoFile(mediaType, files, mediaItem.Path); - maybeNfoFile.IfSome( - nfoFile => + Option maybeNfoFile = LocateNfoFile(mediaType, files, mediaItem.Path); + maybeNfoFile.IfSome( + nfoFile => + { + if (mediaItem.Metadata == null || mediaItem.Metadata.Source == MetadataSource.Fallback || + (mediaItem.Metadata.LastWriteTime ?? DateTime.MinValue) < + _localFileSystem.GetLastWriteTime(nfoFile)) { results.Add(mediaItem, new ItemScanningPlan(nfoFile, ScanningAction.SidecarMetadata)); results.Add(mediaItem, new ItemScanningPlan(mediaItem.Path, ScanningAction.Collections)); - }); - } - - if (string.IsNullOrWhiteSpace(mediaItem.Poster)) - { - Option maybePoster = LocatePoster(mediaType, files, mediaItem.Path); - maybePoster.IfSome( - posterFile => results.Add(mediaItem, new ItemScanningPlan(posterFile, ScanningAction.Poster))); - } + } + }); + + Option maybePoster = LocatePoster(mediaType, files, mediaItem.Path); + maybePoster.IfSome( + posterFile => + { + if (string.IsNullOrWhiteSpace(mediaItem.Poster) || + (mediaItem.PosterLastWriteTime ?? DateTime.MinValue) < + _localFileSystem.GetLastWriteTime(posterFile)) + { + results.Add(mediaItem, new ItemScanningPlan(posterFile, ScanningAction.Poster)); + } + }); } return results.Summarize(); } + private static bool IsExtra(string path) + { + string folder = Path.GetFileName(Path.GetDirectoryName(path) ?? string.Empty); + string file = Path.GetFileNameWithoutExtension(path); + return ExtraDirectories.Contains(folder, StringComparer.OrdinalIgnoreCase) + || ExtraFiles.Any(f => file.EndsWith(f, StringComparison.OrdinalIgnoreCase)); + } + private static Option LocateNfoFile(MediaType mediaType, Seq files, string file) { switch (mediaType) @@ -126,5 +140,19 @@ namespace ErsatzTV.Core.Metadata .Select(g => new LocalMediaItemScanningPlan(g.Key, g.Select(g2 => g2.Item2).ToList())) .ToSeq(); } + + // @formatter:off + private static readonly Seq VideoFileExtensions = Seq( + ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".mp4", + ".m4p", ".m4v", ".avi", ".wmv", ".mov", ".mkv", ".ts"); + + private static readonly Seq ExtraDirectories = Seq( + "behind the scenes","deleted scenes","featurettes", + "interviews","scenes","shorts","trailers","other"); + + private static readonly Seq ExtraFiles = Seq( + "behindthescenes","deleted","featurette", + "interview","scene","short","trailer","other"); + // @formatter:on } } diff --git a/ErsatzTV.Infrastructure/Migrations/20210215153541_MetadataOptimizations.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20210215153541_MetadataOptimizations.Designer.cs new file mode 100644 index 000000000..6974ecfba --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210215153541_MetadataOptimizations.Designer.cs @@ -0,0 +1,905 @@ +// +using System; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace ErsatzTV.Infrastructure.Migrations +{ + [DbContext(typeof(TvContext))] + [Migration("20210215153541_MetadataOptimizations")] + partial class MetadataOptimizations + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.3"); + + modelBuilder.Entity("ErsatzTV.Core.AggregateModels.GenericIntegerId", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + }); + + modelBuilder.Entity("ErsatzTV.Core.AggregateModels.MediaCollectionSummary", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("IsSimple") + .HasColumnType("INTEGER"); + + b.Property("ItemCount") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + }); + + modelBuilder.Entity("ErsatzTV.Core.AggregateModels.MediaItemSummary", b => + { + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Subtitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FFmpegProfileId") + .HasColumnType("INTEGER"); + + b.Property("Logo") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("StreamingMode") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("FFmpegProfileId"); + + b.HasIndex("Number") + .IsUnique(); + + b.ToTable("Channels"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ConfigElement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("ConfigElements"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudioBitrate") + .HasColumnType("INTEGER"); + + b.Property("AudioBufferSize") + .HasColumnType("INTEGER"); + + b.Property("AudioChannels") + .HasColumnType("INTEGER"); + + b.Property("AudioCodec") + .HasColumnType("TEXT"); + + b.Property("AudioSampleRate") + .HasColumnType("INTEGER"); + + b.Property("AudioVolume") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizeAudio") + .HasColumnType("INTEGER"); + + b.Property("NormalizeAudioCodec") + .HasColumnType("INTEGER"); + + b.Property("NormalizeResolution") + .HasColumnType("INTEGER"); + + b.Property("NormalizeVideoCodec") + .HasColumnType("INTEGER"); + + b.Property("ResolutionId") + .HasColumnType("INTEGER"); + + b.Property("ThreadCount") + .HasColumnType("INTEGER"); + + b.Property("Transcode") + .HasColumnType("INTEGER"); + + b.Property("VideoBitrate") + .HasColumnType("INTEGER"); + + b.Property("VideoBufferSize") + .HasColumnType("INTEGER"); + + b.Property("VideoCodec") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ResolutionId"); + + b.ToTable("FFmpegProfiles"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("MediaCollections"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastWriteTime") + .HasColumnType("TEXT"); + + b.Property("MediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("PosterLastWriteTime") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaSourceId"); + + b.ToTable("MediaItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("SourceType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("MediaSources"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("ProgramSchedulePlayoutType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Finish") + .HasColumnType("TEXT"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("Start") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.ToTable("PlayoutItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("MediaCollectionId") + .HasColumnType("INTEGER"); + + b.HasKey("PlayoutId", "ProgramScheduleId", "MediaCollectionId"); + + b.HasIndex("MediaCollectionId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("PlayoutProgramScheduleItemAnchors"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("PlexMediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlexMediaSourceId"); + + b.ToTable("PlexMediaSourceConnections"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceLibrary", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MediaType") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PlexMediaSourceId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlexMediaSourceId"); + + b.ToTable("PlexMediaSourceLibraries"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MediaCollectionPlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ProgramSchedules"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("MediaCollectionId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaCollectionId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("ProgramScheduleItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Resolution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Resolutions"); + }); + + modelBuilder.Entity("MediaItemSimpleMediaCollection", b => + { + b.Property("ItemsId") + .HasColumnType("INTEGER"); + + b.Property("SimpleMediaCollectionsId") + .HasColumnType("INTEGER"); + + b.HasKey("ItemsId", "SimpleMediaCollectionsId"); + + b.HasIndex("SimpleMediaCollectionsId"); + + b.ToTable("MediaItemSimpleMediaCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SimpleMediaCollection", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaCollection"); + + b.ToTable("SimpleMediaCollections"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionMediaCollection", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaCollection"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("ShowTitle") + .HasColumnType("TEXT"); + + b.HasIndex("ShowTitle", "SeasonNumber") + .IsUnique(); + + b.ToTable("TelevisionMediaCollections"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("Folder") + .HasColumnType("TEXT"); + + b.Property("MediaType") + .HasColumnType("INTEGER"); + + b.ToTable("LocalMediaSources"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("ClientIdentifier") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.ToTable("PlexMediaSources"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("OfflineTail") + .HasColumnType("INTEGER"); + + b.Property("PlayoutDuration") + .HasColumnType("TEXT"); + + b.ToTable("ProgramScheduleDurationItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleFloodItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.ToTable("ProgramScheduleMultipleItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleOneItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.HasOne("ErsatzTV.Core.Domain.FFmpegProfile", "FFmpegProfile") + .WithMany() + .HasForeignKey("FFmpegProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FFmpegProfile"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.HasOne("ErsatzTV.Core.Domain.Resolution", "Resolution") + .WithMany() + .HasForeignKey("ResolutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Resolution"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", "Source") + .WithMany() + .HasForeignKey("MediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.MediaMetadata", "Metadata", b1 => + { + b1.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b1.Property("Aired") + .HasColumnType("TEXT"); + + b1.Property("AudioCodec") + .HasColumnType("TEXT"); + + b1.Property("ContentRating") + .HasColumnType("TEXT"); + + b1.Property("Description") + .HasColumnType("TEXT"); + + b1.Property("DisplayAspectRatio") + .HasColumnType("TEXT"); + + b1.Property("Duration") + .HasColumnType("TEXT"); + + b1.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b1.Property("Height") + .HasColumnType("INTEGER"); + + b1.Property("LastWriteTime") + .HasColumnType("TEXT"); + + b1.Property("MediaType") + .HasColumnType("INTEGER"); + + b1.Property("SampleAspectRatio") + .HasColumnType("TEXT"); + + b1.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b1.Property("SortTitle") + .HasColumnType("TEXT"); + + b1.Property("Source") + .HasColumnType("INTEGER"); + + b1.Property("Subtitle") + .HasColumnType("TEXT"); + + b1.Property("Title") + .HasColumnType("TEXT"); + + b1.Property("VideoCodec") + .HasColumnType("TEXT"); + + b1.Property("VideoScanType") + .HasColumnType("INTEGER"); + + b1.Property("Width") + .HasColumnType("INTEGER"); + + b1.HasKey("MediaItemId"); + + b1.ToTable("MediaItems"); + + b1.WithOwner() + .HasForeignKey("MediaItemId"); + }); + + b.Navigation("Metadata"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.HasOne("ErsatzTV.Core.Domain.Channel", "Channel") + .WithMany("Playouts") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Playouts") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 => + { + b1.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b1.Property("NextScheduleItemId") + .HasColumnType("INTEGER"); + + b1.Property("NextStart") + .HasColumnType("TEXT"); + + b1.HasKey("PlayoutId"); + + b1.HasIndex("NextScheduleItemId"); + + b1.ToTable("Playouts"); + + b1.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "NextScheduleItem") + .WithMany() + .HasForeignKey("NextScheduleItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner() + .HasForeignKey("PlayoutId"); + + b1.Navigation("NextScheduleItem"); + }); + + b.Navigation("Anchor"); + + b.Navigation("Channel"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("Items") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaCollection", "MediaCollection") + .WithMany() + .HasForeignKey("MediaCollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("ProgramScheduleAnchors") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany() + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.MediaCollectionEnumeratorState", "EnumeratorState", b1 => + { + b1.Property("PlayoutProgramScheduleAnchorPlayoutId") + .HasColumnType("INTEGER"); + + b1.Property("PlayoutProgramScheduleAnchorProgramScheduleId") + .HasColumnType("INTEGER"); + + b1.Property("PlayoutProgramScheduleAnchorMediaCollectionId") + .HasColumnType("INTEGER"); + + b1.Property("Index") + .HasColumnType("INTEGER"); + + b1.Property("Seed") + .HasColumnType("INTEGER"); + + b1.HasKey("PlayoutProgramScheduleAnchorPlayoutId", "PlayoutProgramScheduleAnchorProgramScheduleId", "PlayoutProgramScheduleAnchorMediaCollectionId"); + + b1.ToTable("PlayoutProgramScheduleItemAnchors"); + + b1.WithOwner() + .HasForeignKey("PlayoutProgramScheduleAnchorPlayoutId", "PlayoutProgramScheduleAnchorProgramScheduleId", "PlayoutProgramScheduleAnchorMediaCollectionId"); + }); + + b.Navigation("EnumeratorState"); + + b.Navigation("MediaCollection"); + + b.Navigation("Playout"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", null) + .WithMany("Connections") + .HasForeignKey("PlexMediaSourceId"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", null) + .WithMany("Libraries") + .HasForeignKey("PlexMediaSourceId"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaCollection", "MediaCollection") + .WithMany() + .HasForeignKey("MediaCollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Items") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaCollection"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("MediaItemSimpleMediaCollection", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithMany() + .HasForeignKey("ItemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.SimpleMediaCollection", null) + .WithMany() + .HasForeignKey("SimpleMediaCollectionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SimpleMediaCollection", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaCollection", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.SimpleMediaCollection", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionMediaCollection", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaCollection", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.TelevisionMediaCollection", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.LocalMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemOne", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Navigation("Items"); + + b.Navigation("ProgramScheduleAnchors"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Navigation("Items"); + + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.Navigation("Connections"); + + b.Navigation("Libraries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/20210215153541_MetadataOptimizations.cs b/ErsatzTV.Infrastructure/Migrations/20210215153541_MetadataOptimizations.cs new file mode 100644 index 000000000..33ff9af95 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210215153541_MetadataOptimizations.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace ErsatzTV.Infrastructure.Migrations +{ + public partial class MetadataOptimizations : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + "Metadata_LastWriteTime", + "MediaItems", + "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + "Metadata_Source", + "MediaItems", + "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + "PosterLastWriteTime", + "MediaItems", + "TEXT", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + "Metadata_LastWriteTime", + "MediaItems"); + + migrationBuilder.DropColumn( + "Metadata_Source", + "MediaItems"); + + migrationBuilder.DropColumn( + "PosterLastWriteTime", + "MediaItems"); + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs index 80651b368..7f072691e 100644 --- a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs +++ b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs @@ -45,6 +45,9 @@ namespace ErsatzTV.Infrastructure.Migrations "ErsatzTV.Core.AggregateModels.MediaItemSummary", b => { + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + b.Property("Poster") .HasColumnType("TEXT"); @@ -221,6 +224,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Property("Poster") .HasColumnType("TEXT"); + b.Property("PosterLastWriteTime") + .HasColumnType("TEXT"); + b.HasKey("Id"); b.HasIndex("MediaSourceId"); @@ -638,6 +644,9 @@ namespace ErsatzTV.Infrastructure.Migrations b1.Property("Height") .HasColumnType("INTEGER"); + b1.Property("LastWriteTime") + .HasColumnType("TEXT"); + b1.Property("MediaType") .HasColumnType("INTEGER"); @@ -650,6 +659,9 @@ namespace ErsatzTV.Infrastructure.Migrations b1.Property("SortTitle") .HasColumnType("TEXT"); + b1.Property("Source") + .HasColumnType("INTEGER"); + b1.Property("Subtitle") .HasColumnType("TEXT");