Browse Source

start to use version/file instead of statistics

pull/31/head
Jason Dove 6 years ago
parent
commit
a656589c18
  1. 26
      ErsatzTV.Application/MediaItems/Mapper.cs
  2. 2
      ErsatzTV.Application/MediaItems/MediaItemViewModel.cs
  3. 23
      ErsatzTV.Application/Playouts/Mapper.cs
  4. 239
      ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsServiceTests.cs
  5. 26
      ErsatzTV.Core.Tests/Fakes/FakeMovieWithPath.cs
  6. 16
      ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs
  7. 50
      ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs
  8. 5
      ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs
  9. 6
      ErsatzTV.Core/Domain/MediaItem/MediaItem.cs
  10. 2
      ErsatzTV.Core/Domain/MediaItem/MediaItemStatistics.cs
  11. 7
      ErsatzTV.Core/Domain/MediaItem/MediaVersion.cs
  12. 2
      ErsatzTV.Core/Domain/MediaItem/VideoScanKind.cs
  13. 82
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs
  14. 15
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  15. 4
      ErsatzTV.Core/Iptv/ChannelGuide.cs
  16. 10
      ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs
  17. 14
      ErsatzTV.Core/Metadata/LocalFolderScanner.cs
  18. 85
      ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs
  19. 13
      ErsatzTV.Core/Metadata/MovieFolderScanner.cs
  20. 15
      ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs
  21. 31
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  22. 14
      ErsatzTV.Infrastructure/Data/Configurations/MediaItem/MediaItemConfiguration.cs
  23. 20
      ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs
  24. 20
      ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs
  25. 2
      ErsatzTV.Infrastructure/Data/TvContext.cs
  26. 1509
      ErsatzTV.Infrastructure/Migrations/20210228021437_Add_MediaVersionVideoScanKind.Designer.cs
  27. 19
      ErsatzTV.Infrastructure/Migrations/20210228021437_Add_MediaVersionVideoScanKind.cs
  28. 1509
      ErsatzTV.Infrastructure/Migrations/20210228021822_Update_MediaVersion.Designer.cs
  29. 42
      ErsatzTV.Infrastructure/Migrations/20210228021822_Update_MediaVersion.cs
  30. 6
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  31. 21
      ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs

26
ErsatzTV.Application/MediaItems/Mapper.cs

@ -1,5 +1,4 @@
using System; using System;
using System.IO;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaItems namespace ErsatzTV.Application.MediaItems
@ -7,10 +6,7 @@ namespace ErsatzTV.Application.MediaItems
internal static class Mapper internal static class Mapper
{ {
internal static MediaItemViewModel ProjectToViewModel(MediaItem mediaItem) => internal static MediaItemViewModel ProjectToViewModel(MediaItem mediaItem) =>
new( new(mediaItem.Id, mediaItem.LibraryPathId);
mediaItem.Id,
mediaItem.LibraryPathId,
mediaItem.Path);
internal static MediaItemSearchResultViewModel ProjectToSearchViewModel(MediaItem mediaItem) => internal static MediaItemSearchResultViewModel ProjectToSearchViewModel(MediaItem mediaItem) =>
mediaItem switch mediaItem switch
@ -42,15 +38,23 @@ namespace ErsatzTV.Application.MediaItems
{ {
Episode e => e.EpisodeMetadata.HeadOrNone() Episode e => e.EpisodeMetadata.HeadOrNone()
.Map(em => $"{em.Title} - s{e.Season.SeasonNumber:00}e{e.EpisodeNumber:00}") .Map(em => $"{em.Title} - s{e.Season.SeasonNumber:00}e{e.EpisodeNumber:00}")
.IfNone(Path.GetFileName(e.Path)), .IfNone("[unknown episode]"),
Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone(Path.GetFileName(m.Path)), Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]"),
_ => string.Empty _ => string.Empty
}; };
private static string GetDisplayDuration(MediaItem mediaItem) => private static string GetDisplayDuration(MediaItem mediaItem)
string.Format( {
mediaItem.Statistics.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}", MediaVersion version = mediaItem switch
mediaItem.Statistics.Duration); {
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 // TODO: fix this when search is reimplemented
private static string GetLibraryName(MediaItem item) => private static string GetLibraryName(MediaItem item) =>

2
ErsatzTV.Application/MediaItems/MediaItemViewModel.cs

@ -1,4 +1,4 @@
namespace ErsatzTV.Application.MediaItems namespace ErsatzTV.Application.MediaItems
{ {
public record MediaItemViewModel(int Id, int LibraryPathId, string Path); public record MediaItemViewModel(int Id, int LibraryPathId);
} }

23
ErsatzTV.Application/Playouts/Mapper.cs

@ -1,5 +1,4 @@
using System.IO; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Playouts namespace ErsatzTV.Application.Playouts
{ {
@ -26,14 +25,22 @@ namespace ErsatzTV.Application.Playouts
{ {
Episode e => e.EpisodeMetadata.HeadOrNone() Episode e => e.EpisodeMetadata.HeadOrNone()
.Map(em => $"{em.Title} - s{e.Season.SeasonNumber:00}e{e.EpisodeNumber:00}") .Map(em => $"{em.Title} - s{e.Season.SeasonNumber:00}e{e.EpisodeNumber:00}")
.IfNone(Path.GetFileName(e.Path)), .IfNone("[unknown episode]"),
Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone(Path.GetFileName(m.Path)), Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]"),
_ => string.Empty _ => string.Empty
}; };
private static string GetDisplayDuration(MediaItem mediaItem) => private static string GetDisplayDuration(MediaItem mediaItem)
string.Format( {
mediaItem.Statistics.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}", MediaVersion version = mediaItem switch
mediaItem.Statistics.Duration); {
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);
}
} }
} }

239
ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsServiceTests.cs

