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 @@ @@ -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 @@ -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 @@ -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) =>

2
ErsatzTV.Application/MediaItems/MediaItemViewModel.cs

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
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 @@ @@ -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 @@ -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);
}
}
}

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

@ -15,15 +15,6 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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);

26
ErsatzTV.Core.Tests/Fakes/FakeMovieWithPath.cs

@ -0,0 +1,26 @@ @@ -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 @@ @@ -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 @@ -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<MediaVersion>
{
new()
{
MediaFiles = new List<MediaFile>
{
new() { Path = path }
}
}
}
});
metadata.Title.Should().Be(title);
// 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 @@ -43,7 +43,7 @@ namespace ErsatzTV.Core.Tests.Metadata
_movieRepository = new Mock<IMovieRepository>();
_movieRepository.Setup(x => x.GetOrAdd(It.IsAny<LibraryPath>(), It.IsAny<string>()))
.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>();
_localMetadataProvider = new Mock<ILocalMetadataProvider>();
@ -92,7 +92,7 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -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 @@ -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<MediaItem>(i => i.Path == moviePath)),
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once);
_localMetadataProvider.Verify(
x => x.RefreshFallbackMetadata(It.Is<MediaItem>(i => i.Path == moviePath)),
x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once);
}
@ -124,7 +124,7 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -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 @@ -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<MediaItem>(i => i.Path == moviePath)),
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once);
_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);
}
@ -157,7 +157,7 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -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 @@ -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<MediaItem>(i => i.Path == moviePath)),
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once);
_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);
}
@ -194,7 +194,7 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -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 @@ -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<MediaItem>(i => i.Path == moviePath)),
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once);
_localMetadataProvider.Verify(
x => x.RefreshFallbackMetadata(It.Is<MediaItem>(i => i.Path == moviePath)),
x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once);
_imageCache.Verify(
@ -235,7 +235,7 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -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 @@ -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<MediaItem>(i => i.Path == moviePath)),
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once);
_localMetadataProvider.Verify(
x => x.RefreshFallbackMetadata(It.Is<MediaItem>(i => i.Path == moviePath)),
x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once);
_imageCache.Verify(
@ -272,7 +272,7 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -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 @@ -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<MediaItem>(i => i.Path == moviePath)),
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once);
_localMetadataProvider.Verify(
x => x.RefreshFallbackMetadata(It.Is<MediaItem>(i => i.Path == moviePath)),
x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once);
}
@ -308,7 +308,7 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -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 @@ -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<MediaItem>(i => i.Path == moviePath)),
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once);
_localMetadataProvider.Verify(
x => x.RefreshFallbackMetadata(It.Is<MediaItem>(i => i.Path == moviePath)),
x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once);
}
@ -342,7 +342,7 @@ namespace ErsatzTV.Core.Tests.Metadata @@ -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 @@ -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<MediaItem>(i => i.Path == moviePath)),
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once);
_localMetadataProvider.Verify(
x => x.RefreshFallbackMetadata(It.Is<MediaItem>(i => i.Path == moviePath)),
x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
Times.Once);
}

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

@ -612,7 +612,10 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -612,7 +612,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
{
Id = id,
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)

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

@ -6,7 +6,8 @@ namespace ErsatzTV.Core.Domain @@ -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 @@ -18,7 +19,8 @@ namespace ErsatzTV.Core.Domain
public int TelevisionEpisodeId { get; set; }
public List<Collection> Collections { 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 @@ -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; }
}

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

@ -1,9 +1,10 @@ @@ -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 @@ -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; }
}

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

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

82
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs

@ -44,7 +44,8 @@ namespace ErsatzTV.Core.FFmpeg @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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]));
}
}

15
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -18,19 +18,28 @@ namespace ErsatzTV.Core.FFmpeg @@ -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 @@ -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();
}

4
ErsatzTV.Core/Iptv/ChannelGuide.cs

@ -60,8 +60,8 @@ namespace ErsatzTV.Core.Iptv @@ -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]"
};

10
ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs

@ -19,16 +19,18 @@ namespace ErsatzTV.Core.Metadata @@ -19,16 +19,18 @@ namespace ErsatzTV.Core.Metadata
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
{ 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;
}

14
ErsatzTV.Core/Metadata/LocalFolderScanner.cs

@ -68,11 +68,17 @@ namespace ErsatzTV.Core.Metadata @@ -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);
}

85
ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs

