diff --git a/ErsatzTV.Application/MediaItems/Mapper.cs b/ErsatzTV.Application/MediaItems/Mapper.cs index edd133867..4df219464 100644 --- a/ErsatzTV.Application/MediaItems/Mapper.cs +++ b/ErsatzTV.Application/MediaItems/Mapper.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using ErsatzTV.Core.Domain; namespace ErsatzTV.Application.MediaItems @@ -7,10 +6,7 @@ namespace ErsatzTV.Application.MediaItems internal static class Mapper { internal static MediaItemViewModel ProjectToViewModel(MediaItem mediaItem) => - new( - mediaItem.Id, - mediaItem.LibraryPathId, - mediaItem.Path); + new(mediaItem.Id, mediaItem.LibraryPathId); internal static MediaItemSearchResultViewModel ProjectToSearchViewModel(MediaItem mediaItem) => mediaItem switch @@ -42,15 +38,23 @@ namespace ErsatzTV.Application.MediaItems { Episode e => e.EpisodeMetadata.HeadOrNone() .Map(em => $"{em.Title} - s{e.Season.SeasonNumber:00}e{e.EpisodeNumber:00}") - .IfNone(Path.GetFileName(e.Path)), - Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone(Path.GetFileName(m.Path)), + .IfNone("[unknown episode]"), + Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]"), _ => string.Empty }; - private static string GetDisplayDuration(MediaItem mediaItem) => - string.Format( - mediaItem.Statistics.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}", - mediaItem.Statistics.Duration); + private static string GetDisplayDuration(MediaItem mediaItem) + { + MediaVersion version = mediaItem switch + { + Movie m => m.MediaVersions.Head(), + Episode e => e.MediaVersions.Head() + }; + + return string.Format( + version.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}", + version.Duration); + } // TODO: fix this when search is reimplemented private static string GetLibraryName(MediaItem item) => diff --git a/ErsatzTV.Application/MediaItems/MediaItemViewModel.cs b/ErsatzTV.Application/MediaItems/MediaItemViewModel.cs index ce62ab340..b2cc42d23 100644 --- a/ErsatzTV.Application/MediaItems/MediaItemViewModel.cs +++ b/ErsatzTV.Application/MediaItems/MediaItemViewModel.cs @@ -1,4 +1,4 @@ namespace ErsatzTV.Application.MediaItems { - public record MediaItemViewModel(int Id, int LibraryPathId, string Path); + public record MediaItemViewModel(int Id, int LibraryPathId); } diff --git a/ErsatzTV.Application/Playouts/Mapper.cs b/ErsatzTV.Application/Playouts/Mapper.cs index 01c2452da..bba6cf488 100644 --- a/ErsatzTV.Application/Playouts/Mapper.cs +++ b/ErsatzTV.Application/Playouts/Mapper.cs @@ -1,5 +1,4 @@ -using System.IO; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; namespace ErsatzTV.Application.Playouts { @@ -26,14 +25,22 @@ namespace ErsatzTV.Application.Playouts { Episode e => e.EpisodeMetadata.HeadOrNone() .Map(em => $"{em.Title} - s{e.Season.SeasonNumber:00}e{e.EpisodeNumber:00}") - .IfNone(Path.GetFileName(e.Path)), - Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone(Path.GetFileName(m.Path)), + .IfNone("[unknown episode]"), + Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]"), _ => string.Empty }; - private static string GetDisplayDuration(MediaItem mediaItem) => - string.Format( - mediaItem.Statistics.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}", - mediaItem.Statistics.Duration); + private static string GetDisplayDuration(MediaItem mediaItem) + { + MediaVersion version = mediaItem switch + { + Movie m => m.MediaVersions.Head(), + Episode e => e.MediaVersions.Head() + }; + + return string.Format( + version.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}", + version.Duration); + } } } diff --git a/ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsServiceTests.cs b/ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsServiceTests.cs index 101e6f6d0..5237e37da 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsServiceTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsServiceTests.cs @@ -15,15 +15,6 @@ namespace ErsatzTV.Core.Tests.FFmpeg public CalculateSettings() => _calculator = new FFmpegPlaybackSettingsCalculator(); - private static PlayoutItem EmptyPlayoutItem() => - new() - { - MediaItem = new MediaItem - { - Statistics = new MediaItemStatistics() - } - }; - [Test] public void Should_UseSpecifiedThreadCount_ForTransportStream() { @@ -32,7 +23,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - EmptyPlayoutItem(), + new MediaVersion(), + DateTimeOffset.Now, DateTimeOffset.Now); actual.ThreadCount.Should().Be(7); @@ -46,7 +38,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.HttpLiveStreaming, ffmpegProfile, - EmptyPlayoutItem(), + new MediaVersion(), + DateTimeOffset.Now, DateTimeOffset.Now); actual.ThreadCount.Should().Be(7); @@ -60,7 +53,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - EmptyPlayoutItem(), + new MediaVersion(), + DateTimeOffset.Now, DateTimeOffset.Now); string[] expected = { "+genpts", "+discardcorrupt", "+igndts" }; @@ -76,7 +70,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.HttpLiveStreaming, ffmpegProfile, - EmptyPlayoutItem(), + new MediaVersion(), + DateTimeOffset.Now, DateTimeOffset.Now); string[] expected = { "+genpts", "+discardcorrupt", "+igndts" }; @@ -92,7 +87,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - EmptyPlayoutItem(), + new MediaVersion(), + DateTimeOffset.Now, DateTimeOffset.Now); actual.RealtimeOutput.Should().BeTrue(); @@ -106,7 +102,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.HttpLiveStreaming, ffmpegProfile, - EmptyPlayoutItem(), + new MediaVersion(), + DateTimeOffset.Now, DateTimeOffset.Now); actual.RealtimeOutput.Should().BeTrue(); @@ -118,13 +115,12 @@ namespace ErsatzTV.Core.Tests.FFmpeg DateTimeOffset now = DateTimeOffset.Now; FFmpegProfile ffmpegProfile = TestProfile(); - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.Start = now.UtcDateTime; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + new MediaVersion(), + now, now.AddMinutes(5)); actual.StreamSeek.IsSome.Should().BeTrue(); @@ -137,13 +133,12 @@ namespace ErsatzTV.Core.Tests.FFmpeg DateTimeOffset now = DateTimeOffset.Now; FFmpegProfile ffmpegProfile = TestProfile(); - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.Start = now.UtcDateTime; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.HttpLiveStreaming, ffmpegProfile, - playoutItem, + new MediaVersion(), + now, now.AddMinutes(5)); actual.StreamSeek.IsSome.Should().BeTrue(); @@ -154,12 +149,12 @@ namespace ErsatzTV.Core.Tests.FFmpeg public void ShouldNot_SetScaledSize_When_NotNormalizingResolution_ForTransportStream() { FFmpegProfile ffmpegProfile = TestProfile() with { NormalizeResolution = false }; - PlayoutItem playoutItem = EmptyPlayoutItem(); FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + new MediaVersion(), + DateTimeOffset.Now, DateTimeOffset.Now); actual.ScaledSize.IsNone.Should().BeTrue(); @@ -174,15 +169,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg Resolution = new Resolution { Width = 1920, Height = 1080 } }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.Width = 1920; - playoutItem.MediaItem.Statistics.Height = 1080; - playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic + // not anamorphic + var version = new MediaVersion { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.ScaledSize.IsNone.Should().BeTrue(); @@ -197,15 +191,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg Resolution = new Resolution { Width = 1920, Height = 1080 } }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.Width = 1918; - playoutItem.MediaItem.Statistics.Height = 1080; - playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic + // not anamorphic + var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.ScaledSize.IsNone.Should().BeTrue(); @@ -220,15 +213,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg Resolution = new Resolution { Width = 1920, Height = 1080 } }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.Width = 1920; - playoutItem.MediaItem.Statistics.Height = 1080; - playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic + // not anamorphic + var version = new MediaVersion { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.ScaledSize.IsNone.Should().BeTrue(); @@ -244,15 +236,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg Resolution = new Resolution { Width = 1920, Height = 1080 } }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.Width = 1918; - playoutItem.MediaItem.Statistics.Height = 1080; - playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic + // not anamorphic + var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.ScaledSize.IsNone.Should().BeTrue(); @@ -268,15 +259,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg Resolution = new Resolution { Width = 1920, Height = 1080 } }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.Width = 1918; - playoutItem.MediaItem.Statistics.Height = 1080; - playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic + // not anamorphic + var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.HttpLiveStreaming, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.ScaledSize.IsNone.Should().BeTrue(); @@ -294,15 +284,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg VideoCodec = "testCodec" }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.Width = 1918; - playoutItem.MediaItem.Statistics.Height = 1080; - playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic + // not anamorphic + var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.ScaledSize.IsNone.Should().BeTrue(); @@ -322,16 +311,15 @@ namespace ErsatzTV.Core.Tests.FFmpeg VideoCodec = "testCodec" }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.Width = 1920; - playoutItem.MediaItem.Statistics.Height = 1080; - playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic - playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video"; + // not anamorphic + var version = new MediaVersion + { Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "mpeg2video" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.ScaledSize.IsNone.Should().BeTrue(); @@ -351,16 +339,15 @@ namespace ErsatzTV.Core.Tests.FFmpeg VideoCodec = "testCodec" }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.Width = 1920; - playoutItem.MediaItem.Statistics.Height = 1080; - playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic - playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video"; + // not anamorphic + var version = new MediaVersion + { Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "mpeg2video" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.HttpLiveStreaming, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.ScaledSize.IsNone.Should().BeTrue(); @@ -379,16 +366,15 @@ namespace ErsatzTV.Core.Tests.FFmpeg VideoCodec = "libx264" }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.Width = 1920; - playoutItem.MediaItem.Statistics.Height = 1080; - playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic - playoutItem.MediaItem.Statistics.VideoCodec = "libx264"; + // not anamorphic + var version = new MediaVersion + { Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "libx264" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.ScaledSize.IsNone.Should().BeTrue(); @@ -408,16 +394,15 @@ namespace ErsatzTV.Core.Tests.FFmpeg VideoCodec = "libx264" }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.Width = 1920; - playoutItem.MediaItem.Statistics.Height = 1080; - playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic - playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video"; + // not anamorphic + var version = new MediaVersion + { Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "mpeg2video" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.ScaledSize.IsNone.Should().BeTrue(); @@ -436,15 +421,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg VideoBitrate = 2525 }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.Width = 1918; - playoutItem.MediaItem.Statistics.Height = 1080; - playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic + // not anamorphic + var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.ScaledSize.IsNone.Should().BeTrue(); @@ -463,16 +447,15 @@ namespace ErsatzTV.Core.Tests.FFmpeg VideoBitrate = 2525 }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.Width = 1920; - playoutItem.MediaItem.Statistics.Height = 1080; - playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic - playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video"; + // not anamorphic + var version = new MediaVersion + { Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "mpeg2video" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.ScaledSize.IsNone.Should().BeTrue(); @@ -491,15 +474,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg VideoBufferSize = 2525 }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.Width = 1918; - playoutItem.MediaItem.Statistics.Height = 1080; - playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic + // not anamorphic + var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.ScaledSize.IsNone.Should().BeTrue(); @@ -519,16 +501,15 @@ namespace ErsatzTV.Core.Tests.FFmpeg VideoBufferSize = 2525 }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.Width = 1920; - playoutItem.MediaItem.Statistics.Height = 1080; - playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic - playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video"; + // not anamorphic + var version = new MediaVersion + { Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "mpeg2video" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.ScaledSize.IsNone.Should().BeTrue(); @@ -545,13 +526,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg AudioCodec = "aac" }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.AudioCodec = "aac"; + var version = new MediaVersion { AudioCodec = "aac" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.AudioCodec.Should().Be("copy"); @@ -566,13 +547,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg AudioCodec = "aac" }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.AudioCodec = "ac3"; + var version = new MediaVersion { AudioCodec = "ac3" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.AudioCodec.Should().Be("copy"); @@ -587,13 +568,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg AudioCodec = "aac" }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.AudioCodec = "ac3"; + var version = new MediaVersion { AudioCodec = "ac3" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.AudioCodec.Should().Be("aac"); @@ -608,13 +589,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg AudioCodec = "aac" }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.AudioCodec = "ac3"; + var version = new MediaVersion { AudioCodec = "ac3" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.HttpLiveStreaming, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.AudioCodec.Should().Be("copy"); @@ -629,13 +610,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg AudioBitrate = 2424 }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.AudioCodec = "ac3"; + var version = new MediaVersion { AudioCodec = "ac3" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.AudioBitrate.IfNone(0).Should().Be(2424); @@ -650,13 +631,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg AudioBufferSize = 2424 }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.AudioCodec = "ac3"; + var version = new MediaVersion { AudioCodec = "ac3" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.AudioBufferSize.IfNone(0).Should().Be(2424); @@ -673,13 +654,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg AudioChannels = 6 }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.AudioCodec = "ac3"; + var version = new MediaVersion { AudioCodec = "ac3" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.AudioChannels.IsNone.Should().BeTrue(); @@ -696,13 +677,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg AudioSampleRate = 48 }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.AudioCodec = "ac3"; + var version = new MediaVersion { AudioCodec = "ac3" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.AudioSampleRate.IsNone.Should().BeTrue(); @@ -718,13 +699,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg AudioChannels = 6 }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.AudioCodec = "ac3"; + var version = new MediaVersion { AudioCodec = "ac3" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.AudioChannels.IfNone(0).Should().Be(6); @@ -740,13 +721,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg AudioSampleRate = 48 }; - PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Statistics.AudioCodec = "ac3"; + var version = new MediaVersion { AudioCodec = "ac3" }; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, ffmpegProfile, - playoutItem, + version, + DateTimeOffset.Now, DateTimeOffset.Now); actual.AudioSampleRate.IfNone(0).Should().Be(48); diff --git a/ErsatzTV.Core.Tests/Fakes/FakeMovieWithPath.cs b/ErsatzTV.Core.Tests/Fakes/FakeMovieWithPath.cs new file mode 100644 index 000000000..f6d096eed --- /dev/null +++ b/ErsatzTV.Core.Tests/Fakes/FakeMovieWithPath.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using ErsatzTV.Core.Domain; + +namespace ErsatzTV.Core.Tests.Fakes +{ + public class FakeMovieWithPath : Movie + { + public FakeMovieWithPath(string path) + { + Path = path; + + MediaVersions = new List + { + new() + { + MediaFiles = new List + { + new() { Path = path } + } + } + }; + } + + public string Path { get; } + } +} diff --git a/ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs b/ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs index 1359cb232..dc036508c 100644 --- a/ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs +++ b/ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Metadata; using FluentAssertions; @@ -39,7 +40,20 @@ namespace ErsatzTV.Core.Tests.Metadata public void GetFallbackMetadata_ShouldHandleVariousFormats(string path, string title, int season, int episode) { (EpisodeMetadata metadata, int episodeNumber) = FallbackMetadataProvider.GetFallbackMetadata( - new Episode { Path = path, LibraryPath = new LibraryPath() }); + new Episode + { + LibraryPath = new LibraryPath(), + MediaVersions = new List + { + new() + { + MediaFiles = new List + { + new() { Path = path } + } + } + } + }); metadata.Title.Should().Be(title); // TODO: how can we test season number? do we need to? diff --git a/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs b/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs index 42d043e11..99d327649 100644 --- a/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs +++ b/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs @@ -43,7 +43,7 @@ namespace ErsatzTV.Core.Tests.Metadata _movieRepository = new Mock(); _movieRepository.Setup(x => x.GetOrAdd(It.IsAny(), It.IsAny())) .Returns( - (LibraryPath _, string path) => Right(new Movie { Path = path }).AsTask()); + (LibraryPath _, string path) => Right(new FakeMovieWithPath(path)).AsTask()); _localStatisticsProvider = new Mock(); _localMetadataProvider = new Mock(); @@ -92,7 +92,7 @@ namespace ErsatzTV.Core.Tests.Metadata Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); MovieFolderScanner service = GetService( - new FakeFileEntry(moviePath) + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now } ); var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot }; @@ -104,11 +104,11 @@ namespace ErsatzTV.Core.Tests.Metadata _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); _localStatisticsProvider.Verify( - x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), + x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), Times.Once); _localMetadataProvider.Verify( - x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), + x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), Times.Once); } @@ -124,7 +124,7 @@ namespace ErsatzTV.Core.Tests.Metadata string metadataPath = Path.ChangeExtension(moviePath, "nfo"); MovieFolderScanner service = GetService( - new FakeFileEntry(moviePath), + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, new FakeFileEntry(metadataPath) ); var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot }; @@ -137,11 +137,11 @@ namespace ErsatzTV.Core.Tests.Metadata _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); _localStatisticsProvider.Verify( - x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), + x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), Times.Once); _localMetadataProvider.Verify( - x => x.RefreshSidecarMetadata(It.Is(i => i.Path == moviePath), metadataPath), + x => x.RefreshSidecarMetadata(It.Is(i => i.Path == moviePath), metadataPath), Times.Once); } @@ -157,7 +157,7 @@ namespace ErsatzTV.Core.Tests.Metadata string metadataPath = Path.Combine(Path.GetDirectoryName(moviePath) ?? string.Empty, "movie.nfo"); MovieFolderScanner service = GetService( - new FakeFileEntry(moviePath), + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, new FakeFileEntry(metadataPath) ); var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot }; @@ -170,11 +170,11 @@ namespace ErsatzTV.Core.Tests.Metadata _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); _localStatisticsProvider.Verify( - x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), + x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), Times.Once); _localMetadataProvider.Verify( - x => x.RefreshSidecarMetadata(It.Is(i => i.Path == moviePath), metadataPath), + x => x.RefreshSidecarMetadata(It.Is(i => i.Path == moviePath), metadataPath), Times.Once); } @@ -194,7 +194,7 @@ namespace ErsatzTV.Core.Tests.Metadata $"poster.{imageExtension}"); MovieFolderScanner service = GetService( - new FakeFileEntry(moviePath), + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, new FakeFileEntry(posterPath) { LastWriteTime = DateTime.Now } ); var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot }; @@ -207,11 +207,11 @@ namespace ErsatzTV.Core.Tests.Metadata _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); _localStatisticsProvider.Verify( - x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), + x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), Times.Once); _localMetadataProvider.Verify( - x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), + x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), Times.Once); _imageCache.Verify( @@ -235,7 +235,7 @@ namespace ErsatzTV.Core.Tests.Metadata $"Movie (2020)-poster.{imageExtension}"); MovieFolderScanner service = GetService( - new FakeFileEntry(moviePath), + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, new FakeFileEntry(posterPath) { LastWriteTime = DateTime.Now } ); var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot }; @@ -248,11 +248,11 @@ namespace ErsatzTV.Core.Tests.Metadata _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); _localStatisticsProvider.Verify( - x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), + x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), Times.Once); _localMetadataProvider.Verify( - x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), + x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), Times.Once); _imageCache.Verify( @@ -272,7 +272,7 @@ namespace ErsatzTV.Core.Tests.Metadata Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); MovieFolderScanner service = GetService( - new FakeFileEntry(moviePath), + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, new FakeFileEntry( Path.Combine( Path.GetDirectoryName(moviePath) ?? string.Empty, @@ -288,11 +288,11 @@ namespace ErsatzTV.Core.Tests.Metadata _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); _localStatisticsProvider.Verify( - x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), + x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), Times.Once); _localMetadataProvider.Verify( - x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), + x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), Times.Once); } @@ -308,7 +308,7 @@ namespace ErsatzTV.Core.Tests.Metadata Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); MovieFolderScanner service = GetService( - new FakeFileEntry(moviePath), + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, new FakeFileEntry( Path.Combine( Path.GetDirectoryName(moviePath) ?? string.Empty, @@ -324,11 +324,11 @@ namespace ErsatzTV.Core.Tests.Metadata _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); _localStatisticsProvider.Verify( - x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), + x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), Times.Once); _localMetadataProvider.Verify( - x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), + x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), Times.Once); } @@ -342,7 +342,7 @@ namespace ErsatzTV.Core.Tests.Metadata Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); MovieFolderScanner service = GetService( - new FakeFileEntry(moviePath) + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now } ); var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot }; @@ -354,11 +354,11 @@ namespace ErsatzTV.Core.Tests.Metadata _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); _localStatisticsProvider.Verify( - x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), + x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), Times.Once); _localMetadataProvider.Verify( - x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), + x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), Times.Once); } diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs index 332382a3d..34984daf2 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs @@ -612,7 +612,10 @@ namespace ErsatzTV.Core.Tests.Scheduling { Id = id, MovieMetadata = new List { new() { ReleaseDate = aired } }, - Statistics = new MediaItemStatistics { Duration = duration } + MediaVersions = new List + { + new() { Duration = duration } + } }; private TestData TestDataFloodForItems(List mediaItems, PlaybackOrder playbackOrder) diff --git a/ErsatzTV.Core/Domain/MediaItem/MediaItem.cs b/ErsatzTV.Core/Domain/MediaItem/MediaItem.cs index c37c7732b..f0432c7c4 100644 --- a/ErsatzTV.Core/Domain/MediaItem/MediaItem.cs +++ b/ErsatzTV.Core/Domain/MediaItem/MediaItem.cs @@ -6,7 +6,8 @@ namespace ErsatzTV.Core.Domain public class MediaItem { public int Id { get; set; } - public MediaItemStatistics Statistics { get; set; } + + // public MediaItemStatistics Statistics { get; set; } public DateTime? LastWriteTime { get; set; } public int LibraryPathId { get; set; } @@ -18,7 +19,8 @@ namespace ErsatzTV.Core.Domain public int TelevisionEpisodeId { get; set; } public List Collections { get; set; } + public List CollectionItems { get; set; } - public string Path { get; set; } + // public string Path { get; set; } } } diff --git a/ErsatzTV.Core/Domain/MediaItemStatistics.cs b/ErsatzTV.Core/Domain/MediaItem/MediaItemStatistics.cs similarity index 90% rename from ErsatzTV.Core/Domain/MediaItemStatistics.cs rename to ErsatzTV.Core/Domain/MediaItem/MediaItemStatistics.cs index 56b0f59ce..7f7b4d7a2 100644 --- a/ErsatzTV.Core/Domain/MediaItemStatistics.cs +++ b/ErsatzTV.Core/Domain/MediaItem/MediaItemStatistics.cs @@ -11,7 +11,7 @@ namespace ErsatzTV.Core.Domain public string DisplayAspectRatio { get; set; } public string VideoCodec { get; set; } public string AudioCodec { get; set; } - public VideoScanType VideoScanType { get; set; } + public VideoScanKind VideoScanType { get; set; } public int Width { get; set; } public int Height { get; set; } } diff --git a/ErsatzTV.Core/Domain/MediaItem/MediaVersion.cs b/ErsatzTV.Core/Domain/MediaItem/MediaVersion.cs index e7151baed..8373841df 100644 --- a/ErsatzTV.Core/Domain/MediaItem/MediaVersion.cs +++ b/ErsatzTV.Core/Domain/MediaItem/MediaVersion.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; +using ErsatzTV.Core.Interfaces.FFmpeg; namespace ErsatzTV.Core.Domain { - public class MediaVersion + public class MediaVersion : IDisplaySize { public int Id { get; set; } public string Name { get; set; } @@ -15,7 +16,9 @@ namespace ErsatzTV.Core.Domain public string DisplayAspectRatio { get; set; } public string VideoCodec { get; set; } public string AudioCodec { get; set; } - public bool IsInterlaced { get; set; } + public VideoScanKind VideoScanKind { get; set; } + public DateTime DateAdded { get; set; } + public DateTime DateUpdated { get; set; } public int Width { get; set; } public int Height { get; set; } } diff --git a/ErsatzTV.Core/Domain/VideoScanType.cs b/ErsatzTV.Core/Domain/MediaItem/VideoScanKind.cs similarity index 79% rename from ErsatzTV.Core/Domain/VideoScanType.cs rename to ErsatzTV.Core/Domain/MediaItem/VideoScanKind.cs index 443ce6f0a..812061ac3 100644 --- a/ErsatzTV.Core/Domain/VideoScanType.cs +++ b/ErsatzTV.Core/Domain/MediaItem/VideoScanKind.cs @@ -1,6 +1,6 @@ namespace ErsatzTV.Core.Domain { - public enum VideoScanType + public enum VideoScanKind { Unknown = 0, Progressive = 1, diff --git a/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs b/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs index 479b906f9..d9e057788 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs @@ -44,7 +44,8 @@ namespace ErsatzTV.Core.FFmpeg public FFmpegPlaybackSettings CalculateSettings( StreamingMode streamingMode, FFmpegProfile ffmpegProfile, - PlayoutItem playoutItem, + MediaVersion version, + DateTimeOffset start, DateTimeOffset now) { var result = new FFmpegPlaybackSettings @@ -53,9 +54,9 @@ namespace ErsatzTV.Core.FFmpeg FormatFlags = CommonFormatFlags }; - if (now != playoutItem.Start) + if (now != start) { - result.StreamSeek = now - playoutItem.Start; + result.StreamSeek = now - start; } switch (streamingMode) @@ -66,24 +67,23 @@ namespace ErsatzTV.Core.FFmpeg result.Deinterlace = false; break; case StreamingMode.TransportStream: - if (NeedToScale(ffmpegProfile, playoutItem.MediaItem.Statistics)) + if (NeedToScale(ffmpegProfile, version)) { - IDisplaySize scaledSize = CalculateScaledSize(ffmpegProfile, playoutItem.MediaItem.Statistics); - if (!scaledSize.IsSameSizeAs(playoutItem.MediaItem.Statistics)) + IDisplaySize scaledSize = CalculateScaledSize(ffmpegProfile, version); + if (!scaledSize.IsSameSizeAs(version)) { - result.ScaledSize = Some( - CalculateScaledSize(ffmpegProfile, playoutItem.MediaItem.Statistics)); + result.ScaledSize = Some(CalculateScaledSize(ffmpegProfile, version)); } } - IDisplaySize sizeAfterScaling = result.ScaledSize.IfNone(playoutItem.MediaItem.Statistics); + IDisplaySize sizeAfterScaling = result.ScaledSize.IfNone(version); if (!sizeAfterScaling.IsSameSizeAs(ffmpegProfile.Resolution)) { result.PadToDesiredResolution = true; } if (result.ScaledSize.IsSome || result.PadToDesiredResolution || - NeedToNormalizeVideoCodec(ffmpegProfile, playoutItem.MediaItem.Statistics)) + NeedToNormalizeVideoCodec(ffmpegProfile, version)) { result.VideoCodec = ffmpegProfile.VideoCodec; result.VideoBitrate = ffmpegProfile.VideoBitrate; @@ -94,7 +94,7 @@ namespace ErsatzTV.Core.FFmpeg result.VideoCodec = "copy"; } - if (NeedToNormalizeAudioCodec(ffmpegProfile, playoutItem.MediaItem.Statistics)) + if (NeedToNormalizeAudioCodec(ffmpegProfile, version)) { result.AudioCodec = ffmpegProfile.AudioCodec; result.AudioBitrate = ffmpegProfile.AudioBitrate; @@ -104,7 +104,7 @@ namespace ErsatzTV.Core.FFmpeg { result.AudioChannels = ffmpegProfile.AudioChannels; result.AudioSampleRate = ffmpegProfile.AudioSampleRate; - result.AudioDuration = playoutItem.MediaItem.Statistics.Duration; + result.AudioDuration = version.Duration; } } else @@ -112,7 +112,7 @@ namespace ErsatzTV.Core.FFmpeg result.AudioCodec = "copy"; } - if (playoutItem.MediaItem.Statistics.VideoScanType == VideoScanType.Interlaced) + if (version.VideoScanKind == VideoScanKind.Interlaced) { result.Deinterlace = true; } @@ -132,35 +132,35 @@ namespace ErsatzTV.Core.FFmpeg AudioCodec = ffmpegProfile.AudioCodec }; - private static bool NeedToScale(FFmpegProfile ffmpegProfile, MediaItemStatistics statistics) => + private static bool NeedToScale(FFmpegProfile ffmpegProfile, MediaVersion version) => ffmpegProfile.NormalizeResolution && - IsIncorrectSize(ffmpegProfile.Resolution, statistics) || - IsTooLarge(ffmpegProfile.Resolution, statistics) || - IsOddSize(statistics); + IsIncorrectSize(ffmpegProfile.Resolution, version) || + IsTooLarge(ffmpegProfile.Resolution, version) || + IsOddSize(version); - private static bool IsIncorrectSize(IDisplaySize desiredResolution, MediaItemStatistics statistics) => - IsAnamorphic(statistics) || - statistics.Width != desiredResolution.Width || - statistics.Height != desiredResolution.Height; + private static bool IsIncorrectSize(IDisplaySize desiredResolution, MediaVersion version) => + IsAnamorphic(version) || + version.Width != desiredResolution.Width || + version.Height != desiredResolution.Height; - private static bool IsTooLarge(IDisplaySize desiredResolution, IDisplaySize mediaSize) => - mediaSize.Height > desiredResolution.Height || - mediaSize.Width > desiredResolution.Width; + private static bool IsTooLarge(IDisplaySize desiredResolution, IDisplaySize displaySize) => + displaySize.Height > desiredResolution.Height || + displaySize.Width > desiredResolution.Width; - private static bool IsOddSize(IDisplaySize displaySize) => - displaySize.Height % 2 == 1 || displaySize.Width % 2 == 1; + private static bool IsOddSize(MediaVersion version) => + version.Height % 2 == 1 || version.Width % 2 == 1; - private static bool NeedToNormalizeVideoCodec(FFmpegProfile ffmpegProfile, MediaItemStatistics statistics) => - ffmpegProfile.NormalizeVideoCodec && ffmpegProfile.VideoCodec != statistics.VideoCodec; + private static bool NeedToNormalizeVideoCodec(FFmpegProfile ffmpegProfile, MediaVersion version) => + ffmpegProfile.NormalizeVideoCodec && ffmpegProfile.VideoCodec != version.VideoCodec; - private static bool NeedToNormalizeAudioCodec(FFmpegProfile ffmpegProfile, MediaItemStatistics statistics) => - ffmpegProfile.NormalizeAudioCodec && ffmpegProfile.AudioCodec != statistics.AudioCodec; + private static bool NeedToNormalizeAudioCodec(FFmpegProfile ffmpegProfile, MediaVersion version) => + ffmpegProfile.NormalizeAudioCodec && ffmpegProfile.AudioCodec != version.AudioCodec; - private static IDisplaySize CalculateScaledSize(FFmpegProfile ffmpegProfile, MediaItemStatistics statistics) + private static IDisplaySize CalculateScaledSize(FFmpegProfile ffmpegProfile, MediaVersion version) { - IDisplaySize sarSize = SARSize(statistics); - int p = statistics.Width * sarSize.Width; - int q = statistics.Height * sarSize.Height; + IDisplaySize sarSize = SARSize(version); + int p = version.Width * sarSize.Width; + int q = version.Height * sarSize.Height; int g = Gcd(q, p); p = p / g; q = q / g; @@ -194,29 +194,29 @@ namespace ErsatzTV.Core.FFmpeg return a | b; } - private static bool IsAnamorphic(MediaItemStatistics statistics) + private static bool IsAnamorphic(MediaVersion version) { - if (statistics.SampleAspectRatio == "1:1") + if (version.SampleAspectRatio == "1:1") { return false; } - if (statistics.SampleAspectRatio != "0:1") + if (version.SampleAspectRatio != "0:1") { return true; } - if (statistics.DisplayAspectRatio == "0:1") + if (version.DisplayAspectRatio == "0:1") { return false; } - return statistics.DisplayAspectRatio != $"{statistics.Width}:{statistics.Height}"; + return version.DisplayAspectRatio != $"{version.Width}:{version.Height}"; } - private static IDisplaySize SARSize(MediaItemStatistics statistics) + private static IDisplaySize SARSize(MediaVersion version) { - string[] split = statistics.SampleAspectRatio.Split(":"); + string[] split = version.SampleAspectRatio.Split(":"); return new DisplaySize(int.Parse(split[0]), int.Parse(split[1])); } } diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs index 99fd9ad87..19d9b0871 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs @@ -18,19 +18,28 @@ namespace ErsatzTV.Core.FFmpeg PlayoutItem item, DateTimeOffset now) { + MediaVersion version = item.MediaItem switch + { + Movie m => m.MediaVersions.Head(), + Episode e => e.MediaVersions.Head() + }; + FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings( channel.StreamingMode, channel.FFmpegProfile, - item, + version, + item.StartOffset, now); + MediaFile file = version.MediaFiles.Head(); + FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath) .WithThreads(playbackSettings.ThreadCount) .WithQuiet() .WithFormatFlags(playbackSettings.FormatFlags) .WithRealtimeOutput(playbackSettings.RealtimeOutput) .WithSeek(playbackSettings.StreamSeek) - .WithInput(item.MediaItem.Path); + .WithInput(file.Path); playbackSettings.ScaledSize.Match( scaledSize => @@ -76,7 +85,7 @@ namespace ErsatzTV.Core.FFmpeg return builder.WithPlaybackArgs(playbackSettings) .WithMetadata(channel) .WithFormat("mpegts") - .WithDuration(item.Start + item.MediaItem.Statistics.Duration - now) + .WithDuration(item.Start + version.Duration - now) .WithPipe() .Build(); } diff --git a/ErsatzTV.Core/Iptv/ChannelGuide.cs b/ErsatzTV.Core/Iptv/ChannelGuide.cs index ba0e8baaf..cec56c5a1 100644 --- a/ErsatzTV.Core/Iptv/ChannelGuide.cs +++ b/ErsatzTV.Core/Iptv/ChannelGuide.cs @@ -60,8 +60,8 @@ namespace ErsatzTV.Core.Iptv string title = playoutItem.MediaItem switch { - Movie m => m.MovieMetadata.HeadOrNone().Match(mm => mm.Title, () => m.Path), - Episode e => e.EpisodeMetadata.HeadOrNone().Match(em => em.Title, () => e.Path), + Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]"), + Episode e => e.EpisodeMetadata.HeadOrNone().Map(em => em.Title).IfNone("[unknown episode]"), _ => "[unknown]" }; diff --git a/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs b/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs index f7dfc1b0c..27ae51ebf 100644 --- a/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs @@ -19,16 +19,18 @@ namespace ErsatzTV.Core.Metadata public static Tuple GetFallbackMetadata(Episode episode) { - string fileName = Path.GetFileName(episode.Path); + string path = episode.MediaVersions.Head().MediaFiles.Head().Path; + string fileName = Path.GetFileName(path); var metadata = new EpisodeMetadata - { MetadataKind = MetadataKind.Fallback, Title = fileName ?? episode.Path }; + { MetadataKind = MetadataKind.Fallback, Title = fileName ?? path }; return fileName != null ? GetEpisodeMetadata(fileName, metadata) : Tuple(metadata, 0); } public static MovieMetadata GetFallbackMetadata(Movie movie) { - string fileName = Path.GetFileName(movie.Path); - var metadata = new MovieMetadata { MetadataKind = MetadataKind.Fallback, Title = fileName ?? movie.Path }; + string path = movie.MediaVersions.Head().MediaFiles.Head().Path; + string fileName = Path.GetFileName(path); + var metadata = new MovieMetadata { MetadataKind = MetadataKind.Fallback, Title = fileName ?? path }; return fileName != null ? GetMovieMetadata(fileName, metadata) : metadata; } diff --git a/ErsatzTV.Core/Metadata/LocalFolderScanner.cs b/ErsatzTV.Core/Metadata/LocalFolderScanner.cs index de1f2b27b..24d8a4328 100644 --- a/ErsatzTV.Core/Metadata/LocalFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/LocalFolderScanner.cs @@ -68,11 +68,17 @@ namespace ErsatzTV.Core.Metadata { try { - if (mediaItem.Statistics is null || - (mediaItem.Statistics.LastWriteTime ?? DateTime.MinValue) < - _localFileSystem.GetLastWriteTime(mediaItem.Path)) + MediaVersion version = mediaItem switch { - _logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", mediaItem.Path); + Movie m => m.MediaVersions.Head(), + Episode e => e.MediaVersions.Head() + }; + + string path = version.MediaFiles.Head().Path; + + if (version.DateUpdated < _localFileSystem.GetLastWriteTime(path)) + { + _logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", path); await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem); } diff --git a/ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs b/ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs index 9be90b171..333f9f171 100644 --- a/ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs +++ b/ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs @@ -33,42 +33,47 @@ namespace ErsatzTV.Core.Metadata { try { - FFprobe ffprobe = await GetProbeOutput(ffprobePath, mediaItem); - MediaItemStatistics statistics = ProjectToMediaItemStatistics(ffprobe); - return await ApplyStatisticsUpdate(mediaItem, statistics); + string filePath = mediaItem switch + { + Movie m => m.MediaVersions.Head().MediaFiles.Head().Path, + Episode e => e.MediaVersions.Head().MediaFiles.Head().Path + }; + + FFprobe ffprobe = await GetProbeOutput(ffprobePath, filePath); + MediaVersion version = ProjectToMediaVersion(ffprobe); + return await ApplyVersionUpdate(mediaItem, version, filePath); } catch (Exception ex) { - _logger.LogWarning(ex, "Failed to refresh statistics for media item at {Path}", mediaItem.Path); + _logger.LogWarning(ex, "Failed to refresh statistics for media item {Id}", mediaItem.Id); return false; } } - private async Task ApplyStatisticsUpdate( - MediaItem mediaItem, - MediaItemStatistics statistics) + private async Task ApplyVersionUpdate(MediaItem mediaItem, MediaVersion version, string filePath) { - if (mediaItem.Statistics == null) + MediaVersion mediaItemVersion = mediaItem switch { - mediaItem.Statistics = new MediaItemStatistics(); - } + Movie m => m.MediaVersions.Head(), + Episode e => e.MediaVersions.Head() + }; - bool durationChange = mediaItem.Statistics.Duration != statistics.Duration; + bool durationChange = mediaItemVersion.Duration != version.Duration; - mediaItem.Statistics.LastWriteTime = _localFileSystem.GetLastWriteTime(mediaItem.Path); - mediaItem.Statistics.Duration = statistics.Duration; - mediaItem.Statistics.AudioCodec = statistics.AudioCodec; - mediaItem.Statistics.SampleAspectRatio = statistics.SampleAspectRatio; - mediaItem.Statistics.DisplayAspectRatio = statistics.DisplayAspectRatio; - mediaItem.Statistics.Width = statistics.Width; - mediaItem.Statistics.Height = statistics.Height; - mediaItem.Statistics.VideoCodec = statistics.VideoCodec; - mediaItem.Statistics.VideoScanType = statistics.VideoScanType; + mediaItemVersion.DateUpdated = _localFileSystem.GetLastWriteTime(filePath); + mediaItemVersion.Duration = version.Duration; + mediaItemVersion.AudioCodec = version.AudioCodec; + mediaItemVersion.SampleAspectRatio = version.SampleAspectRatio; + mediaItemVersion.DisplayAspectRatio = version.DisplayAspectRatio; + mediaItemVersion.Width = version.Width; + mediaItemVersion.Height = version.Height; + mediaItemVersion.VideoCodec = version.VideoCodec; + mediaItemVersion.VideoScanKind = version.VideoScanKind; return await _mediaItemRepository.Update(mediaItem) && durationChange; } - private Task GetProbeOutput(string ffprobePath, MediaItem mediaItem) + private Task GetProbeOutput(string ffprobePath, string filePath) { var startInfo = new ProcessStartInfo { @@ -85,7 +90,7 @@ namespace ErsatzTV.Core.Metadata startInfo.ArgumentList.Add("-show_format"); startInfo.ArgumentList.Add("-show_streams"); startInfo.ArgumentList.Add("-i"); - startInfo.ArgumentList.Add(mediaItem.Path); + startInfo.ArgumentList.Add(filePath); var probe = new Process { @@ -101,7 +106,7 @@ namespace ErsatzTV.Core.Metadata }); } - private MediaItemStatistics ProjectToMediaItemStatistics(FFprobe probeOutput) => + private MediaVersion ProjectToMediaVersion(FFprobe probeOutput) => Optional(probeOutput) .Filter(json => json?.format != null && json.streams != null) .ToValidation("Unable to parse ffprobe output") @@ -111,41 +116,35 @@ namespace ErsatzTV.Core.Metadata { var duration = TimeSpan.FromSeconds(double.Parse(json.format.duration)); - var statistics = new MediaItemStatistics { Duration = duration }; + var version = new MediaVersion { Name = "Main", Duration = duration }; FFprobeStream audioStream = json.streams.FirstOrDefault(s => s.codec_type == "audio"); if (audioStream != null) { - statistics = statistics with - { - AudioCodec = audioStream.codec_name - }; + version.AudioCodec = audioStream.codec_name; } FFprobeStream videoStream = json.streams.FirstOrDefault(s => s.codec_type == "video"); if (videoStream != null) { - statistics = statistics with - { - SampleAspectRatio = videoStream.sample_aspect_ratio, - DisplayAspectRatio = videoStream.display_aspect_ratio, - Width = videoStream.width, - Height = videoStream.height, - VideoCodec = videoStream.codec_name, - VideoScanType = ScanTypeFromFieldOrder(videoStream.field_order) - }; + version.SampleAspectRatio = videoStream.sample_aspect_ratio; + version.DisplayAspectRatio = videoStream.display_aspect_ratio; + version.Width = videoStream.width; + version.Height = videoStream.height; + version.VideoCodec = videoStream.codec_name; + version.VideoScanKind = ScanKindFromFieldOrder(videoStream.field_order); } - return statistics; + return version; }, - _ => new MediaItemStatistics()); + _ => new MediaVersion { Name = "Main" }); - private VideoScanType ScanTypeFromFieldOrder(string fieldOrder) => + private VideoScanKind ScanKindFromFieldOrder(string fieldOrder) => fieldOrder?.ToLowerInvariant() switch { - var x when x == "tt" || x == "bb" || x == "tb" || x == "bt" => VideoScanType.Interlaced, - "progressive" => VideoScanType.Progressive, - _ => VideoScanType.Unknown + var x when x == "tt" || x == "bb" || x == "tb" || x == "bt" => VideoScanKind.Interlaced, + "progressive" => VideoScanKind.Progressive, + _ => VideoScanKind.Unknown }; // ReSharper disable InconsistentNaming diff --git a/ErsatzTV.Core/Metadata/MovieFolderScanner.cs b/ErsatzTV.Core/Metadata/MovieFolderScanner.cs index c3e91b382..7a40a2ddc 100644 --- a/ErsatzTV.Core/Metadata/MovieFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/MovieFolderScanner.cs @@ -111,7 +111,8 @@ namespace ErsatzTV.Core.Metadata { if (!Optional(movie.MovieMetadata).Flatten().Any()) { - _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", movie.Path); + string path = movie.MediaVersions.Head().MediaFiles.Head().Path; + _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path); await _localMetadataProvider.RefreshFallbackMetadata(movie); } }); @@ -148,8 +149,9 @@ namespace ErsatzTV.Core.Metadata private Option LocateNfoFile(Movie movie) { - string movieAsNfo = Path.ChangeExtension(movie.Path, "nfo"); - string movieNfo = Path.Combine(Path.GetDirectoryName(movie.Path) ?? string.Empty, "movie.nfo"); + string path = movie.MediaVersions.Head().MediaFiles.Head().Path; + string movieAsNfo = Path.ChangeExtension(path, "nfo"); + string movieNfo = Path.Combine(Path.GetDirectoryName(path) ?? string.Empty, "movie.nfo"); return Seq.create(movieAsNfo, movieNfo) .Filter(s => _localFileSystem.FileExists(s)) .HeadOrNone(); @@ -157,9 +159,10 @@ namespace ErsatzTV.Core.Metadata private Option LocatePoster(Movie movie) { - string folder = Path.GetDirectoryName(movie.Path) ?? string.Empty; + string path = movie.MediaVersions.Head().MediaFiles.Head().Path; + string folder = Path.GetDirectoryName(path) ?? string.Empty; IEnumerable possibleMoviePosters = ImageFileExtensions.Collect( - ext => new[] { $"poster.{ext}", Path.GetFileNameWithoutExtension(movie.Path) + $"-poster.{ext}" }) + ext => new[] { $"poster.{ext}", Path.GetFileNameWithoutExtension(path) + $"-poster.{ext}" }) .Map(f => Path.Combine(folder, f)); Option result = possibleMoviePosters.Filter(p => _localFileSystem.FileExists(p)).HeadOrNone(); return result; diff --git a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs index 5296c6c32..ef6c099f4 100644 --- a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs @@ -188,7 +188,8 @@ namespace ErsatzTV.Core.Metadata { if (!Optional(episode.EpisodeMetadata).Flatten().Any()) { - _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", episode.Path); + string path = episode.MediaVersions.Head().MediaFiles.Head().Path; + _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path); await _localMetadataProvider.RefreshFallbackMetadata(episode); } }); @@ -280,9 +281,12 @@ namespace ErsatzTV.Core.Metadata Optional(Path.Combine(showFolder, "tvshow.nfo")) .Filter(s => _localFileSystem.FileExists(s)); - private Option LocateNfoFile(Episode episode) => - Optional(Path.ChangeExtension(episode.Path, "nfo")) + private Option LocateNfoFile(Episode episode) + { + string path = episode.MediaVersions.Head().MediaFiles.Head().Path; + return Optional(Path.ChangeExtension(path, "nfo")) .Filter(s => _localFileSystem.FileExists(s)); + } private Option LocatePosterForShow(string showFolder) => ImageFileExtensions @@ -302,9 +306,10 @@ namespace ErsatzTV.Core.Metadata private Option LocateThumbnail(Episode episode) { - string folder = Path.GetDirectoryName(episode.Path) ?? string.Empty; + string path = episode.MediaVersions.Head().MediaFiles.Head().Path; + string folder = Path.GetDirectoryName(path) ?? string.Empty; return ImageFileExtensions - .Map(ext => Path.GetFileNameWithoutExtension(episode.Path) + $"-thumb.{ext}") + .Map(ext => Path.GetFileNameWithoutExtension(path) + $"-thumb.{ext}") .Map(f => Path.Combine(folder, f)) .Filter(f => _localFileSystem.FileExists(f)) .HeadOrNone(); diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs index 6d31ff7be..ed1487ef7 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; using ErsatzTV.Core.Domain; @@ -145,14 +144,20 @@ namespace ErsatzTV.Core.Scheduling DisplayTitle(mediaItem), itemStartTime); + MediaVersion version = mediaItem switch + { + Movie m => m.MediaVersions.Head(), + Episode e => e.MediaVersions.Head() + }; + var playoutItem = new PlayoutItem { MediaItemId = mediaItem.Id, Start = itemStartTime.UtcDateTime, - Finish = itemStartTime.UtcDateTime + mediaItem.Statistics.Duration + Finish = itemStartTime.UtcDateTime + version.Duration }; - currentTime = itemStartTime + mediaItem.Statistics.Duration; + currentTime = itemStartTime + version.Duration; enumerator.MoveNext(); playout.Items.Add(playoutItem); @@ -187,6 +192,12 @@ namespace ErsatzTV.Core.Scheduling enumerator.Current.Do( peekMediaItem => { + MediaVersion peekVersion = peekMediaItem switch + { + Movie m => m.MediaVersions.Head(), + Episode e => e.MediaVersions.Head() + }; + ProgramScheduleItem peekScheduleItem = sortedScheduleItems[(index + 1) % sortedScheduleItems.Count]; DateTimeOffset peekScheduleItemStart = @@ -198,7 +209,7 @@ namespace ErsatzTV.Core.Scheduling // is after, we need to move on to the next schedule item // eventually, spots probably have to fit in this gap bool willNotFinishInTime = currentTime <= peekScheduleItemStart && - currentTime + peekMediaItem.Statistics.Duration > + currentTime + peekVersion.Duration > peekScheduleItemStart; if (willNotFinishInTime) { @@ -213,6 +224,12 @@ namespace ErsatzTV.Core.Scheduling enumerator.Current.Do( peekMediaItem => { + MediaVersion peekVersion = peekMediaItem switch + { + Movie m => m.MediaVersions.Head(), + Episode e => e.MediaVersions.Head() + }; + // remember when we need to finish this duration item if (durationFinish.IsNone) { @@ -221,7 +238,7 @@ namespace ErsatzTV.Core.Scheduling bool willNotFinishInTime = currentTime <= durationFinish.IfNone(DateTime.MinValue) && - currentTime + peekMediaItem.Statistics.Duration > + currentTime + peekVersion.Duration > durationFinish.IfNone(DateTime.MinValue); if (willNotFinishInTime) { @@ -391,10 +408,10 @@ namespace ErsatzTV.Core.Scheduling { Episode e => e.EpisodeMetadata.Any() && e.Season != null ? $"{e.EpisodeMetadata.Head().Title} - s{e.Season.SeasonNumber:00}e{e.EpisodeNumber:00}" - : Path.GetFileName(e.Path), + : "[unknown episode]", Movie m => m.MovieMetadata.HeadOrNone().Match( mm => mm.Title ?? string.Empty, - () => Path.GetFileName(m.Path)), + () => "[unknown movie]"), _ => string.Empty }; diff --git a/ErsatzTV.Infrastructure/Data/Configurations/MediaItem/MediaItemConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/MediaItem/MediaItemConfiguration.cs index 41c4066b7..1117b490b 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/MediaItem/MediaItemConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/MediaItem/MediaItemConfiguration.cs @@ -6,14 +6,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations { public class MediaItemConfiguration : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("MediaItem"); - - builder.OwnsOne(c => c.Statistics).WithOwner(); - - builder.HasIndex(i => i.Path) - .IsUnique(); - } + public void Configure(EntityTypeBuilder builder) => builder.ToTable("MediaItem"); + // builder.OwnsOne(c => c.Statistics).WithOwner(); + // + // builder.HasIndex(i => i.Path) + // .IsUnique(); } } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs index 93db72a16..c61b2152c 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs @@ -46,8 +46,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Include(i => i.MovieMetadata) .ThenInclude(mm => mm.Artwork) .Include(i => i.LibraryPath) - .OrderBy(i => i.Path) - .SingleOrDefaultAsync(i => i.Path == path); + .Include(i => i.MediaVersions) + .OrderBy(i => i.MediaVersions.First().MediaFiles.First().Path) + .SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path); return await maybeExisting.Match( mediaItem => Right(mediaItem).AsTask(), @@ -94,7 +95,20 @@ namespace ErsatzTV.Infrastructure.Data.Repositories { try { - var movie = new Movie { LibraryPathId = libraryPathId, Path = path }; + var movie = new Movie + { + LibraryPathId = libraryPathId, + MediaVersions = new List + { + new() + { + MediaFiles = new List + { + new() { Path = path } + } + } + } + }; await _dbContext.Movies.AddAsync(movie); await _dbContext.SaveChangesAsync(); await _dbContext.Entry(movie).Reference(m => m.LibraryPath).LoadAsync(); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs index d68fd6f14..cdecf2191 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs @@ -219,9 +219,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories Option maybeExisting = await _dbContext.Episodes .Include(i => i.EpisodeMetadata) .ThenInclude(em => em.Artwork) - .Include(i => i.Statistics) - .OrderBy(i => i.Path) - .SingleOrDefaultAsync(i => i.Path == path); + .Include(i => i.MediaVersions) + .OrderBy(i => i.MediaVersions.First().MediaFiles.First().Path) + .SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path); return await maybeExisting.Match( episode => Right(episode).AsTask(), @@ -268,7 +268,6 @@ namespace ErsatzTV.Infrastructure.Data.Repositories var season = new Season { ShowId = show.Id, - Path = path, SeasonNumber = seasonNumber, Episodes = new List(), SeasonMetadata = new List() @@ -291,8 +290,17 @@ namespace ErsatzTV.Infrastructure.Data.Repositories { LibraryPathId = libraryPathId, SeasonId = season.Id, - Path = path, - EpisodeMetadata = new List() + EpisodeMetadata = new List(), + MediaVersions = new List + { + new() + { + MediaFiles = new List + { + new() { Path = path } + } + } + } }; await _dbContext.Episodes.AddAsync(episode); await _dbContext.SaveChangesAsync(); diff --git a/ErsatzTV.Infrastructure/Data/TvContext.cs b/ErsatzTV.Infrastructure/Data/TvContext.cs index 57946a4a1..f809f0f85 100644 --- a/ErsatzTV.Infrastructure/Data/TvContext.cs +++ b/ErsatzTV.Infrastructure/Data/TvContext.cs @@ -22,6 +22,8 @@ namespace ErsatzTV.Infrastructure.Data public DbSet LibraryPaths { get; set; } public DbSet PlexLibraries { get; set; } public DbSet MediaItems { get; set; } + public DbSet MediaVersions { get; set; } + public DbSet MediaFiles { get; set; } public DbSet Movies { get; set; } public DbSet MovieMetadata { get; set; } public DbSet Shows { get; set; } diff --git a/ErsatzTV.Infrastructure/Migrations/20210228021437_Add_MediaVersionVideoScanKind.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20210228021437_Add_MediaVersionVideoScanKind.Designer.cs new file mode 100644 index 000000000..e27d743c9 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210228021437_Add_MediaVersionVideoScanKind.Designer.cs @@ -0,0 +1,1509 @@ +// +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("20210228021437_Add_MediaVersionVideoScanKind")] + partial class Add_MediaVersionVideoScanKind + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.3"); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtworkKind") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("Artwork"); + }); + + 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("Channel"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Collection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.CollectionItem", b => + { + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "MediaItemId"); + + b.HasIndex("MediaItemId"); + + b.ToTable("CollectionItem"); + }); + + 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("ConfigElement"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("EpisodeId") + .HasColumnType("INTEGER"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.ToTable("EpisodeMetadata"); + }); + + 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("FFmpegProfile"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScan") + .HasColumnType("TEXT"); + + b.Property("MediaKind") + .HasColumnType("INTEGER"); + + b.Property("MediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaSourceId"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryPath"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MediaVersionId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaVersionId"); + + b.ToTable("MediaFile"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastWriteTime") + .HasColumnType("TEXT"); + + b.Property("LibraryPathId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("TelevisionEpisodeId") + .HasColumnType("INTEGER"); + + b.Property("TelevisionSeasonId") + .HasColumnType("INTEGER"); + + b.Property("TelevisionShowId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryPathId"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("MediaItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudioCodec") + .HasColumnType("TEXT"); + + b.Property("DisplayAspectRatio") + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("EpisodeId") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("MovieId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("SampleAspectRatio") + .HasColumnType("TEXT"); + + b.Property("VideoCodec") + .HasColumnType("TEXT"); + + b.Property("VideoScanKind") + .HasColumnType("INTEGER"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.HasIndex("MovieId"); + + b.ToTable("MediaVersion"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("MovieId") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("MovieMetadata"); + }); + + 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("Playout"); + }); + + 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("PlayoutItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("PlayoutProgramScheduleAnchor"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexConnection", 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("PlexConnection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaItemPart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("File") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("PlexId") + .HasColumnType("INTEGER"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("PlexMediaItemPart"); + }); + + 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("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("ProgramScheduleItem"); + }); + + 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("Resolution"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("SeasonMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ShowId"); + + b.ToTable("ShowMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.ToTable("LocalLibrary"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("ShouldSyncItems") + .HasColumnType("INTEGER"); + + b.ToTable("PlexLibrary"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.HasIndex("SeasonId"); + + b.ToTable("Episode"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Movie"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("ShowId") + .HasColumnType("INTEGER"); + + b.HasIndex("ShowId"); + + b.ToTable("Season"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Show"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.ToTable("LocalMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("ClientIdentifier") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.Property("ServerName") + .HasColumnType("TEXT"); + + b.ToTable("PlexMediaSource"); + }); + + 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("ProgramScheduleDurationItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleFloodItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.ToTable("ProgramScheduleMultipleItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleOneItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("PartId") + .HasColumnType("INTEGER"); + + b.HasIndex("PartId"); + + b.ToTable("PlexMovie"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b => + { + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Artwork") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Artwork") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Artwork") + .HasForeignKey("SeasonMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Artwork") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + 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.CollectionItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany("CollectionItems") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany("CollectionItems") + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", "Episode") + .WithMany("EpisodeMetadata") + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Episode"); + }); + + 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.Library", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", "MediaSource") + .WithMany("Libraries") + .HasForeignKey("MediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", "Library") + .WithMany("Paths") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaFile", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion") + .WithMany("MediaFiles") + .HasForeignKey("MediaVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaVersion"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.LibraryPath", "LibraryPath") + .WithMany("MediaItems") + .HasForeignKey("LibraryPathId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.MediaItemStatistics", "Statistics", b1 => + { + b1.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b1.Property("AudioCodec") + .HasColumnType("TEXT"); + + b1.Property("DisplayAspectRatio") + .HasColumnType("TEXT"); + + b1.Property("Duration") + .HasColumnType("TEXT"); + + b1.Property("Height") + .HasColumnType("INTEGER"); + + b1.Property("LastWriteTime") + .HasColumnType("TEXT"); + + b1.Property("SampleAspectRatio") + .HasColumnType("TEXT"); + + b1.Property("VideoCodec") + .HasColumnType("TEXT"); + + b1.Property("VideoScanType") + .HasColumnType("INTEGER"); + + b1.Property("Width") + .HasColumnType("INTEGER"); + + b1.HasKey("MediaItemId"); + + b1.ToTable("MediaItem"); + + b1.WithOwner() + .HasForeignKey("MediaItemId"); + }); + + b.Navigation("LibraryPath"); + + b.Navigation("Statistics"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", null) + .WithMany("MediaVersions") + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Movie", null) + .WithMany("MediaVersions") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Movie", "Movie") + .WithMany("MovieMetadata") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + 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("Playout"); + + 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.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId"); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId"); + + 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.CollectionEnumeratorState", "EnumeratorState", b1 => + { + b1.Property("PlayoutProgramScheduleAnchorId") + .HasColumnType("INTEGER"); + + b1.Property("Index") + .HasColumnType("INTEGER"); + + b1.Property("Seed") + .HasColumnType("INTEGER"); + + b1.HasKey("PlayoutProgramScheduleAnchorId"); + + b1.ToTable("PlayoutProgramScheduleAnchor"); + + b1.WithOwner() + .HasForeignKey("PlayoutProgramScheduleAnchorId"); + }); + + b.Navigation("Collection"); + + b.Navigation("EnumeratorState"); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", "PlexMediaSource") + .WithMany("Connections") + .HasForeignKey("PlexMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlexMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId"); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId"); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Items") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Season", "Season") + .WithMany("SeasonMetadata") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Show", "Show") + .WithMany("ShowMetadata") + .HasForeignKey("ShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.LocalLibrary", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexLibrary", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Episode", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Season", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Movie", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Season", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Show", "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.PlexMovie", b => + { + b.HasOne("ErsatzTV.Core.Domain.Movie", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexMovie", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.PlexMediaItemPart", "Part") + .WithMany() + .HasForeignKey("PartId"); + + b.Navigation("Part"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => + { + b.Navigation("CollectionItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.Navigation("Artwork"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + { + b.Navigation("Paths"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.Navigation("MediaItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Navigation("CollectionItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Navigation("Libraries"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.Navigation("MediaFiles"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.Navigation("Artwork"); + }); + + 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.SeasonMetadata", b => + { + b.Navigation("Artwork"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.Navigation("Artwork"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.Navigation("EpisodeMetadata"); + + b.Navigation("MediaVersions"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.Navigation("MediaVersions"); + + b.Navigation("MovieMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.Navigation("Episodes"); + + b.Navigation("SeasonMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.Navigation("Seasons"); + + b.Navigation("ShowMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.Navigation("Connections"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/20210228021437_Add_MediaVersionVideoScanKind.cs b/ErsatzTV.Infrastructure/Migrations/20210228021437_Add_MediaVersionVideoScanKind.cs new file mode 100644 index 000000000..ed5ccb2a9 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210228021437_Add_MediaVersionVideoScanKind.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace ErsatzTV.Infrastructure.Migrations +{ + public partial class Add_MediaVersionVideoScanKind : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) => + migrationBuilder.RenameColumn( + "IsInterlaced", + "MediaVersion", + "VideoScanKind"); + + protected override void Down(MigrationBuilder migrationBuilder) => + migrationBuilder.RenameColumn( + "VideoScanKind", + "MediaVersion", + "IsInterlaced"); + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/20210228021822_Update_MediaVersion.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20210228021822_Update_MediaVersion.Designer.cs new file mode 100644 index 000000000..199ad0fff --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210228021822_Update_MediaVersion.Designer.cs @@ -0,0 +1,1509 @@ +// +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("20210228021822_Update_MediaVersion")] + partial class Update_MediaVersion + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.3"); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtworkKind") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("Artwork"); + }); + + 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("Channel"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Collection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.CollectionItem", b => + { + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "MediaItemId"); + + b.HasIndex("MediaItemId"); + + b.ToTable("CollectionItem"); + }); + + 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("ConfigElement"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("EpisodeId") + .HasColumnType("INTEGER"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.ToTable("EpisodeMetadata"); + }); + + 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("FFmpegProfile"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScan") + .HasColumnType("TEXT"); + + b.Property("MediaKind") + .HasColumnType("INTEGER"); + + b.Property("MediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaSourceId"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryPath"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MediaVersionId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaVersionId"); + + b.ToTable("MediaFile"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastWriteTime") + .HasColumnType("TEXT"); + + b.Property("LibraryPathId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("TelevisionEpisodeId") + .HasColumnType("INTEGER"); + + b.Property("TelevisionSeasonId") + .HasColumnType("INTEGER"); + + b.Property("TelevisionShowId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryPathId"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("MediaItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudioCodec") + .HasColumnType("TEXT"); + + b.Property("DisplayAspectRatio") + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("EpisodeId") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("MovieId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("SampleAspectRatio") + .HasColumnType("TEXT"); + + b.Property("VideoCodec") + .HasColumnType("TEXT"); + + b.Property("VideoScanKind") + .HasColumnType("INTEGER"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.HasIndex("MovieId"); + + b.ToTable("MediaVersion"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("MovieId") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("MovieMetadata"); + }); + + 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("Playout"); + }); + + 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("PlayoutItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("PlayoutProgramScheduleAnchor"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexConnection", 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("PlexConnection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaItemPart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("File") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("PlexId") + .HasColumnType("INTEGER"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("PlexMediaItemPart"); + }); + + 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("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("ProgramScheduleItem"); + }); + + 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("Resolution"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("SeasonMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ShowId"); + + b.ToTable("ShowMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.ToTable("LocalLibrary"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("ShouldSyncItems") + .HasColumnType("INTEGER"); + + b.ToTable("PlexLibrary"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.HasIndex("SeasonId"); + + b.ToTable("Episode"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Movie"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("ShowId") + .HasColumnType("INTEGER"); + + b.HasIndex("ShowId"); + + b.ToTable("Season"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Show"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.ToTable("LocalMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("ClientIdentifier") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.Property("ServerName") + .HasColumnType("TEXT"); + + b.ToTable("PlexMediaSource"); + }); + + 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("ProgramScheduleDurationItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleFloodItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.ToTable("ProgramScheduleMultipleItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleOneItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("PartId") + .HasColumnType("INTEGER"); + + b.HasIndex("PartId"); + + b.ToTable("PlexMovie"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b => + { + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Artwork") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Artwork") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Artwork") + .HasForeignKey("SeasonMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Artwork") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + 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.CollectionItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany("CollectionItems") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany("CollectionItems") + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", "Episode") + .WithMany("EpisodeMetadata") + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Episode"); + }); + + 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.Library", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", "MediaSource") + .WithMany("Libraries") + .HasForeignKey("MediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", "Library") + .WithMany("Paths") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaFile", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion") + .WithMany("MediaFiles") + .HasForeignKey("MediaVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaVersion"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.LibraryPath", "LibraryPath") + .WithMany("MediaItems") + .HasForeignKey("LibraryPathId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.MediaItemStatistics", "Statistics", b1 => + { + b1.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b1.Property("AudioCodec") + .HasColumnType("TEXT"); + + b1.Property("DisplayAspectRatio") + .HasColumnType("TEXT"); + + b1.Property("Duration") + .HasColumnType("TEXT"); + + b1.Property("Height") + .HasColumnType("INTEGER"); + + b1.Property("LastWriteTime") + .HasColumnType("TEXT"); + + b1.Property("SampleAspectRatio") + .HasColumnType("TEXT"); + + b1.Property("VideoCodec") + .HasColumnType("TEXT"); + + b1.Property("VideoScanType") + .HasColumnType("INTEGER"); + + b1.Property("Width") + .HasColumnType("INTEGER"); + + b1.HasKey("MediaItemId"); + + b1.ToTable("MediaItem"); + + b1.WithOwner() + .HasForeignKey("MediaItemId"); + }); + + b.Navigation("LibraryPath"); + + b.Navigation("Statistics"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", null) + .WithMany("MediaVersions") + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Movie", null) + .WithMany("MediaVersions") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Movie", "Movie") + .WithMany("MovieMetadata") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + 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("Playout"); + + 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.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId"); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId"); + + 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.CollectionEnumeratorState", "EnumeratorState", b1 => + { + b1.Property("PlayoutProgramScheduleAnchorId") + .HasColumnType("INTEGER"); + + b1.Property("Index") + .HasColumnType("INTEGER"); + + b1.Property("Seed") + .HasColumnType("INTEGER"); + + b1.HasKey("PlayoutProgramScheduleAnchorId"); + + b1.ToTable("PlayoutProgramScheduleAnchor"); + + b1.WithOwner() + .HasForeignKey("PlayoutProgramScheduleAnchorId"); + }); + + b.Navigation("Collection"); + + b.Navigation("EnumeratorState"); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", "PlexMediaSource") + .WithMany("Connections") + .HasForeignKey("PlexMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlexMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId"); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId"); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Items") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Season", "Season") + .WithMany("SeasonMetadata") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Show", "Show") + .WithMany("ShowMetadata") + .HasForeignKey("ShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.LocalLibrary", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexLibrary", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Episode", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Season", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Movie", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Season", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Show", "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.PlexMovie", b => + { + b.HasOne("ErsatzTV.Core.Domain.Movie", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexMovie", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.PlexMediaItemPart", "Part") + .WithMany() + .HasForeignKey("PartId"); + + b.Navigation("Part"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => + { + b.Navigation("CollectionItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.Navigation("Artwork"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + { + b.Navigation("Paths"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.Navigation("MediaItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Navigation("CollectionItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Navigation("Libraries"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.Navigation("MediaFiles"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.Navigation("Artwork"); + }); + + 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.SeasonMetadata", b => + { + b.Navigation("Artwork"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.Navigation("Artwork"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.Navigation("EpisodeMetadata"); + + b.Navigation("MediaVersions"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.Navigation("MediaVersions"); + + b.Navigation("MovieMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.Navigation("Episodes"); + + b.Navigation("SeasonMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.Navigation("Seasons"); + + b.Navigation("ShowMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.Navigation("Connections"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/20210228021822_Update_MediaVersion.cs b/ErsatzTV.Infrastructure/Migrations/20210228021822_Update_MediaVersion.cs new file mode 100644 index 000000000..3bd108f2a --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210228021822_Update_MediaVersion.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace ErsatzTV.Infrastructure.Migrations +{ + public partial class Update_MediaVersion : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + // movie versions + migrationBuilder.Sql( + @"INSERT INTO MediaVersion (Name, Duration, SampleAspectRatio, DisplayAspectRatio, VideoCodec, AudioCodec, VideoScanKind, Width, Height, EpisodeId, MovieId) + SELECT 'Main', mi.Statistics_Duration, mi.Statistics_SampleAspectRatio, mi.Statistics_DisplayAspectRatio, mi.Statistics_VideoCodec, mi.Statistics_AudioCodec, mi.Statistics_VideoScanType, mi.Statistics_Width, mi.Statistics_Height, null, m.Id + FROM MediaItem mi + INNER JOIN Movie m on m.Id = mi.Id"); + + // episode versions + migrationBuilder.Sql( + @"INSERT INTO MediaVersion (Name, Duration, SampleAspectRatio, DisplayAspectRatio, VideoCodec, AudioCodec, VideoScanKind, Width, Height, EpisodeId, MovieId) + SELECT 'Main', mi.Statistics_Duration, mi.Statistics_SampleAspectRatio, mi.Statistics_DisplayAspectRatio, mi.Statistics_VideoCodec, mi.Statistics_AudioCodec, mi.Statistics_VideoScanType, mi.Statistics_Width, mi.Statistics_Height, e.Id, null + FROM MediaItem mi + INNER JOIN Episode e on e.Id = mi.Id"); + + // movie files + migrationBuilder.Sql( + @"INSERT INTO MediaFile (Path, MediaVersionId) + SELECT mi.Path, mv.Id + FROM MediaItem mi + INNER JOIN MediaVersion mv ON mv.MovieId = mi.Id"); + + // episode files + migrationBuilder.Sql( + @"INSERT INTO MediaFile (Path, MediaVersionId) + SELECT mi.Path, mv.Id + FROM MediaItem mi + INNER JOIN MediaVersion mv ON mv.EpisodeId = mi.Id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs index ac3cefe85..feb80b733 100644 --- a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs +++ b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs @@ -410,9 +410,6 @@ namespace ErsatzTV.Infrastructure.Migrations b.Property("Height") .HasColumnType("INTEGER"); - b.Property("IsInterlaced") - .HasColumnType("INTEGER"); - b.Property("MovieId") .HasColumnType("INTEGER"); @@ -425,6 +422,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Property("VideoCodec") .HasColumnType("TEXT"); + b.Property("VideoScanKind") + .HasColumnType("INTEGER"); + b.Property("Width") .HasColumnType("INTEGER"); diff --git a/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs b/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs index 16411a32e..eb92e922b 100644 --- a/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs +++ b/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs @@ -141,16 +141,17 @@ namespace ErsatzTV.Infrastructure.Plex Key = response.Key, LastWriteTime = lastWriteTime, MovieMetadata = new List { metadata }, - Statistics = new MediaItemStatistics - { - Duration = TimeSpan.FromMilliseconds(media.Duration), - Width = media.Width, - Height = media.Height, - // TODO: aspect ratio - AudioCodec = media.AudioCodec, - VideoCodec = media.VideoCodec, - LastWriteTime = lastWriteTime - }, + // TODO: versions + // Statistics = new MediaItemStatistics + // { + // Duration = TimeSpan.FromMilliseconds(media.Duration), + // Width = media.Width, + // Height = media.Height, + // // TODO: aspect ratio + // AudioCodec = media.AudioCodec, + // VideoCodec = media.VideoCodec, + // LastWriteTime = lastWriteTime + // }, // TODO: multi-part movies? Part = new PlexMediaItemPart {