@ -15,15 +15,6 @@ namespace ErsatzTV.Core.Tests.FFmpeg
public CalculateSettings() => _calculator = new FFmpegPlaybackSettingsCalculator(); public CalculateSettings() => _calculator = new FFmpegPlaybackSettingsCalculator();
private static PlayoutItem EmptyPlayoutItem() =>
new()
{
MediaItem = new MediaItem
{
Statistics = new MediaItemStatistics()
}
};
[Test] [Test]
public void Should_UseSpecifiedThreadCount_ForTransportStream() public void Should_UseSpecifiedThreadCount_ForTransportStream()
{ {
@ -32,7 +23,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
EmptyPlayoutItem(), new MediaVersion(),
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.ThreadCount.Should().Be(7); actual.ThreadCount.Should().Be(7);
@ -46,7 +38,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreaming, StreamingMode.HttpLiveStreaming,
ffmpegProfile, ffmpegProfile,
EmptyPlayoutItem(), new MediaVersion(),
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.ThreadCount.Should().Be(7); actual.ThreadCount.Should().Be(7);
@ -60,7 +53,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
EmptyPlayoutItem(), new MediaVersion(),
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
string[] expected = { "+genpts", "+discardcorrupt", "+igndts" }; string[] expected = { "+genpts", "+discardcorrupt", "+igndts" };
@ -76,7 +70,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreaming, StreamingMode.HttpLiveStreaming,
ffmpegProfile, ffmpegProfile,
EmptyPlayoutItem(), new MediaVersion(),
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
string[] expected = { "+genpts", "+discardcorrupt", "+igndts" }; string[] expected = { "+genpts", "+discardcorrupt", "+igndts" };
@ -92,7 +87,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
EmptyPlayoutItem(), new MediaVersion(),
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.RealtimeOutput.Should().BeTrue(); actual.RealtimeOutput.Should().BeTrue();
@ -106,7 +102,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreaming, StreamingMode.HttpLiveStreaming,
ffmpegProfile, ffmpegProfile,
EmptyPlayoutItem(), new MediaVersion(),
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.RealtimeOutput.Should().BeTrue(); actual.RealtimeOutput.Should().BeTrue();
@ -118,13 +115,12 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset now = DateTimeOffset.Now; DateTimeOffset now = DateTimeOffset.Now;
FFmpegProfile ffmpegProfile = TestProfile(); FFmpegProfile ffmpegProfile = TestProfile();
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.Start = now.UtcDateTime;
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, new MediaVersion(),
now,
now.AddMinutes(5)); now.AddMinutes(5));
actual.StreamSeek.IsSome.Should().BeTrue(); actual.StreamSeek.IsSome.Should().BeTrue();
@ -137,13 +133,12 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset now = DateTimeOffset.Now; DateTimeOffset now = DateTimeOffset.Now;
FFmpegProfile ffmpegProfile = TestProfile(); FFmpegProfile ffmpegProfile = TestProfile();
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.Start = now.UtcDateTime;
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreaming, StreamingMode.HttpLiveStreaming,
ffmpegProfile, ffmpegProfile,
playoutItem, new MediaVersion(),
now,
now.AddMinutes(5)); now.AddMinutes(5));
actual.StreamSeek.IsSome.Should().BeTrue(); actual.StreamSeek.IsSome.Should().BeTrue();
@ -154,12 +149,12 @@ namespace ErsatzTV.Core.Tests.FFmpeg
public void ShouldNot_SetScaledSize_When_NotNormalizingResolution_ForTransportStream() public void ShouldNot_SetScaledSize_When_NotNormalizingResolution_ForTransportStream()
{ {
FFmpegProfile ffmpegProfile = TestProfile() with { NormalizeResolution = false }; FFmpegProfile ffmpegProfile = TestProfile() with { NormalizeResolution = false };
PlayoutItem playoutItem = EmptyPlayoutItem();
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, new MediaVersion(),
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
@ -174,15 +169,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
Resolution = new Resolution { Width = 1920, Height = 1080 } Resolution = new Resolution { Width = 1920, Height = 1080 }
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1920; var version = new MediaVersion { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" };
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
@ -197,15 +191,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
Resolution = new Resolution { Width = 1920, Height = 1080 } Resolution = new Resolution { Width = 1920, Height = 1080 }
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1918; var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" };
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
@ -220,15 +213,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
Resolution = new Resolution { Width = 1920, Height = 1080 } Resolution = new Resolution { Width = 1920, Height = 1080 }
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1920; var version = new MediaVersion { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" };
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
@ -244,15 +236,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
Resolution = new Resolution { Width = 1920, Height = 1080 } Resolution = new Resolution { Width = 1920, Height = 1080 }
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1918; var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" };
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
@ -268,15 +259,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
Resolution = new Resolution { Width = 1920, Height = 1080 } Resolution = new Resolution { Width = 1920, Height = 1080 }
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1918; var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" };
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreaming, StreamingMode.HttpLiveStreaming,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
@ -294,15 +284,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
VideoCodec = "testCodec" VideoCodec = "testCodec"
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1918; var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" };
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
@ -322,16 +311,15 @@ namespace ErsatzTV.Core.Tests.FFmpeg
VideoCodec = "testCodec" VideoCodec = "testCodec"
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1920; var version = new MediaVersion
playoutItem.MediaItem.Statistics.Height = 1080; { Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "mpeg2video" };
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
@ -351,16 +339,15 @@ namespace ErsatzTV.Core.Tests.FFmpeg
VideoCodec = "testCodec" VideoCodec = "testCodec"
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1920; var version = new MediaVersion
playoutItem.MediaItem.Statistics.Height = 1080; { Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "mpeg2video" };
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreaming, StreamingMode.HttpLiveStreaming,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
@ -379,16 +366,15 @@ namespace ErsatzTV.Core.Tests.FFmpeg
VideoCodec = "libx264" VideoCodec = "libx264"
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1920; var version = new MediaVersion
playoutItem.MediaItem.Statistics.Height = 1080; { Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "libx264" };
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Statistics.VideoCodec = "libx264";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
@ -408,16 +394,15 @@ namespace ErsatzTV.Core.Tests.FFmpeg
VideoCodec = "libx264" VideoCodec = "libx264"
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1920; var version = new MediaVersion
playoutItem.MediaItem.Statistics.Height = 1080; { Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "mpeg2video" };
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
@ -436,15 +421,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
VideoBitrate = 2525 VideoBitrate = 2525
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1918; var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" };
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
@ -463,16 +447,15 @@ namespace ErsatzTV.Core.Tests.FFmpeg
VideoBitrate = 2525 VideoBitrate = 2525
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1920; var version = new MediaVersion
playoutItem.MediaItem.Statistics.Height = 1080; { Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "mpeg2video" };
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
@ -491,15 +474,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
VideoBufferSize = 2525 VideoBufferSize = 2525
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1918; var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" };
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
@ -519,16 +501,15 @@ namespace ErsatzTV.Core.Tests.FFmpeg
VideoBufferSize = 2525 VideoBufferSize = 2525
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1920; var version = new MediaVersion
playoutItem.MediaItem.Statistics.Height = 1080; { Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "mpeg2video" };
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
@ -545,13 +526,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg
AudioCodec = "aac" AudioCodec = "aac"
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); var version = new MediaVersion { AudioCodec = "aac" };
playoutItem.MediaItem.Statistics.AudioCodec = "aac";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.AudioCodec.Should().Be("copy"); actual.AudioCodec.Should().Be("copy");
@ -566,13 +547,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg
AudioCodec = "aac" AudioCodec = "aac"
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); var version = new MediaVersion { AudioCodec = "ac3" };
playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.AudioCodec.Should().Be("copy"); actual.AudioCodec.Should().Be("copy");
@ -587,13 +568,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg
AudioCodec = "aac" AudioCodec = "aac"
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); var version = new MediaVersion { AudioCodec = "ac3" };
playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.AudioCodec.Should().Be("aac"); actual.AudioCodec.Should().Be("aac");
@ -608,13 +589,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg
AudioCodec = "aac" AudioCodec = "aac"
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); var version = new MediaVersion { AudioCodec = "ac3" };
playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreaming, StreamingMode.HttpLiveStreaming,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.AudioCodec.Should().Be("copy"); actual.AudioCodec.Should().Be("copy");
@ -629,13 +610,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg
AudioBitrate = 2424 AudioBitrate = 2424
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); var version = new MediaVersion { AudioCodec = "ac3" };
playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.AudioBitrate.IfNone(0).Should().Be(2424); actual.AudioBitrate.IfNone(0).Should().Be(2424);
@ -650,13 +631,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg
AudioBufferSize = 2424 AudioBufferSize = 2424
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); var version = new MediaVersion { AudioCodec = "ac3" };
playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.AudioBufferSize.IfNone(0).Should().Be(2424); actual.AudioBufferSize.IfNone(0).Should().Be(2424);
@ -673,13 +654,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg
AudioChannels = 6 AudioChannels = 6
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); var version = new MediaVersion { AudioCodec = "ac3" };
playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.AudioChannels.IsNone.Should().BeTrue(); actual.AudioChannels.IsNone.Should().BeTrue();
@ -696,13 +677,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg
AudioSampleRate = 48 AudioSampleRate = 48
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); var version = new MediaVersion { AudioCodec = "ac3" };
playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.AudioSampleRate.IsNone.Should().BeTrue(); actual.AudioSampleRate.IsNone.Should().BeTrue();
@ -718,13 +699,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg
AudioChannels = 6 AudioChannels = 6
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); var version = new MediaVersion { AudioCodec = "ac3" };
playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.AudioChannels.IfNone(0).Should().Be(6); actual.AudioChannels.IfNone(0).Should().Be(6);
@ -740,13 +721,13 @@ namespace ErsatzTV.Core.Tests.FFmpeg
AudioSampleRate = 48 AudioSampleRate = 48
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); var version = new MediaVersion { AudioCodec = "ac3" };
playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
ffmpegProfile, ffmpegProfile,
playoutItem, version,
DateTimeOffset.Now,
DateTimeOffset.Now); DateTimeOffset.Now);
actual.AudioSampleRate.IfNone(0).Should().Be(48); actual.AudioSampleRate.IfNone(0).Should().Be(48);