@ -33,42 +33,47 @@ namespace ErsatzTV.Core.Metadata @@ -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<bool> ApplyStatisticsUpdate(
MediaItem mediaItem,
MediaItemStatistics statistics)
private async Task<bool> 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<FFprobe> GetProbeOutput(string ffprobePath, MediaItem mediaItem)
private Task<FFprobe> GetProbeOutput(string ffprobePath, string filePath)
{
var startInfo = new ProcessStartInfo
{
@ -85,7 +90,7 @@ namespace ErsatzTV.Core.Metadata @@ -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 @@ -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<BaseError>("Unable to parse ffprobe output")
@ -111,41 +116,35 @@ namespace ErsatzTV.Core.Metadata @@ -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

13
ErsatzTV.Core/Metadata/MovieFolderScanner.cs

@ -111,7 +111,8 @@ namespace ErsatzTV.Core.Metadata @@ -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 @@ -148,8 +149,9 @@ namespace ErsatzTV.Core.Metadata
private Option<string> 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 @@ -157,9 +159,10 @@ namespace ErsatzTV.Core.Metadata
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(
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<string> result = possibleMoviePosters.Filter(p => _localFileSystem.FileExists(p)).HeadOrNone();
return result;

15
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

@ -188,7 +188,8 @@ namespace ErsatzTV.Core.Metadata @@ -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 @@ -280,9 +281,12 @@ namespace ErsatzTV.Core.Metadata
Optional(Path.Combine(showFolder, "tvshow.nfo"))
.Filter(s => _localFileSystem.FileExists(s));
private Option<string> LocateNfoFile(Episode episode) =>
Optional(Path.ChangeExtension(episode.Path, "nfo"))
private Option<string> LocateNfoFile(Episode episode)
{
string path = episode.MediaVersions.Head().MediaFiles.Head().Path;
return Optional(Path.ChangeExtension(path, "nfo"))
.Filter(s => _localFileSystem.FileExists(s));
}
private Option<string> LocatePosterForShow(string showFolder) =>
ImageFileExtensions
@ -302,9 +306,10 @@ namespace ErsatzTV.Core.Metadata @@ -302,9 +306,10 @@ namespace ErsatzTV.Core.Metadata
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
.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();

31
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -1,6 +1,5 @@ @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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
};

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

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

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

@ -46,8 +46,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -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<BaseError, Movie>(mediaItem).AsTask(),
@ -94,7 +95,20 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -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<MediaVersion>
{
new()
{
MediaFiles = new List<MediaFile>
{
new() { Path = path }
}
}
}
};
await _dbContext.Movies.AddAsync(movie);
await _dbContext.SaveChangesAsync();
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 @@ -219,9 +219,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
Option<Episode> 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<BaseError, Episode>(episode).AsTask(),
@ -268,7 +268,6 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -268,7 +268,6 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
var season = new Season
{
ShowId = show.Id,
Path = path,
SeasonNumber = seasonNumber,
Episodes = new List<Episode>(),
SeasonMetadata = new List<SeasonMetadata>()
@ -291,8 +290,17 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -291,8 +290,17 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{
LibraryPathId = libraryPathId,
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.SaveChangesAsync();

2
ErsatzTV.Infrastructure/Data/TvContext.cs

@ -22,6 +22,8 @@ namespace ErsatzTV.Infrastructure.Data @@ -22,6 +22,8 @@ namespace ErsatzTV.Infrastructure.Data
public DbSet<LibraryPath> LibraryPaths { get; set; }
public DbSet<PlexLibrary> PlexLibraries { 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<MovieMetadata> MovieMetadata { 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 @@ @@ -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 @@ @@ -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 @@ -410,9 +410,6 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<int>("Height")
.HasColumnType("INTEGER");
b.Property<bool>("IsInterlaced")
.HasColumnType("INTEGER");
b.Property<int?>("MovieId")
.HasColumnType("INTEGER");
@ -425,6 +422,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -425,6 +422,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<string>("VideoCodec")
.HasColumnType("TEXT");
b.Property<int>("VideoScanKind")
.HasColumnType("INTEGER");
b.Property<int>("Width")
.HasColumnType("INTEGER");

21
ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs

@ -141,16 +141,17 @@ namespace ErsatzTV.Infrastructure.Plex @@ -141,16 +141,17 @@ namespace ErsatzTV.Infrastructure.Plex
Key = response.Key,
LastWriteTime = lastWriteTime,
MovieMetadata = new List<MovieMetadata> { 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
{

Loading…
Cancel
Save