Browse Source

update metadata and posters when nfo/poster files are updated

pull/25/head
Jason Dove 6 years ago
parent
commit
4dcee3e624
  1. 347
      ErsatzTV.Core.Tests/Metadata/TestMediaScannerTests.cs
  2. 1
      ErsatzTV.Core/Domain/MediaItem.cs
  3. 1
      ErsatzTV.Core/Domain/MediaMetadata.cs
  4. 4
      ErsatzTV.Core/Domain/MetadataSource.cs
  5. 4
      ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs
  6. 3
      ErsatzTV.Core/Metadata/LocalFileSystem.cs
  7. 131
      ErsatzTV.Core/Metadata/LocalMediaScanner.cs
  8. 70
      ErsatzTV.Core/Metadata/TestMediaScanner.cs
  9. 905
      ErsatzTV.Infrastructure/Migrations/20210215153541_MetadataOptimizations.Designer.cs
  10. 44
      ErsatzTV.Infrastructure/Migrations/20210215153541_MetadataOptimizations.cs
  11. 12
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

347
ErsatzTV.Core.Tests/Metadata/TestMediaScannerTests.cs

@ -1,5 +1,7 @@ @@ -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 @@ -10,18 +12,30 @@ namespace ErsatzTV.Core.Tests.Metadata
[TestFixture]
public class TestMediaScannerTests
{
private static TestMediaScanner ScannerForFiles(IEnumerable<string> fileNames)
{
var localFileSystem = new FakeLocalFileSystem(fileNames);
return new TestMediaScanner(localFileSystem);
}
private static TestMediaScanner ScannerForNewFiles(IEnumerable<string> fileNames)
{
IEnumerable<FakeFileSystemEntry> 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<LocalMediaItemScanningPlan> result = _scanner.DetermineActions(
Seq<LocalMediaItemScanningPlan> result = ScannerForFiles(fileNames).DetermineActions(
MediaType.Movie,
Seq<MediaItem>.Empty,
fileNames.ToSeq());
@ -45,7 +59,7 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -45,7 +59,7 @@ namespace ErsatzTV.Core.Tests.Metadata
var nfoFileName = $"/movies/test (2021)/{nfoFile}";
string[] fileNames = { movieFileName, nfoFileName };
Seq<LocalMediaItemScanningPlan> result = _scanner.DetermineActions(
Seq<LocalMediaItemScanningPlan> result = ScannerForFiles(fileNames).DetermineActions(
MediaType.Movie,
Seq<MediaItem>.Empty,
fileNames.ToSeq());
@ -72,7 +86,7 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -72,7 +86,7 @@ namespace ErsatzTV.Core.Tests.Metadata
string[] fileNames = { movieFileName, posterFileName };
Seq<LocalMediaItemScanningPlan> result = _scanner.DetermineActions(
Seq<LocalMediaItemScanningPlan> result = ScannerForFiles(fileNames).DetermineActions(
MediaType.Movie,
Seq<MediaItem>.Empty,
fileNames.ToSeq());
@ -103,7 +117,7 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -103,7 +117,7 @@ namespace ErsatzTV.Core.Tests.Metadata
string[] fileNames = { movieFileName, nfoFileName, posterFileName };
Seq<LocalMediaItemScanningPlan> result = _scanner.DetermineActions(
Seq<LocalMediaItemScanningPlan> result = ScannerForFiles(fileNames).DetermineActions(
MediaType.Movie,
Seq<MediaItem>.Empty,
fileNames.ToSeq());
@ -123,11 +137,8 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -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 @@ -140,7 +151,57 @@ namespace ErsatzTV.Core.Tests.Metadata
var nfoFileName = $"/movies/test (2021)/{nfoFile}";
string[] fileNames = { movieMediaItem.Path, nfoFileName };
Seq<LocalMediaItemScanningPlan> result = _scanner.DetermineActions(
Seq<LocalMediaItemScanningPlan> result = ScannerForFiles(fileNames).DetermineActions(
MediaType.Movie,
Seq.create(movieMediaItem),
fileNames.ToSeq());
result.Count.Should().Be(1);
(Either<string, MediaItem> source, List<ItemScanningPlan> 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<LocalMediaItemScanningPlan> 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<LocalMediaItemScanningPlan> result = ScannerForNewFiles(fileNames).DetermineActions(
MediaType.Movie,
Seq.create(movieMediaItem),
fileNames.ToSeq());
@ -170,7 +231,64 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -170,7 +231,64 @@ namespace ErsatzTV.Core.Tests.Metadata
var posterFileName = $"/movies/test (2021)/{basePosterName}poster.{posterExtension}";
string[] fileNames = { movieMediaItem.Path, posterFileName };
Seq<LocalMediaItemScanningPlan> result = _scanner.DetermineActions(
Seq<LocalMediaItemScanningPlan> result = ScannerForFiles(fileNames).DetermineActions(
MediaType.Movie,
Seq.create(movieMediaItem),
fileNames.ToSeq());
result.Count.Should().Be(1);
(Either<string, MediaItem> source, List<ItemScanningPlan> 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<LocalMediaItemScanningPlan> 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<LocalMediaItemScanningPlan> result = ScannerForNewFiles(fileNames).DetermineActions(
MediaType.Movie,
Seq.create(movieMediaItem),
fileNames.ToSeq());
@ -202,7 +320,7 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -202,7 +320,7 @@ namespace ErsatzTV.Core.Tests.Metadata
var posterFileName = $"/movies/test (2021)/{basePosterName}poster.{posterExtension}";
string[] fileNames = { movieMediaItem.Path, nfoFileName, posterFileName };
Seq<LocalMediaItemScanningPlan> result = _scanner.DetermineActions(
Seq<LocalMediaItemScanningPlan> result = ScannerForFiles(fileNames).DetermineActions(
MediaType.Movie,
Seq.create(movieMediaItem),
fileNames.ToSeq());
@ -221,15 +339,13 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -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<LocalMediaItemScanningPlan> result = _scanner.DetermineActions(
Seq<LocalMediaItemScanningPlan> result = ScannerForFiles(fileNames).DetermineActions(
MediaType.TvShow,
Seq<MediaItem>.Empty,
fileNames.ToSeq());
@ -251,7 +367,7 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -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<LocalMediaItemScanningPlan> result = _scanner.DetermineActions(
Seq<LocalMediaItemScanningPlan> result = ScannerForFiles(fileNames).DetermineActions(
MediaType.TvShow,
Seq<MediaItem>.Empty,
fileNames.ToSeq());
@ -276,7 +392,7 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -276,7 +392,7 @@ namespace ErsatzTV.Core.Tests.Metadata
string[] fileNames = { episodeFileName, posterFileName };
Seq<LocalMediaItemScanningPlan> result = _scanner.DetermineActions(
Seq<LocalMediaItemScanningPlan> result = ScannerForFiles(fileNames).DetermineActions(
MediaType.TvShow,
Seq<MediaItem>.Empty,
fileNames.ToSeq());
@ -303,7 +419,7 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -303,7 +419,7 @@ namespace ErsatzTV.Core.Tests.Metadata
string[] fileNames = { episodeFileName, nfoFileName, posterFileName };
Seq<LocalMediaItemScanningPlan> result = _scanner.DetermineActions(
Seq<LocalMediaItemScanningPlan> result = ScannerForFiles(fileNames).DetermineActions(
MediaType.TvShow,
Seq<MediaItem>.Empty,
fileNames.ToSeq());
@ -319,15 +435,12 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -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 @@ -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<LocalMediaItemScanningPlan> result = _scanner.DetermineActions(
Seq<LocalMediaItemScanningPlan> result = ScannerForFiles(fileNames).DetermineActions(
MediaType.TvShow,
Seq.create(episodeMediaItem),
fileNames.ToSeq());
result.Count.Should().Be(1);
(Either<string, MediaItem> source, List<ItemScanningPlan> 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<LocalMediaItemScanningPlan> 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<LocalMediaItemScanningPlan> result = ScannerForNewFiles(fileNames).DetermineActions(
MediaType.TvShow,
Seq.create(episodeMediaItem),
fileNames.ToSeq());
@ -366,7 +525,60 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -366,7 +525,60 @@ namespace ErsatzTV.Core.Tests.Metadata
var posterFileName = $"/tv/test (2021)/poster.{posterExtension}";
string[] fileNames = { episodeMediaItem.Path, posterFileName };
Seq<LocalMediaItemScanningPlan> result = _scanner.DetermineActions(
Seq<LocalMediaItemScanningPlan> result = ScannerForFiles(fileNames).DetermineActions(
MediaType.TvShow,
Seq.create(episodeMediaItem),
fileNames.ToSeq());
result.Count.Should().Be(1);
(Either<string, MediaItem> source, List<ItemScanningPlan> 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<LocalMediaItemScanningPlan> 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<LocalMediaItemScanningPlan> result = ScannerForNewFiles(fileNames).DetermineActions(
MediaType.TvShow,
Seq.create(episodeMediaItem),
fileNames.ToSeq());
@ -394,7 +606,7 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -394,7 +606,7 @@ namespace ErsatzTV.Core.Tests.Metadata
var posterFileName = $"/tv/test (2021)/poster.{posterExtension}";
string[] fileNames = { episodeMediaItem.Path, nfoFileName, posterFileName };
Seq<LocalMediaItemScanningPlan> result = _scanner.DetermineActions(
Seq<LocalMediaItemScanningPlan> result = ScannerForFiles(fileNames).DetermineActions(
MediaType.TvShow,
Seq.create(episodeMediaItem),
fileNames.ToSeq());
@ -409,5 +621,84 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -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<LocalMediaItemScanningPlan> result = ScannerForFiles(fileNames).DetermineActions(
MediaType.Movie,
Seq<MediaItem>.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<LocalMediaItemScanningPlan> result = ScannerForFiles(fileNames).DetermineActions(
MediaType.Movie,
Seq<MediaItem>.Empty,
fileNames.ToSeq());
result.Count.Should().Be(0);
}
private class FakeLocalFileSystem : ILocalFileSystem
{
private readonly Dictionary<string, DateTime> _files = new();
public FakeLocalFileSystem(IEnumerable<string> files)
{
// default to being old/unmodified
foreach (string file in files)
{
_files.Add(file, DateTime.MinValue);
}
}
public FakeLocalFileSystem(IEnumerable<FakeFileSystemEntry> 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<string> 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);
}
}

1
ErsatzTV.Core/Domain/MediaItem.cs

@ -10,6 +10,7 @@ namespace ErsatzTV.Core.Domain @@ -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<SimpleMediaCollection> SimpleMediaCollections { get; set; }

1
ErsatzTV.Core/Domain/MediaMetadata.cs

@ -6,6 +6,7 @@ namespace ErsatzTV.Core.Domain @@ -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; }

4
ErsatzTV.Core/Domain/MetadataSource.cs

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
{
public enum MetadataSource
{
Sidecar = 1,
Fallback = 2
Fallback = 0,
Sidecar = 1
}
}

4
ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs

@ -1,10 +1,12 @@ @@ -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<string> FindRelevantVideos(LocalMediaSource localMediaSource);
public bool ShouldRefreshMetadata(LocalMediaSource localMediaSource, MediaItem mediaItem);

3
ErsatzTV.Core/Metadata/LocalFileSystem.cs

@ -10,6 +10,9 @@ namespace ErsatzTV.Core.Metadata @@ -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);

131
ErsatzTV.Core/Metadata/LocalMediaScanner.cs

@ -64,60 +64,99 @@ namespace ErsatzTV.Core.Metadata @@ -64,60 +64,99 @@ namespace ErsatzTV.Core.Metadata
Seq<string> allFiles = _localFileSystem.FindRelevantVideos(localMediaSource);
// check if the media item exists
(Seq<string> newFiles, Seq<MediaItem> 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<MediaItem> staleMetadataMediaItems = scanningMode == ScanningMode.RescanAll
? existingMediaItems
: existingMediaItems.Filter(i => _localFileSystem.ShouldRefreshMetadata(localMediaSource, i));
Seq<MediaItem> 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<MediaItem>();
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<MediaItem> stalePosterMediaItems = existingMediaItems
.Filter(_localFileSystem.ShouldRefreshPoster)
.Concat(addedMediaItems);
await RefreshPosterForItems(stalePosterMediaItems);
foreach (int playoutId in modifiedPlayoutIds.Distinct())
{
Option<Playout> 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<string> newFiles, Seq<MediaItem> 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<MediaItem> staleMetadataMediaItems = scanningMode == ScanningMode.RescanAll
// ? existingMediaItems
// : existingMediaItems.Filter(i => _localFileSystem.ShouldRefreshMetadata(localMediaSource, i));
// Seq<MediaItem> modifiedMediaItems = await RefreshMetadataForItems(ffprobePath, staleMetadataMediaItems);
// modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(modifiedMediaItems));
//
// // if new, add and store mtime, refresh metadata
// var addedMediaItems = new List<MediaItem>();
// 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<MediaItem> stalePosterMediaItems = existingMediaItems
// .Filter(_localFileSystem.ShouldRefreshPoster)
// .Concat(addedMediaItems);
// await RefreshPosterForItems(stalePosterMediaItems);
//
// foreach (int playoutId in modifiedPlayoutIds.Distinct())
// {
// Option<Playout> 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<string> FindAllFiles(LocalMediaSource localMediaSource)
{
Seq<string> 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<string> excluded = allDirectories.Filter(path => File.Exists(Path.Combine(path, ".etvignore")));
Seq<string> 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<Seq<MediaItem>> RefreshMetadataForItems(
string ffprobePath,

70
ErsatzTV.Core/Metadata/TestMediaScanner.cs

@ -3,6 +3,7 @@ using System.Collections.Generic; @@ -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 @@ -10,13 +11,10 @@ namespace ErsatzTV.Core.Metadata
{
public class TestMediaScanner
{
// @formatter:off
private static readonly Seq<string> VideoFileExtensions = Seq(
".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".mp4",
".m4p", ".m4v", ".avi", ".wmv", ".mov", ".mkv", ".ts");
// @formatter:on
private static readonly Seq<string> ImageFileExtensions = Seq("jpg", "jpeg", "png", "gif", "tbn");
private readonly ILocalFileSystem _localFileSystem;
public TestMediaScanner(ILocalFileSystem localFileSystem) => _localFileSystem = localFileSystem;
public Seq<LocalMediaItemScanningPlan> DetermineActions(
MediaType mediaType,
@ -24,7 +22,8 @@ namespace ErsatzTV.Core.Metadata @@ -24,7 +22,8 @@ namespace ErsatzTV.Core.Metadata
Seq<string> files)
{
var results = new IntermediateResults();
Seq<string> videoFiles = files.Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f)));
Seq<string> videoFiles = files.Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f)))
.Filter(f => !IsExtra(f));
(Seq<string> newFiles, Seq<MediaItem> existingMediaItems) = videoFiles.Map(
s => mediaItems.Find(i => i.Path == s).ToEither(s))
@ -49,28 +48,43 @@ namespace ErsatzTV.Core.Metadata @@ -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<string> maybeNfoFile = LocateNfoFile(mediaType, files, mediaItem.Path);
maybeNfoFile.IfSome(
nfoFile =>
Option<string> 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<string> maybePoster = LocatePoster(mediaType, files, mediaItem.Path);
maybePoster.IfSome(
posterFile => results.Add(mediaItem, new ItemScanningPlan(posterFile, ScanningAction.Poster)));
}
}
});
Option<string> 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<string> LocateNfoFile(MediaType mediaType, Seq<string> files, string file)
{
switch (mediaType)
@ -126,5 +140,19 @@ namespace ErsatzTV.Core.Metadata @@ -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<string> VideoFileExtensions = Seq(
".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".mp4",
".m4p", ".m4v", ".avi", ".wmv", ".mov", ".mkv", ".ts");
private static readonly Seq<string> ExtraDirectories = Seq(
"behind the scenes","deleted scenes","featurettes",
"interviews","scenes","shorts","trailers","other");
private static readonly Seq<string> ExtraFiles = Seq(
"behindthescenes","deleted","featurette",
"interview","scene","short","trailer","other");
// @formatter:on
}
}

905
ErsatzTV.Infrastructure/Migrations/20210215153541_MetadataOptimizations.Designer.cs generated

@ -0,0 +1,905 @@ @@ -0,0 +1,905 @@
// <auto-generated />
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<int>("Id")
.HasColumnType("INTEGER");
});
modelBuilder.Entity("ErsatzTV.Core.AggregateModels.MediaCollectionSummary", b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER");
b.Property<bool>("IsSimple")
.HasColumnType("INTEGER");
b.Property<int>("ItemCount")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
});
modelBuilder.Entity("ErsatzTV.Core.AggregateModels.MediaItemSummary", b =>
{
b.Property<int>("MediaItemId")
.HasColumnType("INTEGER");
b.Property<string>("Poster")
.HasColumnType("TEXT");
b.Property<string>("SortTitle")
.HasColumnType("TEXT");
b.Property<string>("Subtitle")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("FFmpegProfileId")
.HasColumnType("INTEGER");
b.Property<string>("Logo")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("Number")
.HasColumnType("INTEGER");
b.Property<int>("StreamingMode")
.HasColumnType("INTEGER");
b.Property<Guid>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("ConfigElements");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AudioBitrate")
.HasColumnType("INTEGER");
b.Property<int>("AudioBufferSize")
.HasColumnType("INTEGER");
b.Property<int>("AudioChannels")
.HasColumnType("INTEGER");
b.Property<string>("AudioCodec")
.HasColumnType("TEXT");
b.Property<int>("AudioSampleRate")
.HasColumnType("INTEGER");
b.Property<int>("AudioVolume")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<bool>("NormalizeAudio")
.HasColumnType("INTEGER");
b.Property<bool>("NormalizeAudioCodec")
.HasColumnType("INTEGER");
b.Property<bool>("NormalizeResolution")
.HasColumnType("INTEGER");
b.Property<bool>("NormalizeVideoCodec")
.HasColumnType("INTEGER");
b.Property<int>("ResolutionId")
.HasColumnType("INTEGER");
b.Property<int>("ThreadCount")
.HasColumnType("INTEGER");
b.Property<bool>("Transcode")
.HasColumnType("INTEGER");
b.Property<int>("VideoBitrate")
.HasColumnType("INTEGER");
b.Property<int>("VideoBufferSize")
.HasColumnType("INTEGER");
b.Property<string>("VideoCodec")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ResolutionId");
b.ToTable("FFmpegProfiles");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaCollection", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("MediaCollections");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastWriteTime")
.HasColumnType("TEXT");
b.Property<int>("MediaSourceId")
.HasColumnType("INTEGER");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.Property<string>("Poster")
.HasColumnType("TEXT");
b.Property<DateTime?>("PosterLastWriteTime")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("MediaSourceId");
b.ToTable("MediaItems");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("SourceType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("MediaSources");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ChannelId")
.HasColumnType("INTEGER");
b.Property<int>("ProgramScheduleId")
.HasColumnType("INTEGER");
b.Property<int>("ProgramSchedulePlayoutType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ChannelId");
b.HasIndex("ProgramScheduleId");
b.ToTable("Playouts");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Finish")
.HasColumnType("TEXT");
b.Property<int>("MediaItemId")
.HasColumnType("INTEGER");
b.Property<int>("PlayoutId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Start")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("MediaItemId");
b.HasIndex("PlayoutId");
b.ToTable("PlayoutItems");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b =>
{
b.Property<int>("PlayoutId")
.HasColumnType("INTEGER");
b.Property<int>("ProgramScheduleId")
.HasColumnType("INTEGER");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<int?>("PlexMediaSourceId")
.HasColumnType("INTEGER");
b.Property<string>("Uri")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("PlexMediaSourceId");
b.ToTable("PlexMediaSourceConnections");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceLibrary", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<int>("MediaType")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int?>("PlexMediaSourceId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("PlexMediaSourceId");
b.ToTable("PlexMediaSourceLibraries");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("MediaCollectionPlaybackOrder")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("ProgramSchedules");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Index")
.HasColumnType("INTEGER");
b.Property<int>("MediaCollectionId")
.HasColumnType("INTEGER");
b.Property<int>("ProgramScheduleId")
.HasColumnType("INTEGER");
b.Property<TimeSpan?>("StartTime")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("MediaCollectionId");
b.HasIndex("ProgramScheduleId");
b.ToTable("ProgramScheduleItems");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Resolution", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Height")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("Width")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Resolutions");
});
modelBuilder.Entity("MediaItemSimpleMediaCollection", b =>
{
b.Property<int>("ItemsId")
.HasColumnType("INTEGER");
b.Property<int>("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<int?>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<string>("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<string>("Folder")
.HasColumnType("TEXT");
b.Property<int>("MediaType")
.HasColumnType("INTEGER");
b.ToTable("LocalMediaSources");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaSource");
b.Property<string>("ClientIdentifier")
.HasColumnType("TEXT");
b.Property<string>("ProductVersion")
.HasColumnType("TEXT");
b.ToTable("PlexMediaSources");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
b.Property<bool>("OfflineTail")
.HasColumnType("INTEGER");
b.Property<TimeSpan>("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<int>("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<int>("MediaItemId")
.HasColumnType("INTEGER");
b1.Property<DateTime?>("Aired")
.HasColumnType("TEXT");
b1.Property<string>("AudioCodec")
.HasColumnType("TEXT");
b1.Property<string>("ContentRating")
.HasColumnType("TEXT");
b1.Property<string>("Description")
.HasColumnType("TEXT");
b1.Property<string>("DisplayAspectRatio")
.HasColumnType("TEXT");
b1.Property<TimeSpan>("Duration")
.HasColumnType("TEXT");
b1.Property<int?>("EpisodeNumber")
.HasColumnType("INTEGER");
b1.Property<int>("Height")
.HasColumnType("INTEGER");
b1.Property<DateTime?>("LastWriteTime")
.HasColumnType("TEXT");
b1.Property<int>("MediaType")
.HasColumnType("INTEGER");
b1.Property<string>("SampleAspectRatio")
.HasColumnType("TEXT");
b1.Property<int?>("SeasonNumber")
.HasColumnType("INTEGER");
b1.Property<string>("SortTitle")
.HasColumnType("TEXT");
b1.Property<int>("Source")
.HasColumnType("INTEGER");
b1.Property<string>("Subtitle")
.HasColumnType("TEXT");
b1.Property<string>("Title")
.HasColumnType("TEXT");
b1.Property<string>("VideoCodec")
.HasColumnType("TEXT");
b1.Property<int>("VideoScanType")
.HasColumnType("INTEGER");
b1.Property<int>("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<int>("PlayoutId")
.HasColumnType("INTEGER");
b1.Property<int>("NextScheduleItemId")
.HasColumnType("INTEGER");
b1.Property<DateTimeOffset>("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<int>("PlayoutProgramScheduleAnchorPlayoutId")
.HasColumnType("INTEGER");
b1.Property<int>("PlayoutProgramScheduleAnchorProgramScheduleId")
.HasColumnType("INTEGER");
b1.Property<int>("PlayoutProgramScheduleAnchorMediaCollectionId")
.HasColumnType("INTEGER");
b1.Property<int>("Index")
.HasColumnType("INTEGER");
b1.Property<int>("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
}
}
}

44
ErsatzTV.Infrastructure/Migrations/20210215153541_MetadataOptimizations.cs

@ -0,0 +1,44 @@ @@ -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<DateTime>(
"Metadata_LastWriteTime",
"MediaItems",
"TEXT",
nullable: true);
migrationBuilder.AddColumn<int>(
"Metadata_Source",
"MediaItems",
"INTEGER",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
"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");
}
}
}

12
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -45,6 +45,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -45,6 +45,9 @@ namespace ErsatzTV.Infrastructure.Migrations
"ErsatzTV.Core.AggregateModels.MediaItemSummary",
b =>
{
b.Property<int>("MediaItemId")
.HasColumnType("INTEGER");
b.Property<string>("Poster")
.HasColumnType("TEXT");
@ -221,6 +224,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -221,6 +224,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<string>("Poster")
.HasColumnType("TEXT");
b.Property<DateTime?>("PosterLastWriteTime")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("MediaSourceId");
@ -638,6 +644,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -638,6 +644,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b1.Property<int>("Height")
.HasColumnType("INTEGER");
b1.Property<DateTime?>("LastWriteTime")
.HasColumnType("TEXT");
b1.Property<int>("MediaType")
.HasColumnType("INTEGER");
@ -650,6 +659,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -650,6 +659,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b1.Property<string>("SortTitle")
.HasColumnType("TEXT");
b1.Property<int>("Source")
.HasColumnType("INTEGER");
b1.Property<string>("Subtitle")
.HasColumnType("TEXT");

Loading…
Cancel
Save