26
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<MediaVersion>
{
new()
{
MediaFiles = new List<MediaFile>
{
new() { Path = path }
}
}
};
}
public string Path { get; }
}
}

16
ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Metadata;
using FluentAssertions; using FluentAssertions;
@ -39,7 +40,20 @@ namespace ErsatzTV.Core.Tests.Metadata
public void GetFallbackMetadata_ShouldHandleVariousFormats(string path, string title, int season, int episode) public void GetFallbackMetadata_ShouldHandleVariousFormats(string path, string title, int season, int episode)
{ {
(EpisodeMetadata metadata, int episodeNumber) = FallbackMetadataProvider.GetFallbackMetadata( (EpisodeMetadata metadata, int episodeNumber) = FallbackMetadataProvider.GetFallbackMetadata(
new Episode { Path = path, LibraryPath = new LibraryPath() }); new Episode
{
LibraryPath = new LibraryPath(),
MediaVersions = new List<MediaVersion>
{
new()
{
MediaFiles = new List<MediaFile>
{
new() { Path = path }
}
}
}
});
metadata.Title.Should().Be(title); metadata.Title.Should().Be(title);
// TODO: how can we test season number? do we need to? // TODO: how can we test season number? do we need to?

50
ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs

@ -43,7 +43,7 @@ namespace ErsatzTV.Core.Tests.Metadata
_movieRepository = new Mock<IMovieRepository>(); _movieRepository = new Mock<IMovieRepository>();
_movieRepository.Setup(x => x.GetOrAdd(It.IsAny<LibraryPath>(), It.IsAny<string>())) _movieRepository.Setup(x => x.GetOrAdd(It.IsAny<LibraryPath>(), It.IsAny<string>()))
.Returns( .Returns(
(LibraryPath _, string path) => Right<BaseError, Movie>(new Movie { Path = path }).AsTask()); (LibraryPath _, string path) => Right<BaseError, Movie>(new FakeMovieWithPath(path)).AsTask());
_localStatisticsProvider = new Mock<ILocalStatisticsProvider>(); _localStatisticsProvider = new Mock<ILocalStatisticsProvider>();
_localMetadataProvider = new Mock<ILocalMetadataProvider>(); _localMetadataProvider = new Mock<ILocalMetadataProvider>();
@ -92,7 +92,7 @@ namespace ErsatzTV.Core.Tests.Metadata
Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}"));
MovieFolderScanner service = GetService( MovieFolderScanner service = GetService(
new FakeFileEntry(moviePath) new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }
); );
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot }; 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); _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
_localStatisticsProvider.Verify( _localStatisticsProvider.Verify(
x => x.RefreshStatistics(FFprobePath, It.Is<MediaItem>(i => i.Path == moviePath)), x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once); Times.Once);
_localMetadataProvider.Verify( _localMetadataProvider.Verify(
x => x.RefreshFallbackMetadata(It.Is<MediaItem>(i => i.Path == moviePath)), x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once); Times.Once);
} }
@ -124,7 +124,7 @@ namespace ErsatzTV.Core.Tests.Metadata
string metadataPath = Path.ChangeExtension(moviePath, "nfo"); string metadataPath = Path.ChangeExtension(moviePath, "nfo");
MovieFolderScanner service = GetService( MovieFolderScanner service = GetService(
new FakeFileEntry(moviePath), new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now },
new FakeFileEntry(metadataPath) new FakeFileEntry(metadataPath)
); );
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot }; 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); _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
_localStatisticsProvider.Verify( _localStatisticsProvider.Verify(
x => x.RefreshStatistics(FFprobePath, It.Is<MediaItem>(i => i.Path == moviePath)), x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once); Times.Once);
_localMetadataProvider.Verify( _localMetadataProvider.Verify(
x => x.RefreshSidecarMetadata(It.Is<MediaItem>(i => i.Path == moviePath), metadataPath), x => x.RefreshSidecarMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath), metadataPath),
Times.Once); Times.Once);
} }
@ -157,7 +157,7 @@ namespace ErsatzTV.Core.Tests.Metadata
string metadataPath = Path.Combine(Path.GetDirectoryName(moviePath) ?? string.Empty, "movie.nfo"); string metadataPath = Path.Combine(Path.GetDirectoryName(moviePath) ?? string.Empty, "movie.nfo");
MovieFolderScanner service = GetService( MovieFolderScanner service = GetService(
new FakeFileEntry(moviePath), new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now },
new FakeFileEntry(metadataPath) new FakeFileEntry(metadataPath)
); );
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot }; 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); _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
_localStatisticsProvider.Verify( _localStatisticsProvider.Verify(
x => x.RefreshStatistics(FFprobePath, It.Is<MediaItem>(i => i.Path == moviePath)), x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once); Times.Once);
_localMetadataProvider.Verify( _localMetadataProvider.Verify(
x => x.RefreshSidecarMetadata(It.Is<MediaItem>(i => i.Path == moviePath), metadataPath), x => x.RefreshSidecarMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath), metadataPath),
Times.Once); Times.Once);
} }
@ -194,7 +194,7 @@ namespace ErsatzTV.Core.Tests.Metadata
$"poster.{imageExtension}"); $"poster.{imageExtension}");
MovieFolderScanner service = GetService( MovieFolderScanner service = GetService(
new FakeFileEntry(moviePath), new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now },
new FakeFileEntry(posterPath) { LastWriteTime = DateTime.Now } new FakeFileEntry(posterPath) { LastWriteTime = DateTime.Now }
); );
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot }; 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); _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
_localStatisticsProvider.Verify( _localStatisticsProvider.Verify(
x => x.RefreshStatistics(FFprobePath, It.Is<MediaItem>(i => i.Path == moviePath)), x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once); Times.Once);
_localMetadataProvider.Verify( _localMetadataProvider.Verify(
x => x.RefreshFallbackMetadata(It.Is<MediaItem>(i => i.Path == moviePath)), x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once); Times.Once);
_imageCache.Verify( _imageCache.Verify(
@ -235,7 +235,7 @@ namespace ErsatzTV.Core.Tests.Metadata
$"Movie (2020)-poster.{imageExtension}"); $"Movie (2020)-poster.{imageExtension}");
MovieFolderScanner service = GetService( MovieFolderScanner service = GetService(
new FakeFileEntry(moviePath), new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now },
new FakeFileEntry(posterPath) { LastWriteTime = DateTime.Now } new FakeFileEntry(posterPath) { LastWriteTime = DateTime.Now }
); );
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot }; 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); _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
_localStatisticsProvider.Verify( _localStatisticsProvider.Verify(
x => x.RefreshStatistics(FFprobePath, It.Is<MediaItem>(i => i.Path == moviePath)), x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once); Times.Once);
_localMetadataProvider.Verify( _localMetadataProvider.Verify(
x => x.RefreshFallbackMetadata(It.Is<MediaItem>(i => i.Path == moviePath)), x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once); Times.Once);
_imageCache.Verify( _imageCache.Verify(
@ -272,7 +272,7 @@ namespace ErsatzTV.Core.Tests.Metadata
Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}"));
MovieFolderScanner service = GetService( MovieFolderScanner service = GetService(
new FakeFileEntry(moviePath), new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now },
new FakeFileEntry( new FakeFileEntry(
Path.Combine( Path.Combine(
Path.GetDirectoryName(moviePath) ?? string.Empty, Path.GetDirectoryName(moviePath) ?? string.Empty,
@ -288,11 +288,11 @@ namespace ErsatzTV.Core.Tests.Metadata
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
_localStatisticsProvider.Verify( _localStatisticsProvider.Verify(
x => x.RefreshStatistics(FFprobePath, It.Is<MediaItem>(i => i.Path == moviePath)), x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once); Times.Once);
_localMetadataProvider.Verify( _localMetadataProvider.Verify(
x => x.RefreshFallbackMetadata(It.Is<MediaItem>(i => i.Path == moviePath)), x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once); Times.Once);
} }
@ -308,7 +308,7 @@ namespace ErsatzTV.Core.Tests.Metadata
Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}"));
MovieFolderScanner service = GetService( MovieFolderScanner service = GetService(
new FakeFileEntry(moviePath), new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now },
new FakeFileEntry( new FakeFileEntry(
Path.Combine( Path.Combine(
Path.GetDirectoryName(moviePath) ?? string.Empty, Path.GetDirectoryName(moviePath) ?? string.Empty,
@ -324,11 +324,11 @@ namespace ErsatzTV.Core.Tests.Metadata
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
_localStatisticsProvider.Verify( _localStatisticsProvider.Verify(
x => x.RefreshStatistics(FFprobePath, It.Is<MediaItem>(i => i.Path == moviePath)), x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once); Times.Once);
_localMetadataProvider.Verify( _localMetadataProvider.Verify(
x => x.RefreshFallbackMetadata(It.Is<MediaItem>(i => i.Path == moviePath)), x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once); Times.Once);
} }
@ -342,7 +342,7 @@ namespace ErsatzTV.Core.Tests.Metadata
Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}"));
MovieFolderScanner service = GetService( MovieFolderScanner service = GetService(
new FakeFileEntry(moviePath) new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }
); );
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot }; 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); _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
_localStatisticsProvider.Verify( _localStatisticsProvider.Verify(
x => x.RefreshStatistics(FFprobePath, It.Is<MediaItem>(i => i.Path == moviePath)), x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once); Times.Once);
_localMetadataProvider.Verify( _localMetadataProvider.Verify(
x => x.RefreshFallbackMetadata(It.Is<MediaItem>(i => i.Path == moviePath)), x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once); Times.Once);
} }

5
ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs

@ -612,7 +612,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
{ {
Id = id, Id = id,
MovieMetadata = new List<MovieMetadata> { new() { ReleaseDate = aired } }, MovieMetadata = new List<MovieMetadata> { new() { ReleaseDate = aired } },
Statistics = new MediaItemStatistics { Duration = duration } MediaVersions = new List<MediaVersion>
{
new() { Duration = duration }
}
}; };
private TestData TestDataFloodForItems(List<MediaItem> mediaItems, PlaybackOrder playbackOrder) private TestData TestDataFloodForItems(List<MediaItem> mediaItems, PlaybackOrder playbackOrder)

6
ErsatzTV.Core/Domain/MediaItem/MediaItem.cs

@ -6,7 +6,8 @@ namespace ErsatzTV.Core.Domain
public class MediaItem public class MediaItem
{ {
public int Id { get; set; } public int Id { get; set; }
public MediaItemStatistics Statistics { get; set; }
// public MediaItemStatistics Statistics { get; set; }
public DateTime? LastWriteTime { get; set; } public DateTime? LastWriteTime { get; set; }
public int LibraryPathId { get; set; } public int LibraryPathId { get; set; }
@ -18,7 +19,8 @@ namespace ErsatzTV.Core.Domain
public int TelevisionEpisodeId { get; set; } public int TelevisionEpisodeId { get; set; }
public List<Collection> Collections { get; set; } public List<Collection> Collections { get; set; }
public List<CollectionItem> CollectionItems { get; set; } public List<CollectionItem> CollectionItems { get; set; }
public string Path { get; set; } // public string Path { get; set; }
} }
} }

2
ErsatzTV.Core/Domain/MediaItemStatistics.cs → ErsatzTV.Core/Domain/MediaItem/MediaItemStatistics.cs

@ -11,7 +11,7 @@ namespace ErsatzTV.Core.Domain
public string DisplayAspectRatio { get; set; } public string DisplayAspectRatio { get; set; }
public string VideoCodec { get; set; } public string VideoCodec { get; set; }
public string AudioCodec { get; set; } public string AudioCodec { get; set; }
public VideoScanType VideoScanType { get; set; } public VideoScanKind VideoScanType { get; set; }
public int Width { get; set; } public int Width { get; set; }
public int Height { get; set; } public int Height { get; set; }
} }

7
ErsatzTV.Core/Domain/MediaItem/MediaVersion.cs

@ -1,9 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using ErsatzTV.Core.Interfaces.FFmpeg;
namespace ErsatzTV.Core.Domain namespace ErsatzTV.Core.Domain
{ {
public class MediaVersion public class MediaVersion : IDisplaySize
{ {
public int Id { get; set; } public int Id { get; set; }
public string Name { get; set; } public string Name { get; set; }
@ -15,7 +16,9 @@ namespace ErsatzTV.Core.Domain
public string DisplayAspectRatio { get; set; } public string DisplayAspectRatio { get; set; }
public string VideoCodec { get; set; } public string VideoCodec { get; set; }
public string AudioCodec { 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 Width { get; set; }
public int Height { get; set; } public int Height { get; set; }
} }

2
ErsatzTV.Core/Domain/VideoScanType.cs → ErsatzTV.Core/Domain/MediaItem/VideoScanKind.cs

@ -1,6 +1,6 @@
namespace ErsatzTV.Core.Domain namespace ErsatzTV.Core.Domain
{ {
public enum VideoScanType public enum VideoScanKind
{ {
Unknown = 0, Unknown = 0,
Progressive = 1, Progressive = 1,

82
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs

@ -44,7 +44,8 @@ namespace ErsatzTV.Core.FFmpeg
public FFmpegPlaybackSettings CalculateSettings( public FFmpegPlaybackSettings CalculateSettings(
StreamingMode streamingMode, StreamingMode streamingMode,
FFmpegProfile ffmpegProfile, FFmpegProfile ffmpegProfile,
PlayoutItem playoutItem, MediaVersion version,
DateTimeOffset start,
DateTimeOffset now) DateTimeOffset now)
{ {
var result = new FFmpegPlaybackSettings var result = new FFmpegPlaybackSettings
@ -53,9 +54,9 @@ namespace ErsatzTV.Core.FFmpeg
FormatFlags = CommonFormatFlags FormatFlags = CommonFormatFlags
}; };
if (now != playoutItem.Start) if (now != start)
{ {
result.StreamSeek = now - playoutItem.Start; result.StreamSeek = now - start;
} }
switch (streamingMode) switch (streamingMode)
@ -66,24 +67,23 @@ namespace ErsatzTV.Core.FFmpeg
result.Deinterlace = false; result.Deinterlace = false;
break; break;
case StreamingMode.TransportStream: case StreamingMode.TransportStream:
if (NeedToScale(ffmpegProfile, playoutItem.MediaItem.Statistics)) if (NeedToScale(ffmpegProfile, version))
{ {
IDisplaySize scaledSize = CalculateScaledSize(ffmpegProfile, playoutItem.MediaItem.Statistics); IDisplaySize scaledSize = CalculateScaledSize(ffmpegProfile, version);
if (!scaledSize.IsSameSizeAs(playoutItem.MediaItem.Statistics)) if (!scaledSize.IsSameSizeAs(version))
{ {
result.ScaledSize = Some( result.ScaledSize = Some(CalculateScaledSize(ffmpegProfile, version));
CalculateScaledSize(ffmpegProfile, playoutItem.MediaItem.Statistics));
} }
} }
IDisplaySize sizeAfterScaling = result.ScaledSize.IfNone(playoutItem.MediaItem.Statistics); IDisplaySize sizeAfterScaling = result.ScaledSize.IfNone(version);
if (!sizeAfterScaling.IsSameSizeAs(ffmpegProfile.Resolution)) if (!sizeAfterScaling.IsSameSizeAs(ffmpegProfile.Resolution))
{ {
result.PadToDesiredResolution = true; result.PadToDesiredResolution = true;
} }
if (result.ScaledSize.IsSome || result.PadToDesiredResolution || if (result.ScaledSize.IsSome || result.PadToDesiredResolution ||
NeedToNormalizeVideoCodec(ffmpegProfile, playoutItem.MediaItem.Statistics)) NeedToNormalizeVideoCodec(ffmpegProfile, version))
{ {
result.VideoCodec = ffmpegProfile.VideoCodec; result.VideoCodec = ffmpegProfile.VideoCodec;
result.VideoBitrate = ffmpegProfile.VideoBitrate; result.VideoBitrate = ffmpegProfile.VideoBitrate;
@ -94,7 +94,7 @@ namespace ErsatzTV.Core.FFmpeg
result.VideoCodec = "copy"; result.VideoCodec = "copy";
} }
if (NeedToNormalizeAudioCodec(ffmpegProfile, playoutItem.MediaItem.Statistics)) if (NeedToNormalizeAudioCodec(ffmpegProfile, version))
{ {
result.AudioCodec = ffmpegProfile.AudioCodec; result.AudioCodec = ffmpegProfile.AudioCodec;
result.AudioBitrate = ffmpegProfile.AudioBitrate; result.AudioBitrate = ffmpegProfile.AudioBitrate;
@ -104,7 +104,7 @@ namespace ErsatzTV.Core.FFmpeg
{ {
result.AudioChannels = ffmpegProfile.AudioChannels; result.AudioChannels = ffmpegProfile.AudioChannels;
result.AudioSampleRate = ffmpegProfile.AudioSampleRate; result.AudioSampleRate = ffmpegProfile.AudioSampleRate;
result.AudioDuration = playoutItem.MediaItem.Statistics.Duration; result.AudioDuration = version.Duration;
} }
} }
else else
@ -112,7 +112,7 @@ namespace ErsatzTV.Core.FFmpeg
result.AudioCodec = "copy"; result.AudioCodec = "copy";
} }
if (playoutItem.MediaItem.Statistics.VideoScanType == VideoScanType.Interlaced) if (version.VideoScanKind == VideoScanKind.Interlaced)
{ {
result.Deinterlace = true; result.Deinterlace = true;
} }
@ -132,35 +132,35 @@ namespace ErsatzTV.Core.FFmpeg
AudioCodec = ffmpegProfile.AudioCodec AudioCodec = ffmpegProfile.AudioCodec
}; };
private static bool NeedToScale(FFmpegProfile ffmpegProfile, MediaItemStatistics statistics) => private static bool NeedToScale(FFmpegProfile ffmpegProfile, MediaVersion version) =>
ffmpegProfile.NormalizeResolution && ffmpegProfile.NormalizeResolution &&
IsIncorrectSize(ffmpegProfile.Resolution, statistics) || IsIncorrectSize(ffmpegProfile.Resolution, version) ||
IsTooLarge(ffmpegProfile.Resolution, statistics) || IsTooLarge(ffmpegProfile.Resolution, version) ||
IsOddSize(statistics); IsOddSize(version);
private static bool IsIncorrectSize(IDisplaySize desiredResolution, MediaItemStatistics statistics) => private static bool IsIncorrectSize(IDisplaySize desiredResolution, MediaVersion version) =>
IsAnamorphic(statistics) || IsAnamorphic(version) ||
statistics.Width != desiredResolution.Width || version.Width != desiredResolution.Width ||
statistics.Height != desiredResolution.Height; version.Height != desiredResolution.Height;
private static bool IsTooLarge(IDisplaySize desiredResolution, IDisplaySize mediaSize) => private static bool IsTooLarge(IDisplaySize desiredResolution, IDisplaySize displaySize) =>
mediaSize.Height > desiredResolution.Height || displaySize.Height > desiredResolution.Height ||
mediaSize.Width > desiredResolution.Width; displaySize.Width > desiredResolution.Width;
private static bool IsOddSize(IDisplaySize displaySize) => private static bool IsOddSize(MediaVersion version) =>
displaySize.Height % 2 == 1 || displaySize.Width % 2 == 1; version.Height % 2 == 1 || version.Width % 2 == 1;
private static bool NeedToNormalizeVideoCodec(FFmpegProfile ffmpegProfile, MediaItemStatistics statistics) => private static bool NeedToNormalizeVideoCodec(FFmpegProfile ffmpegProfile, MediaVersion version) =>
ffmpegProfile.NormalizeVideoCodec && ffmpegProfile.VideoCodec != statistics.VideoCodec; ffmpegProfile.NormalizeVideoCodec && ffmpegProfile.VideoCodec != version.VideoCodec;
private static bool NeedToNormalizeAudioCodec(FFmpegProfile ffmpegProfile, MediaItemStatistics statistics) => private static bool NeedToNormalizeAudioCodec(FFmpegProfile ffmpegProfile, MediaVersion version) =>
ffmpegProfile.NormalizeAudioCodec && ffmpegProfile.AudioCodec != statistics.AudioCodec; 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); IDisplaySize sarSize = SARSize(version);
int p = statistics.Width * sarSize.Width; int p = version.Width * sarSize.Width;
int q = statistics.Height * sarSize.Height; int q = version.Height * sarSize.Height;
int g = Gcd(q, p); int g = Gcd(q, p);
p = p / g; p = p / g;
q = q / g; q = q / g;
@ -194,29 +194,29 @@ namespace ErsatzTV.Core.FFmpeg
return a | b; 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; return false;
} }
if (statistics.SampleAspectRatio != "0:1") if (version.SampleAspectRatio != "0:1")
{ {
return true; return true;
} }
if (statistics.DisplayAspectRatio == "0:1") if (version.DisplayAspectRatio == "0:1")
{ {
return false; 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])); return new DisplaySize(int.Parse(split[0]), int.Parse(split[1]));
} }
} }

15
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -18,19 +18,28 @@ namespace ErsatzTV.Core.FFmpeg
PlayoutItem item, PlayoutItem item,
DateTimeOffset now) DateTimeOffset now)
{ {
MediaVersion version = item.MediaItem switch
{
Movie m => m.MediaVersions.Head(),
Episode e => e.MediaVersions.Head()
};
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings( FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings(
channel.StreamingMode, channel.StreamingMode,
channel.FFmpegProfile, channel.FFmpegProfile,
item, version,
item.StartOffset,
now); now);
MediaFile file = version.MediaFiles.Head();
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath) FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath)
.WithThreads(playbackSettings.ThreadCount) .WithThreads(playbackSettings.ThreadCount)
.WithQuiet() .WithQuiet()
.WithFormatFlags(playbackSettings.FormatFlags) .WithFormatFlags(playbackSettings.FormatFlags)
.WithRealtimeOutput(playbackSettings.RealtimeOutput) .WithRealtimeOutput(playbackSettings.RealtimeOutput)
.WithSeek(playbackSettings.StreamSeek) .WithSeek(playbackSettings.StreamSeek)
.WithInput(item.MediaItem.Path); .WithInput(file.Path);
playbackSettings.ScaledSize.Match( playbackSettings.ScaledSize.Match(
scaledSize => scaledSize =>
@ -76,7 +85,7 @@ namespace ErsatzTV.Core.FFmpeg
return builder.WithPlaybackArgs(playbackSettings) return builder.WithPlaybackArgs(playbackSettings)
.WithMetadata(channel) .WithMetadata(channel)
.WithFormat("mpegts") .WithFormat("mpegts")
.WithDuration(item.Start + item.MediaItem.Statistics.Duration - now) .WithDuration(item.Start + version.Duration - now)
.WithPipe() .WithPipe()
.Build(); .Build();
} }

4
ErsatzTV.Core/Iptv/ChannelGuide.cs

@ -60,8 +60,8 @@ namespace ErsatzTV.Core.Iptv
string title = playoutItem.MediaItem switch string title = playoutItem.MediaItem switch
{ {
Movie m => m.MovieMetadata.HeadOrNone().Match(mm => mm.Title, () => m.Path), Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]"),
Episode e => e.EpisodeMetadata.HeadOrNone().Match(em => em.Title, () => e.Path), Episode e => e.EpisodeMetadata.HeadOrNone().Map(em => em.Title).IfNone("[unknown episode]"),
_ => "[unknown]" _ => "[unknown]"
}; };

10
ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs

@ -19,16 +19,18 @@ namespace ErsatzTV.Core.Metadata
public static Tuple<EpisodeMetadata, int> GetFallbackMetadata(Episode episode) public static Tuple<EpisodeMetadata, int> 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 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); return fileName != null ? GetEpisodeMetadata(fileName, metadata) : Tuple(metadata, 0);
} }
public static MovieMetadata GetFallbackMetadata(Movie movie) public static MovieMetadata GetFallbackMetadata(Movie movie)
{ {
string fileName = Path.GetFileName(movie.Path); string path = movie.MediaVersions.Head().MediaFiles.Head().Path;
var metadata = new MovieMetadata { MetadataKind = MetadataKind.Fallback, Title = fileName ?? movie.Path }; string fileName = Path.GetFileName(path);
var metadata = new MovieMetadata { MetadataKind = MetadataKind.Fallback, Title = fileName ?? path };
return fileName != null ? GetMovieMetadata(fileName, metadata) : metadata; return fileName != null ? GetMovieMetadata(fileName, metadata) : metadata;
} }

14
ErsatzTV.Core/Metadata/LocalFolderScanner.cs

@ -68,11 +68,17 @@ namespace ErsatzTV.Core.Metadata
{ {
try try
{ {
if (mediaItem.Statistics is null || MediaVersion version = mediaItem switch
(mediaItem.Statistics.LastWriteTime ?? DateTime.MinValue) <
_localFileSystem.GetLastWriteTime(mediaItem.Path))
{ {
_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); await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem);
} }

85
ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs

@ -33,42 +33,47 @@ namespace ErsatzTV.Core.Metadata
{ {
try try
{ {
FFprobe ffprobe = await GetProbeOutput(ffprobePath, mediaItem); string filePath = mediaItem switch
MediaItemStatistics statistics = ProjectToMediaItemStatistics(ffprobe); {
return await ApplyStatisticsUpdate(mediaItem, statistics); 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) 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; return false;
} }
} }
private async Task<bool> ApplyStatisticsUpdate( private async Task<bool> ApplyVersionUpdate(MediaItem mediaItem, MediaVersion version, string filePath)
MediaItem mediaItem,
MediaItemStatistics statistics)
{ {
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); mediaItemVersion.DateUpdated = _localFileSystem.GetLastWriteTime(filePath);
mediaItem.Statistics.Duration = statistics.Duration; mediaItemVersion.Duration = version.Duration;
mediaItem.Statistics.AudioCodec = statistics.AudioCodec; mediaItemVersion.AudioCodec = version.AudioCodec;
mediaItem.Statistics.SampleAspectRatio = statistics.SampleAspectRatio; mediaItemVersion.SampleAspectRatio = version.SampleAspectRatio;
mediaItem.Statistics.DisplayAspectRatio = statistics.DisplayAspectRatio; mediaItemVersion.DisplayAspectRatio = version.DisplayAspectRatio;
mediaItem.Statistics.Width = statistics.Width; mediaItemVersion.Width = version.Width;
mediaItem.Statistics.Height = statistics.Height; mediaItemVersion.Height = version.Height;
mediaItem.Statistics.VideoCodec = statistics.VideoCodec; mediaItemVersion.VideoCodec = version.VideoCodec;
mediaItem.Statistics.VideoScanType = statistics.VideoScanType; mediaItemVersion.VideoScanKind = version.VideoScanKind;
return await _mediaItemRepository.Update(mediaItem) && durationChange; return await _mediaItemRepository.Update(mediaItem) && durationChange;
} }
private Task<FFprobe> GetProbeOutput(string ffprobePath, MediaItem mediaItem) private Task<FFprobe> GetProbeOutput(string ffprobePath, string filePath)
{ {
var startInfo = new ProcessStartInfo var startInfo = new ProcessStartInfo
{ {
@ -85,7 +90,7 @@ namespace ErsatzTV.Core.Metadata
startInfo.ArgumentList.Add("-show_format"); startInfo.ArgumentList.Add("-show_format");
startInfo.ArgumentList.Add("-show_streams"); startInfo.ArgumentList.Add("-show_streams");
startInfo.ArgumentList.Add("-i"); startInfo.ArgumentList.Add("-i");
startInfo.ArgumentList.Add(mediaItem.Path); startInfo.ArgumentList.Add(filePath);
var probe = new Process var probe = new Process
{ {
@ -101,7 +106,7 @@ namespace ErsatzTV.Core.Metadata
}); });
} }
private MediaItemStatistics ProjectToMediaItemStatistics(FFprobe probeOutput) => private MediaVersion ProjectToMediaVersion(FFprobe probeOutput) =>
Optional(probeOutput) Optional(probeOutput)
.Filter(json => json?.format != null && json.streams != null) .Filter(json => json?.format != null && json.streams != null)
.ToValidation<BaseError>("Unable to parse ffprobe output") .ToValidation<BaseError>("Unable to parse ffprobe output")
@ -111,41 +116,35 @@ namespace ErsatzTV.Core.Metadata
{ {
var duration = TimeSpan.FromSeconds(double.Parse(json.format.duration)); 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"); FFprobeStream audioStream = json.streams.FirstOrDefault(s => s.codec_type == "audio");
if (audioStream != null) if (audioStream != null)
{ {
statistics = statistics with version.AudioCodec = audioStream.codec_name;
{
AudioCodec = audioStream.codec_name
};
} }
FFprobeStream videoStream = json.streams.FirstOrDefault(s => s.codec_type == "video"); FFprobeStream videoStream = json.streams.FirstOrDefault(s => s.codec_type == "video");
if (videoStream != null) if (videoStream != null)
{ {
statistics = statistics with version.SampleAspectRatio = videoStream.sample_aspect_ratio;
{ version.DisplayAspectRatio = videoStream.display_aspect_ratio;
SampleAspectRatio = videoStream.sample_aspect_ratio, version.Width = videoStream.width;
DisplayAspectRatio = videoStream.display_aspect_ratio, version.Height = videoStream.height;
Width = videoStream.width, version.VideoCodec = videoStream.codec_name;
Height = videoStream.height, version.VideoScanKind = ScanKindFromFieldOrder(videoStream.field_order);
VideoCodec = videoStream.codec_name,
VideoScanType = ScanTypeFromFieldOrder(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 fieldOrder?.ToLowerInvariant() switch
{ {
var x when x == "tt" || x == "bb" || x == "tb" || x == "bt" => VideoScanType.Interlaced, var x when x == "tt" || x == "bb" || x == "tb" || x == "bt" => VideoScanKind.Interlaced,
"progressive" => VideoScanType.Progressive, "progressive" => VideoScanKind.Progressive,
_ => VideoScanType.Unknown _ => VideoScanKind.Unknown
}; };
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming

13
ErsatzTV.Core/Metadata/MovieFolderScanner.cs

@ -111,7 +111,8 @@ namespace ErsatzTV.Core.Metadata
{ {
if (!Optional(movie.MovieMetadata).Flatten().Any()) 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); await _localMetadataProvider.RefreshFallbackMetadata(movie);
} }
}); });
@ -148,8 +149,9 @@ namespace ErsatzTV.Core.Metadata
private Option<string> LocateNfoFile(Movie movie) private Option<string> LocateNfoFile(Movie movie)
{ {
string movieAsNfo = Path.ChangeExtension(movie.Path, "nfo"); string path = movie.MediaVersions.Head().MediaFiles.Head().Path;
string movieNfo = Path.Combine(Path.GetDirectoryName(movie.Path) ?? string.Empty, "movie.nfo"); string movieAsNfo = Path.ChangeExtension(path, "nfo");
string movieNfo = Path.Combine(Path.GetDirectoryName(path) ?? string.Empty, "movie.nfo");
return Seq.create(movieAsNfo, movieNfo) return Seq.create(movieAsNfo, movieNfo)
.Filter(s => _localFileSystem.FileExists(s)) .Filter(s => _localFileSystem.FileExists(s))
.HeadOrNone(); .HeadOrNone();
@ -157,9 +159,10 @@ namespace ErsatzTV.Core.Metadata
private Option<string> LocatePoster(Movie movie) private Option<string> 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<string> possibleMoviePosters = ImageFileExtensions.Collect( IEnumerable<string> 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)); .Map(f => Path.Combine(folder, f));
Option<string> result = possibleMoviePosters.Filter(p => _localFileSystem.FileExists(p)).HeadOrNone(); Option<string> result = possibleMoviePosters.Filter(p => _localFileSystem.FileExists(p)).HeadOrNone();
return result; return result;

15
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

@ -188,7 +188,8 @@ namespace ErsatzTV.Core.Metadata
{ {
if (!Optional(episode.EpisodeMetadata).Flatten().Any()) 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); await _localMetadataProvider.RefreshFallbackMetadata(episode);
} }
}); });
@ -280,9 +281,12 @@ namespace ErsatzTV.Core.Metadata
Optional(Path.Combine(showFolder, "tvshow.nfo")) Optional(Path.Combine(showFolder, "tvshow.nfo"))
.Filter(s => _localFileSystem.FileExists(s)); .Filter(s => _localFileSystem.FileExists(s));
private Option<string> LocateNfoFile(Episode episode) => private Option<string> LocateNfoFile(Episode episode)
Optional(Path.ChangeExtension(episode.Path, "nfo")) {
string path = episode.MediaVersions.Head().MediaFiles.Head().Path;
return Optional(Path.ChangeExtension(path, "nfo"))
.Filter(s => _localFileSystem.FileExists(s)); .Filter(s => _localFileSystem.FileExists(s));
}
private Option<string> LocatePosterForShow(string showFolder) => private Option<string> LocatePosterForShow(string showFolder) =>
ImageFileExtensions ImageFileExtensions
@ -302,9 +306,10 @@ namespace ErsatzTV.Core.Metadata
private Option<string> LocateThumbnail(Episode episode) private Option<string> 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 return ImageFileExtensions
.Map(ext => Path.GetFileNameWithoutExtension(episode.Path) + $"-thumb.{ext}") .Map(ext => Path.GetFileNameWithoutExtension(path) + $"-thumb.{ext}")
.Map(f => Path.Combine(folder, f)) .Map(f => Path.Combine(folder, f))
.Filter(f => _localFileSystem.FileExists(f)) .Filter(f => _localFileSystem.FileExists(f))
.HeadOrNone(); .HeadOrNone();

31
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
@ -145,14 +144,20 @@ namespace ErsatzTV.Core.Scheduling
DisplayTitle(mediaItem), DisplayTitle(mediaItem),
itemStartTime); itemStartTime);
MediaVersion version = mediaItem switch
{
Movie m => m.MediaVersions.Head(),
Episode e => e.MediaVersions.Head()
};
var playoutItem = new PlayoutItem var playoutItem = new PlayoutItem
{ {
MediaItemId = mediaItem.Id, MediaItemId = mediaItem.Id,
Start = itemStartTime.UtcDateTime, 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(); enumerator.MoveNext();
playout.Items.Add(playoutItem); playout.Items.Add(playoutItem);
@ -187,6 +192,12 @@ namespace ErsatzTV.Core.Scheduling
enumerator.Current.Do( enumerator.Current.Do(
peekMediaItem => peekMediaItem =>
{ {
MediaVersion peekVersion = peekMediaItem switch
{
Movie m => m.MediaVersions.Head(),
Episode e => e.MediaVersions.Head()
};
ProgramScheduleItem peekScheduleItem = ProgramScheduleItem peekScheduleItem =
sortedScheduleItems[(index + 1) % sortedScheduleItems.Count]; sortedScheduleItems[(index + 1) % sortedScheduleItems.Count];
DateTimeOffset peekScheduleItemStart = DateTimeOffset peekScheduleItemStart =
@ -198,7 +209,7 @@ namespace ErsatzTV.Core.Scheduling
// is after, we need to move on to the next schedule item // is after, we need to move on to the next schedule item
// eventually, spots probably have to fit in this gap // eventually, spots probably have to fit in this gap
bool willNotFinishInTime = currentTime <= peekScheduleItemStart && bool willNotFinishInTime = currentTime <= peekScheduleItemStart &&
currentTime + peekMediaItem.Statistics.Duration > currentTime + peekVersion.Duration >
peekScheduleItemStart; peekScheduleItemStart;
if (willNotFinishInTime) if (willNotFinishInTime)
{ {
@ -213,6 +224,12 @@ namespace ErsatzTV.Core.Scheduling
enumerator.Current.Do( enumerator.Current.Do(
peekMediaItem => peekMediaItem =>
{ {
MediaVersion peekVersion = peekMediaItem switch
{
Movie m => m.MediaVersions.Head(),
Episode e => e.MediaVersions.Head()
};
// remember when we need to finish this duration item // remember when we need to finish this duration item
if (durationFinish.IsNone) if (durationFinish.IsNone)
{ {
@ -221,7 +238,7 @@ namespace ErsatzTV.Core.Scheduling
bool willNotFinishInTime = bool willNotFinishInTime =
currentTime <= durationFinish.IfNone(DateTime.MinValue) && currentTime <= durationFinish.IfNone(DateTime.MinValue) &&
currentTime + peekMediaItem.Statistics.Duration > currentTime + peekVersion.Duration >
durationFinish.IfNone(DateTime.MinValue); durationFinish.IfNone(DateTime.MinValue);
if (willNotFinishInTime) if (willNotFinishInTime)
{ {
@ -391,10 +408,10 @@ namespace ErsatzTV.Core.Scheduling
{ {
Episode e => e.EpisodeMetadata.Any() && e.Season != null Episode e => e.EpisodeMetadata.Any() && e.Season != null
? $"{e.EpisodeMetadata.Head().Title} - s{e.Season.SeasonNumber:00}e{e.EpisodeNumber:00}" ? $"{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( Movie m => m.MovieMetadata.HeadOrNone().Match(
mm => mm.Title ?? string.Empty, mm => mm.Title ?? string.Empty,
() => Path.GetFileName(m.Path)), () => "[unknown movie]"),
_ => string.Empty _ => string.Empty
}; };

14
ErsatzTV.Infrastructure/Data/Configurations/MediaItem/MediaItemConfiguration.cs

@ -6,14 +6,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
{ {
public class MediaItemConfiguration : IEntityTypeConfiguration<MediaItem> public class MediaItemConfiguration : IEntityTypeConfiguration<MediaItem>
{ {
public void Configure(EntityTypeBuilder<MediaItem> builder) public void Configure(EntityTypeBuilder<MediaItem> builder) => builder.ToTable("MediaItem");
{ // builder.OwnsOne(c => c.Statistics).WithOwner();
builder.ToTable("MediaItem"); //
// builder.HasIndex(i => i.Path)
builder.OwnsOne(c => c.Statistics).WithOwner(); // .IsUnique();
builder.HasIndex(i => i.Path)
.IsUnique();
}
} }
} }

20
ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs

@ -46,8 +46,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Include(i => i.MovieMetadata) .Include(i => i.MovieMetadata)
.ThenInclude(mm => mm.Artwork) .ThenInclude(mm => mm.Artwork)
.Include(i => i.LibraryPath) .Include(i => i.LibraryPath)
.OrderBy(i => i.Path) .Include(i => i.MediaVersions)
.SingleOrDefaultAsync(i => i.Path == path); .OrderBy(i => i.MediaVersions.First().MediaFiles.First().Path)
.SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path);
return await maybeExisting.Match( return await maybeExisting.Match(
mediaItem => Right<BaseError, Movie>(mediaItem).AsTask(), mediaItem => Right<BaseError, Movie>(mediaItem).AsTask(),
@ -94,7 +95,20 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{ {
try try
{ {
var movie = new Movie { LibraryPathId = libraryPathId, Path = path }; var movie = new Movie
{
LibraryPathId = libraryPathId,
MediaVersions = new List<MediaVersion>
{
new()
{
MediaFiles = new List<MediaFile>
{
new() { Path = path }
}
}
}
};
await _dbContext.Movies.AddAsync(movie); await _dbContext.Movies.AddAsync(movie);
await _dbContext.SaveChangesAsync(); await _dbContext.SaveChangesAsync();
await _dbContext.Entry(movie).Reference(m => m.LibraryPath).LoadAsync(); await _dbContext.Entry(movie).Reference(m => m.LibraryPath).LoadAsync();

20
ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs

@ -219,9 +219,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
Option<Episode> maybeExisting = await _dbContext.Episodes Option<Episode> maybeExisting = await _dbContext.Episodes
.Include(i => i.EpisodeMetadata) .Include(i => i.EpisodeMetadata)
.ThenInclude(em => em.Artwork) .ThenInclude(em => em.Artwork)
.Include(i => i.Statistics) .Include(i => i.MediaVersions)
.OrderBy(i => i.Path) .OrderBy(i => i.MediaVersions.First().MediaFiles.First().Path)
.SingleOrDefaultAsync(i => i.Path == path); .SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path);
return await maybeExisting.Match( return await maybeExisting.Match(
episode => Right<BaseError, Episode>(episode).AsTask(), episode => Right<BaseError, Episode>(episode).AsTask(),
@ -268,7 +268,6 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
var season = new Season var season = new Season
{ {
ShowId = show.Id, ShowId = show.Id,
Path = path,
SeasonNumber = seasonNumber, SeasonNumber = seasonNumber,
Episodes = new List<Episode>(), Episodes = new List<Episode>(),
SeasonMetadata = new List<SeasonMetadata>() SeasonMetadata = new List<SeasonMetadata>()
@ -291,8 +290,17 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{ {
LibraryPathId = libraryPathId, LibraryPathId = libraryPathId,
SeasonId = season.Id, SeasonId = season.Id,
Path = path, EpisodeMetadata = new List<EpisodeMetadata>(),
EpisodeMetadata = new List<EpisodeMetadata>() MediaVersions = new List<MediaVersion>
{
new()
{
MediaFiles = new List<MediaFile>
{
new() { Path = path }
}
}
}
}; };
await _dbContext.Episodes.AddAsync(episode); await _dbContext.Episodes.AddAsync(episode);
await _dbContext.SaveChangesAsync(); await _dbContext.SaveChangesAsync();

2
ErsatzTV.Infrastructure/Data/TvContext.cs

@ -22,6 +22,8 @@ namespace ErsatzTV.Infrastructure.Data
public DbSet<LibraryPath> LibraryPaths { get; set; } public DbSet<LibraryPath> LibraryPaths { get; set; }
public DbSet<PlexLibrary> PlexLibraries { get; set; } public DbSet<PlexLibrary> PlexLibraries { get; set; }
public DbSet<MediaItem> MediaItems { get; set; } public DbSet<MediaItem> MediaItems { get; set; }
public DbSet<MediaVersion> MediaVersions { get; set; }
public DbSet<MediaFile> MediaFiles { get; set; }
public DbSet<Movie> Movies { get; set; } public DbSet<Movie> Movies { get; set; }
public DbSet<MovieMetadata> MovieMetadata { get; set; } public DbSet<MovieMetadata> MovieMetadata { get; set; }
public DbSet<Show> Shows { get; set; } public DbSet<Show> Shows { get; set; }

1509
ErsatzTV.Infrastructure/Migrations/20210228021437_Add_MediaVersionVideoScanKind.Designer.cs generated

File diff suppressed because it is too large Load Diff

19
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");
}
}

1509
ErsatzTV.Infrastructure/Migrations/20210228021822_Update_MediaVersion.Designer.cs generated

File diff suppressed because it is too large Load Diff

42
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)
{
}
}
}

6
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -410,9 +410,6 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<int>("Height") b.Property<int>("Height")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<bool>("IsInterlaced")
.HasColumnType("INTEGER");
b.Property<int?>("MovieId") b.Property<int?>("MovieId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -425,6 +422,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<string>("VideoCodec") b.Property<string>("VideoCodec")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int>("VideoScanKind")
.HasColumnType("INTEGER");
b.Property<int>("Width") b.Property<int>("Width")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

21
ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs

@ -141,16 +141,17 @@ namespace ErsatzTV.Infrastructure.Plex
Key = response.Key, Key = response.Key,
LastWriteTime = lastWriteTime, LastWriteTime = lastWriteTime,
MovieMetadata = new List<MovieMetadata> { metadata }, MovieMetadata = new List<MovieMetadata> { metadata },
Statistics = new MediaItemStatistics // TODO: versions
{ // Statistics = new MediaItemStatistics
Duration = TimeSpan.FromMilliseconds(media.Duration), // {
Width = media.Width, // Duration = TimeSpan.FromMilliseconds(media.Duration),
Height = media.Height, // Width = media.Width,
// TODO: aspect ratio // Height = media.Height,
AudioCodec = media.AudioCodec, // // TODO: aspect ratio
VideoCodec = media.VideoCodec, // AudioCodec = media.AudioCodec,
LastWriteTime = lastWriteTime // VideoCodec = media.VideoCodec,
}, // LastWriteTime = lastWriteTime
// },
// TODO: multi-part movies? // TODO: multi-part movies?
Part = new PlexMediaItemPart Part = new PlexMediaItemPart
{ {

Loading…
Cancel
Save