diff --git a/ErsatzTV.Application/Emby/Commands/CallEmbyLibraryScannerHandler.cs b/ErsatzTV.Application/Emby/Commands/CallEmbyLibraryScannerHandler.cs index 0f6d2722d..3494537fb 100644 --- a/ErsatzTV.Application/Emby/Commands/CallEmbyLibraryScannerHandler.cs +++ b/ErsatzTV.Application/Emby/Commands/CallEmbyLibraryScannerHandler.cs @@ -42,8 +42,7 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler, { var arguments = new List { - "--emby", - request.EmbyLibraryId.ToString() + "scan-emby", request.EmbyLibraryId.ToString() }; if (request.ForceScan) diff --git a/ErsatzTV.Application/Jellyfin/Commands/CallJellyfinLibraryScannerHandler.cs b/ErsatzTV.Application/Jellyfin/Commands/CallJellyfinLibraryScannerHandler.cs index a18554909..e676d2593 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/CallJellyfinLibraryScannerHandler.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/CallJellyfinLibraryScannerHandler.cs @@ -42,8 +42,7 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler, { var arguments = new List { - "--jellyfin", - request.JellyfinLibraryId.ToString() + "scan-jellyfin", request.JellyfinLibraryId.ToString() }; if (request.ForceScan) diff --git a/ErsatzTV.Application/MediaSources/Commands/CallLocalLibraryScannerHandler.cs b/ErsatzTV.Application/MediaSources/Commands/CallLocalLibraryScannerHandler.cs index 2b36c3688..30a4345e0 100644 --- a/ErsatzTV.Application/MediaSources/Commands/CallLocalLibraryScannerHandler.cs +++ b/ErsatzTV.Application/MediaSources/Commands/CallLocalLibraryScannerHandler.cs @@ -40,8 +40,7 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler, { var arguments = new List { - "--local", - request.LibraryId.ToString() + "scan-local", request.LibraryId.ToString() }; if (request.ForceScan) diff --git a/ErsatzTV.Application/MediaSources/LocalMediaSourceViewModel.cs b/ErsatzTV.Application/MediaSources/LocalMediaSourceViewModel.cs deleted file mode 100644 index f280086e4..000000000 --- a/ErsatzTV.Application/MediaSources/LocalMediaSourceViewModel.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace ErsatzTV.Application.MediaSources; - -public record LocalMediaSourceViewModel(int Id) : MediaSourceViewModel(Id, "Local"); diff --git a/ErsatzTV.Application/Plex/Commands/CallPlexLibraryScannerHandler.cs b/ErsatzTV.Application/Plex/Commands/CallPlexLibraryScannerHandler.cs index 52197164c..127657ca5 100644 --- a/ErsatzTV.Application/Plex/Commands/CallPlexLibraryScannerHandler.cs +++ b/ErsatzTV.Application/Plex/Commands/CallPlexLibraryScannerHandler.cs @@ -42,8 +42,7 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler, { var arguments = new List { - "--plex", - request.PlexLibraryId.ToString() + "scan-plex", request.PlexLibraryId.ToString() }; if (request.ForceScan) diff --git a/ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs index 51052b1d5..28efe1353 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs @@ -30,7 +30,7 @@ public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler(ConfigElementKey.FFmpegSaveReports) .Map(result => result.IfNone(false)); - Command process = _ffmpegProcessService.WrapSegmenter( + Command process = await _ffmpegProcessService.WrapSegmenter( ffmpegPath, saveReports, channel, diff --git a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj index 3e936c19c..e7ee9d926 100644 --- a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj +++ b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj @@ -38,21 +38,12 @@ Always - - Always - - - Always - Always Always - - Always - diff --git a/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs b/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs deleted file mode 100644 index b0b1f7da3..000000000 --- a/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs +++ /dev/null @@ -1,777 +0,0 @@ -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.FFmpeg; -using ErsatzTV.FFmpeg.State; -using FluentAssertions; -using NUnit.Framework; - -namespace ErsatzTV.Core.Tests.FFmpeg; - -[TestFixture] -public class FFmpegComplexFilterBuilderTests -{ - [TestFixture] - public class Build - { - [Test] - public void Should_Return_None_With_No_Filters() - { - var builder = new FFmpegComplexFilterBuilder(); - - Option result = builder.Build(false, 0, 0, 0, 1, false); - - result.IsNone.Should().BeTrue(); - } - - [Test] - public void Should_Return_Audio_Filter_With_AudioDuration() - { - var duration = TimeSpan.FromMilliseconds(1000.1); - FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() - .WithAlignedAudio(duration); - - Option result = builder.Build(false, 0, 0, 0, 1, false); - - result.IsSome.Should().BeTrue(); - result.IfSome( - filter => - { - filter.ComplexFilter.Should().Be("[0:1]apad=whole_dur=1000.1ms[a]"); - filter.AudioLabel.Should().Be("[a]"); - filter.VideoLabel.Should().Be("0:0"); - }); - } - - [Test] - // this needs to be a culture where '.' is a group separator - [SetCulture("it-IT")] - public void Should_Return_Audio_Filter_With_AudioDuration_Decimal() - { - var duration = TimeSpan.FromMilliseconds(1000.1); - FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() - .WithAlignedAudio(duration); - - Option result = builder.Build(false, 0, 0, 0, 1, false); - - result.IsSome.Should().BeTrue(); - result.IfSome( - filter => - { - filter.ComplexFilter.Should().Be("[0:1]apad=whole_dur=1000.1ms[a]"); - filter.AudioLabel.Should().Be("[a]"); - filter.VideoLabel.Should().Be("0:0"); - }); - } - - [Test] - public void Should_Return_Audio_And_Video_Filter() - { - var duration = TimeSpan.FromMinutes(54); - FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() - .WithAlignedAudio(duration) - .WithDeinterlace(true); - - Option result = builder.Build(false, 0, 0, 0, 1, false); - - result.IsSome.Should().BeTrue(); - result.IfSome( - filter => - { - filter.ComplexFilter.Should().Be( - $"[0:1]apad=whole_dur={duration.TotalMilliseconds}ms[a];[0:0]yadif=1[v]"); - filter.AudioLabel.Should().Be("[a]"); - filter.VideoLabel.Should().Be("[v]"); - }); - } - - [Test] - [TestCase(true, false, false, "[0:0]yadif=1[v]", "[v]")] - [TestCase(true, true, false, "[0:0]yadif=1,scale=1920:1000:flags=fast_bilinear,setsar=1[v]", "[v]")] - [TestCase(true, false, true, "[0:0]yadif=1,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", "[v]")] - [TestCase( - true, - true, - true, - "[0:0]yadif=1,scale=1920:1000:flags=fast_bilinear,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", - "[v]")] - [TestCase(false, true, false, "[0:0]scale=1920:1000:flags=fast_bilinear,setsar=1[v]", "[v]")] - [TestCase(false, false, true, "[0:0]setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", "[v]")] - [TestCase( - false, - true, - true, - "[0:0]scale=1920:1000:flags=fast_bilinear,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", - "[v]")] - public void Should_Return_Software_Video_Filter( - bool deinterlace, - bool scale, - bool pad, - string expectedVideoFilter, - string expectedVideoLabel) - { - FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() - .WithDeinterlace(deinterlace); - - if (scale) - { - builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 }); - } - - if (pad) - { - builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); - } - - Option result = builder.Build(false, 0, 0, 0, 1, false); - - result.IsSome.Should().BeTrue(); - result.IfSome( - filter => - { - filter.ComplexFilter.Should().Be(expectedVideoFilter); - filter.AudioLabel.Should().Be("0:1"); - filter.VideoLabel.Should().Be(expectedVideoLabel); - }); - } - - [Test] - [TestCase( - false, - false, - false, - WatermarkLocation.BottomLeft, - false, - 100, - "[0:0][1:v]overlay=x=134:y=H-h-54[v]", - "0:1", - "[v]")] - [TestCase( - false, - false, - false, - WatermarkLocation.BottomRight, - false, - 100, - "[0:0][1:v]overlay=x=W-w-134:y=H-h-54[v]", - "0:1", - "[v]")] - [TestCase( - false, - false, - false, - WatermarkLocation.TopLeft, - false, - 100, - "[0:0][1:v]overlay=x=134:y=54[v]", - "0:1", - "[v]")] - [TestCase( - false, - false, - false, - WatermarkLocation.TopRight, - false, - 100, - "[0:0][1:v]overlay=x=W-w-134:y=54[v]", - "0:1", - "[v]")] - [TestCase( - false, - false, - true, - WatermarkLocation.TopLeft, - false, - 100, - "[1:v]format=yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)'[wmp];[0:0][wmp]overlay=x=134:y=54,format=nv12[v]", - "0:1", - "[v]")] - [TestCase( - false, - false, - false, - WatermarkLocation.TopLeft, - true, - 100, - "[1:v]scale=384:-1[wmp];[0:0][wmp]overlay=x=134:y=54[v]", - "0:1", - "[v]")] - [TestCase( - false, - false, - false, - WatermarkLocation.TopLeft, - false, - 90, - "[1:v]format=yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8,colorchannelmixer=aa=0.90[wmp];[0:0][wmp]overlay=x=134:y=54[v]", - "0:1", - "[v]")] - [TestCase( - false, - true, - false, - WatermarkLocation.TopLeft, - false, - 100, - "[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]", - "0:1", - "[v]")] - [TestCase( - false, - true, - false, - WatermarkLocation.TopLeft, - true, - 100, - "[0:0]yadif=1[vt];[1:v]scale=384:-1[wmp];[vt][wmp]overlay=x=134:y=54[v]", - "0:1", - "[v]")] - [TestCase( - true, - true, - false, - WatermarkLocation.TopLeft, - false, - 100, - "[0:1]apad=whole_dur=3300000ms[a];[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]", - "[a]", - "[v]")] - [TestCase( - true, - false, - false, - WatermarkLocation.TopLeft, - false, - 100, - "[0:1]apad=whole_dur=3300000ms[a];[0:0][1:v]overlay=x=134:y=54[v]", - "[a]", - "[v]")] - public void Should_Return_Watermark( - bool alignAudio, - bool deinterlace, - bool intermittent, - WatermarkLocation location, - bool scaled, - int opacity, - string expectedVideoFilter, - string expectedAudioLabel, - string expectedVideoLabel) - { - var watermark = new ChannelWatermark - { - Mode = intermittent - ? ChannelWatermarkMode.Intermittent - : ChannelWatermarkMode.Permanent, - DurationSeconds = intermittent ? 15 : 0, - FrequencyMinutes = intermittent ? 10 : 0, - Location = location, - Size = scaled ? WatermarkSize.Scaled : WatermarkSize.ActualSize, - WidthPercent = scaled ? 20 : 0, - Opacity = opacity, - HorizontalMarginPercent = 7, - VerticalMarginPercent = 5 - }; - - Option> maybeFadePoints = watermark.Mode == ChannelWatermarkMode.Intermittent - ? Some( - WatermarkCalculator.CalculateFadePoints( - new DateTimeOffset(2022, 01, 31, 12, 25, 0, TimeSpan.FromHours(-5)), - TimeSpan.Zero, - TimeSpan.FromMinutes(55), - TimeSpan.Zero, - watermark.FrequencyMinutes, - watermark.DurationSeconds)) - : None; - - FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() - .WithWatermark( - Some(watermark), - maybeFadePoints, - new Resolution { Width = 1920, Height = 1080 }, - None) - .WithDeinterlace(deinterlace) - .WithAlignedAudio(alignAudio ? Some(TimeSpan.FromMinutes(55)) : None); - - Option result = builder.Build(false, 0, 0, 0, 1, false); - - result.IsSome.Should().BeTrue(); - result.IfSome( - filter => - { - filter.ComplexFilter.Should().Be(expectedVideoFilter); - filter.AudioLabel.Should().Be(expectedAudioLabel); - filter.VideoLabel.Should().Be(expectedVideoLabel); - }); - } - - [Test] - [TestCase( - false, - false, - false, - WatermarkLocation.BottomLeft, - false, - 100, - "[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=H-h-54[v]", - "0:1", - "[v]", - false)] - [TestCase( - false, - false, - false, - WatermarkLocation.BottomLeft, - false, - 100, - "[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=H-h-54,hwupload[v]", - "0:1", - "[v]", - true)] - [TestCase( - false, - false, - true, - WatermarkLocation.TopLeft, - false, - 100, - "[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)',hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]", - "0:1", - "[v]", - false)] - [TestCase( - false, - false, - true, - WatermarkLocation.TopLeft, - false, - 100, - "[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)',hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]", - "0:1", - "[v]", - true)] - [TestCase( - false, - false, - false, - WatermarkLocation.TopLeft, - true, - 100, - "[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,scale=384:-1,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]", - "0:1", - "[v]", - false)] - [TestCase( - false, - false, - false, - WatermarkLocation.TopLeft, - true, - 100, - "[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,scale=384:-1,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]", - "0:1", - "[v]", - true)] - [TestCase( - false, - false, - false, - WatermarkLocation.TopLeft, - false, - 90, - "[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,colorchannelmixer=aa=0.90,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]", - "0:1", - "[v]", - false)] - [TestCase( - false, - false, - false, - WatermarkLocation.TopLeft, - false, - 90, - "[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,colorchannelmixer=aa=0.90,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]", - "0:1", - "[v]", - true)] - // TODO: do we need these anymore? interlaced content that isn't handled by mpeg2_cuvid? - // [TestCase( - // false, - // true, - // false, - // WatermarkLocation.TopLeft, - // false, - // 100, - // "[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]", - // "0:1", - // "[v]")] - // [TestCase( - // false, - // true, - // false, - // WatermarkLocation.TopLeft, - // true, - // 100, - // "[0:0]yadif=1[vt];[1:v]scale=384:-1[wmp];[vt][wmp]overlay=x=134:y=54[v]", - // "0:1", - // "[v]")] - // [TestCase( - // true, - // true, - // false, - // WatermarkLocation.TopLeft, - // false, - // 100, - // "[0:1]apad=whole_dur=3300000ms[a];[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]", - // "[a]", - // "[v]")] - [TestCase( - true, - false, - false, - WatermarkLocation.TopLeft, - false, - 100, - "[0:1]apad=whole_dur=3300000ms[a];[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]", - "[a]", - "[v]", - false)] - [TestCase( - true, - false, - false, - WatermarkLocation.TopLeft, - false, - 100, - "[0:1]apad=whole_dur=3300000ms[a];[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]", - "[a]", - "[v]", - true)] - public void Should_Return_NVENC_Watermark( - bool alignAudio, - bool deinterlace, - bool intermittent, - WatermarkLocation location, - bool scaled, - int opacity, - string expectedVideoFilter, - string expectedAudioLabel, - string expectedVideoLabel, - bool scaledSource) - { - var watermark = new ChannelWatermark - { - Mode = intermittent - ? ChannelWatermarkMode.Intermittent - : ChannelWatermarkMode.Permanent, - DurationSeconds = intermittent ? 15 : 0, - FrequencyMinutes = intermittent ? 10 : 0, - Location = location, - Size = scaled ? WatermarkSize.Scaled : WatermarkSize.ActualSize, - WidthPercent = scaled ? 20 : 0, - Opacity = opacity, - HorizontalMarginPercent = 7, - VerticalMarginPercent = 5 - }; - - Option> maybeFadePoints = watermark.Mode == ChannelWatermarkMode.Intermittent - ? Some( - WatermarkCalculator.CalculateFadePoints( - new DateTimeOffset(2022, 01, 31, 12, 25, 0, TimeSpan.FromHours(-5)), - TimeSpan.Zero, - TimeSpan.FromMinutes(55), - TimeSpan.Zero, - watermark.FrequencyMinutes, - watermark.DurationSeconds)) - : None; - - FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() - .WithHardwareAcceleration(HardwareAccelerationKind.Nvenc) - .WithWatermark( - Some(watermark), - maybeFadePoints, - new Resolution { Width = 1920, Height = 1080 }, - None) - .WithDeinterlace(deinterlace) - .WithAlignedAudio(alignAudio ? Some(TimeSpan.FromMinutes(55)) : None); - - if (scaledSource) - { - builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1080 }); - } - - Option result = builder.Build(false, 0, 0, 0, 1, false); - - result.IsSome.Should().BeTrue(); - result.IfSome( - filter => - { - filter.ComplexFilter.Should().Be(expectedVideoFilter); - filter.AudioLabel.Should().Be(expectedAudioLabel); - filter.VideoLabel.Should().Be(expectedVideoLabel); - }); - } - - [Test] - [TestCase(true, false, false, "[0:0]deinterlace_qsv[v]", "[v]")] - [TestCase( - true, - true, - false, - "[0:0]deinterlace_qsv,scale_qsv=w=1920:h=1000,setsar=1[v]", - "[v]")] - [TestCase( - true, - false, - true, - "[0:0]deinterlace_qsv,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]", - "[v]")] - [TestCase( - true, - true, - true, - "[0:0]deinterlace_qsv,scale_qsv=w=1920:h=1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]", - "[v]")] - [TestCase( - false, - true, - false, - "[0:0]scale_qsv=w=1920:h=1000,setsar=1[v]", - "[v]")] - [TestCase( - false, - false, - true, - "[0:0]setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]", - "[v]")] - [TestCase( - false, - true, - true, - "[0:0]scale_qsv=w=1920:h=1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]", - "[v]")] - public void Should_Return_QSV_Video_Filter( - bool deinterlace, - bool scale, - bool pad, - string expectedVideoFilter, - string expectedVideoLabel) - { - FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() - .WithHardwareAcceleration(HardwareAccelerationKind.Qsv) - .WithDeinterlace(deinterlace); - - if (scale) - { - builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 }); - } - - if (pad) - { - builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); - } - - Option result = builder.Build(false, 0, 0, 0, 1, false); - - result.IsSome.Should().BeTrue(); - result.IfSome( - filter => - { - filter.ComplexFilter.Should().Be(expectedVideoFilter); - filter.AudioLabel.Should().Be("0:1"); - filter.VideoLabel.Should().Be(expectedVideoLabel); - }); - } - - [Test] - [TestCase(true, false, false, "[0:0]yadif_cuda[v]", "[v]")] - [TestCase( - true, - true, - false, - "[0:0]yadif_cuda,scale_cuda=1920:1000,setsar=1[v]", - "[v]")] - [TestCase( - true, - false, - true, - "[0:0]yadif_cuda,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - [TestCase( - true, - true, - true, - "[0:0]yadif_cuda,scale_cuda=1920:1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - [TestCase( - false, - true, - false, - "[0:0]scale_cuda=1920:1000,setsar=1[v]", - "[v]")] - [TestCase( - false, - false, - true, - "[0:0]setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - [TestCase( - false, - true, - true, - "[0:0]scale_cuda=1920:1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - public void Should_Return_NVENC_Video_Filter( - bool deinterlace, - bool scale, - bool pad, - string expectedVideoFilter, - string expectedVideoLabel) - { - FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() - .WithHardwareAcceleration(HardwareAccelerationKind.Nvenc) - .WithDeinterlace(deinterlace) - .WithInputPixelFormat("h264"); - - if (scale) - { - builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 }); - } - - if (pad) - { - builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); - } - - Option result = builder.Build(false, 0, 0, 0, 1, false); - - result.IsSome.Should().BeTrue(); - result.IfSome( - filter => - { - filter.ComplexFilter.Should().Be(expectedVideoFilter); - filter.AudioLabel.Should().Be("0:1"); - filter.VideoLabel.Should().Be(expectedVideoLabel); - }); - } - - [Test] - [TestCase("h264", true, false, false, "[0:0]deinterlace_vaapi[v]", "[v]")] - [TestCase( - "h264", - true, - true, - false, - "[0:0]deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]", - "[v]")] - [TestCase( - "h264", - true, - false, - true, - "[0:0]deinterlace_vaapi,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - [TestCase( - "h264", - true, - true, - true, - "[0:0]deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - [TestCase( - "h264", - false, - true, - false, - "[0:0]scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]", - "[v]")] - [TestCase( - "h264", - false, - false, - true, - "[0:0]setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - [TestCase( - "h264", - false, - true, - true, - "[0:0]scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - [TestCase("mpeg4", true, false, false, "[0:0]hwupload,deinterlace_vaapi[v]", "[v]")] - [TestCase( - "mpeg4", - true, - true, - false, - "[0:0]hwupload,deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]", - "[v]")] - [TestCase( - "mpeg4", - true, - false, - true, - "[0:0]hwupload,deinterlace_vaapi,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - [TestCase( - "mpeg4", - true, - true, - true, - "[0:0]hwupload,deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - [TestCase( - "mpeg4", - false, - true, - false, - "[0:0]hwupload,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]", - "[v]")] - [TestCase( - "mpeg4", - false, - false, - true, - "[0:0]setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - [TestCase( - "mpeg4", - false, - true, - true, - "[0:0]hwupload,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - public void Should_Return_VAAPI_Video_Filter( - string codec, - bool deinterlace, - bool scale, - bool pad, - string expectedVideoFilter, - string expectedVideoLabel) - { - FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() - .WithHardwareAcceleration(HardwareAccelerationKind.Vaapi) - .WithInputCodec(codec) - .WithDeinterlace(deinterlace); - - if (scale) - { - builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 }); - } - - if (pad) - { - builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); - } - - Option result = builder.Build(false, 0, 0, 0, 1, false); - - result.IsSome.Should().BeTrue(); - result.IfSome( - filter => - { - filter.ComplexFilter.Should().Be(expectedVideoFilter); - filter.AudioLabel.Should().Be("0:1"); - filter.VideoLabel.Should().Be(expectedVideoLabel); - }); - } - } -} diff --git a/ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs b/ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs index 03f89be55..9417cf343 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs @@ -1,5 +1,4 @@ -using System.Globalization; -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; using System.Text; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.FFmpeg; @@ -9,29 +8,16 @@ namespace ErsatzTV.Core.FFmpeg; public class FFmpegComplexFilterBuilder { - private Option _audioDuration = None; private bool _boxBlur; - private bool _deinterlace; - private Option _hardwareAccelerationKind = None; - private string _inputCodec; private Option> _maybeFadePoints = None; - private bool _normalizeLoudness; private Option _padToSize = None; private string _pixelFormat; private IDisplaySize _resolution; private Option _scaleToSize = None; private Option _subtitle; - private string _videoDecoder; - private FFmpegProfileVideoFormat _videoFormat; private Option _watermark; private Option _watermarkIndex; - public FFmpegComplexFilterBuilder WithHardwareAcceleration(HardwareAccelerationKind hardwareAccelerationKind) - { - _hardwareAccelerationKind = Some(hardwareAccelerationKind); - return this; - } - public FFmpegComplexFilterBuilder WithScaling(IDisplaySize scaleToSize) { _scaleToSize = Some(scaleToSize); @@ -44,40 +30,6 @@ public class FFmpegComplexFilterBuilder return this; } - public FFmpegComplexFilterBuilder WithDeinterlace(bool deinterlace) - { - _deinterlace = deinterlace; - return this; - } - - public FFmpegComplexFilterBuilder WithAlignedAudio(Option audioDuration) - { - _audioDuration = audioDuration; - return this; - } - - public FFmpegComplexFilterBuilder WithNormalizeLoudness(bool normalizeLoudness) - { - _normalizeLoudness = normalizeLoudness; - return this; - } - - public FFmpegComplexFilterBuilder WithInputCodec(Option maybeCodec) - { - foreach (string codec in maybeCodec) - { - _inputCodec = codec; - } - - return this; - } - - public FFmpegComplexFilterBuilder WithDecoder(string decoder) - { - _videoDecoder = decoder; - return this; - } - public FFmpegComplexFilterBuilder WithInputPixelFormat(Option maybePixelFormat) { foreach (string pixelFormat in maybePixelFormat) @@ -131,12 +83,6 @@ public class FFmpegComplexFilterBuilder return this; } - public FFmpegComplexFilterBuilder WithVideoFormat(FFmpegProfileVideoFormat videoFormat) - { - _videoFormat = videoFormat; - return this; - } - public Option Build( bool videoOnly, int videoInput, @@ -153,121 +99,13 @@ public class FFmpegComplexFilterBuilder string videoLabel = $"{videoInput}:{(isSong ? "v" : videoStreamIndex.ToString())}"; string audioLabel = audioStreamIndex.Match(index => $"{audioInput}:{index}", () => "0:a"); - HardwareAccelerationKind acceleration = _hardwareAccelerationKind.IfNone(HardwareAccelerationKind.None); - - bool isHardwareDecode = acceleration switch - { - HardwareAccelerationKind.Vaapi => !isSong && _inputCodec != "mpeg4" && - (_deinterlace == false || !_pixelFormat.Contains("p10le")), - - // we need an initial hwupload_cuda when only padding with these pixel formats - HardwareAccelerationKind.Nvenc when _scaleToSize.IsNone && _padToSize.IsSome => - !isSong && !_pixelFormat.Contains("p10le") && !_pixelFormat.Contains("444"), - - HardwareAccelerationKind.Nvenc => !isSong && - (string.IsNullOrWhiteSpace(_videoDecoder) || - _videoDecoder.Contains("cuvid")), - HardwareAccelerationKind.Qsv => !isSong, - HardwareAccelerationKind.VideoToolbox => false, - HardwareAccelerationKind.Amf => false, - _ => false - }; - - bool nvencDeinterlace = acceleration == HardwareAccelerationKind.Nvenc && _videoDecoder == "mpeg2_cuvid" && - _deinterlace; - // mpeg2_cuvid will handle deinterlace and is "not" a hardware decode - if (nvencDeinterlace) - { - _deinterlace = false; - isHardwareDecode = false; - } - - var audioFilterQueue = new List(); var videoFilterQueue = new List(); var watermarkPreprocess = new List(); string watermarkOverlay = string.Empty; - if (_normalizeLoudness) - { - audioFilterQueue.Add("loudnorm=I=-16:TP=-1.5:LRA=11"); - } - - _audioDuration.IfSome( - audioDuration => - { - var durationString = audioDuration.TotalMilliseconds.ToString(NumberFormatInfo.InvariantInfo); - audioFilterQueue.Add($"apad=whole_dur={durationString}ms"); - }); - - bool usesHardwareFilters = acceleration != HardwareAccelerationKind.None && - acceleration != HardwareAccelerationKind.VideoToolbox && - acceleration != HardwareAccelerationKind.Amf && - !isHardwareDecode && - (_deinterlace || _scaleToSize.IsSome); - if (isSong) { - switch (acceleration) - { - case HardwareAccelerationKind.Qsv: - videoFilterQueue.Add("format=nv12"); - break; - case HardwareAccelerationKind.Vaapi: - videoFilterQueue.Add("format=nv12|vaapi"); - break; - default: - videoFilterQueue.Add("format=yuv420p"); - break; - } - } - - switch (usesHardwareFilters || isSong, acceleration) - { - case (true, HardwareAccelerationKind.Nvenc): - videoFilterQueue.Add("hwupload_cuda"); - break; - case (true, HardwareAccelerationKind.Qsv): - videoFilterQueue.Add("hwupload=extra_hw_frames=64"); - break; - case (true, HardwareAccelerationKind.Vaapi): - videoFilterQueue.Add("hwupload"); - break; - case (true, _) when usesHardwareFilters: - videoFilterQueue.Add("hwupload"); - break; - } - - if (_deinterlace) - { - Option maybeFilter = acceleration switch - { - HardwareAccelerationKind.Qsv => "deinterlace_qsv", - HardwareAccelerationKind.Nvenc when !usesHardwareFilters && _pixelFormat.Contains("p10le") => - "hwupload_cuda,yadif_cuda", - HardwareAccelerationKind.Nvenc => "yadif_cuda", - HardwareAccelerationKind.Vaapi => "deinterlace_vaapi", - _ => "yadif=1" - }; - - foreach (string filter in maybeFilter) - { - videoFilterQueue.Add(filter); - } - } - - string[] h264hevc = { "h264", "hevc" }; - - if (_deinterlace == false && acceleration == HardwareAccelerationKind.Vaapi && - (_pixelFormat ?? string.Empty).EndsWith("p10le") && - h264hevc.Contains(_inputCodec) && (_pixelFormat != "yuv420p10le" || _inputCodec != "hevc")) - { - videoFilterQueue.Add("format=p010le,format=nv12|vaapi,hwupload"); - } - - if (acceleration == HardwareAccelerationKind.Vaapi && _pixelFormat == "yuv444p" && - h264hevc.Contains(_inputCodec)) - { - videoFilterQueue.Add("format=nv12|vaapi,hwupload"); + videoFilterQueue.Add("format=yuv420p"); } bool scaleOrPad = _scaleToSize.IsSome || _padToSize.IsSome; @@ -277,31 +115,6 @@ public class FFmpegComplexFilterBuilder var softwareFilterQueue = new List(); if (usesSoftwareFilters) { - if (acceleration != HardwareAccelerationKind.None && (isHardwareDecode || usesHardwareFilters)) - { - Option maybeFormat = acceleration switch - { - HardwareAccelerationKind.Vaapi => "format=nv12|vaapi", - HardwareAccelerationKind.Nvenc when _padToSize.IsNone || nvencDeinterlace => None, - HardwareAccelerationKind.Nvenc when _pixelFormat == "yuv420p10le" => - "format=p010le,format=nv12", - HardwareAccelerationKind.Qsv when isSong => "format=nv12,format=yuv420p", - _ when isSong => "format=yuv420p", - _ => "format=nv12" - }; - - foreach (string format in maybeFormat) - { - softwareFilterQueue.Add("hwdownload"); - softwareFilterQueue.Add(format); - } - - if (nvencDeinterlace) - { - softwareFilterQueue.Add("hwdownload"); - } - } - if (_boxBlur) { softwareFilterQueue.Add("boxblur=40"); @@ -314,16 +127,9 @@ public class FFmpegComplexFilterBuilder foreach (ChannelWatermark watermark in _watermark) { - Option maybeFormats = acceleration switch - { - // overlay_cuda only supports alpha with yuva420p - HardwareAccelerationKind.Nvenc => "yuva420p", - - _ when watermark.Opacity != 100 || hasFadePoints => - "yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8", - - _ => None - }; + Option maybeFormats = watermark.Opacity != 100 || hasFadePoints + ? "yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8" + : None; foreach (string formats in maybeFormats) { @@ -362,69 +168,29 @@ public class FFmpegComplexFilterBuilder watermarkPreprocess.AddRange(fadePoints.Map(fp => fp.ToFilter())); } - if (acceleration == HardwareAccelerationKind.Nvenc) - { - watermarkPreprocess.Add("hwupload_cuda"); - } - - watermarkOverlay = acceleration switch - { - HardwareAccelerationKind.Nvenc => $"overlay_cuda={position}", - _ => $"overlay={position}" - }; + watermarkOverlay = $"overlay={position}"; - if (hasFadePoints && acceleration != HardwareAccelerationKind.Nvenc) + if (hasFadePoints) { - watermarkOverlay += "," + acceleration switch + watermarkOverlay += "," + isSong switch { - HardwareAccelerationKind.Vaapi => "format=nv12|vaapi", - _ when isSong => "format=yuv420p", - _ => "format=nv12" + true => "format=yuv420p", + false => "format=nv12" }; } } } string outputPixelFormat = null; - if (!usesSoftwareFilters && string.IsNullOrWhiteSpace(watermarkOverlay)) - { - switch (acceleration, _videoFormat, _pixelFormat) - { - case (HardwareAccelerationKind.Nvenc, FFmpegProfileVideoFormat.H264, "yuv420p10le"): - outputPixelFormat = "yuv420p"; - break; - case (HardwareAccelerationKind.Nvenc, FFmpegProfileVideoFormat.H264, "yuv444p10le"): - outputPixelFormat = "yuv444p"; - break; - } - } - - string outputFormat = (acceleration, _videoFormat, _pixelFormat) switch - { - (HardwareAccelerationKind.Nvenc, FFmpegProfileVideoFormat.Hevc, "yuv420p10le") => "p010le", - (HardwareAccelerationKind.Nvenc, FFmpegProfileVideoFormat.H264, "yuv420p10le") => "p010le", - _ => null - }; _scaleToSize.IfSome( size => { - string filter = acceleration switch + string filter = videoOnly switch { - HardwareAccelerationKind.Qsv => $"scale_qsv=w={size.Width}:h={size.Height}", - HardwareAccelerationKind.Nvenc when _watermark.IsSome && _scaleToSize.IsNone => - $"format=yuv420p,hwupload_cuda,scale_cuda={size.Width}:{size.Height}", - HardwareAccelerationKind.Nvenc when _watermark.IsSome && _padToSize.IsNone => - $"scale_cuda={size.Width}:{size.Height}", - HardwareAccelerationKind.Nvenc when _watermark.IsNone && !string.IsNullOrEmpty(outputFormat) => - $"scale_cuda={size.Width}:{size.Height}:format={outputFormat}", - HardwareAccelerationKind.Nvenc when _pixelFormat is "yuv420p10le" && usesHardwareFilters == false => - $"hwupload_cuda,scale_cuda={size.Width}:{size.Height}", - HardwareAccelerationKind.Nvenc => $"scale_cuda={size.Width}:{size.Height}", - HardwareAccelerationKind.Vaapi => $"scale_vaapi=format=nv12:w={size.Width}:h={size.Height}", - _ when videoOnly => + true => $"scale={size.Width}:{size.Height}:force_original_aspect_ratio=increase,crop={size.Width}:{size.Height}", - _ => $"scale={size.Width}:{size.Height}:flags=fast_bilinear" + false => $"scale={size.Width}:{size.Height}:flags=fast_bilinear" }; if (!string.IsNullOrWhiteSpace(filter)) @@ -435,14 +201,6 @@ public class FFmpegComplexFilterBuilder if (scaleOrPad && _boxBlur == false) { - if (acceleration == HardwareAccelerationKind.Nvenc) - { - if (!isHardwareDecode && !string.IsNullOrWhiteSpace(outputPixelFormat)) - { - videoFilterQueue.Add($"hwdownload,format={outputPixelFormat}"); - } - } - videoFilterQueue.Add("setsar=1"); } @@ -450,63 +208,13 @@ public class FFmpegComplexFilterBuilder _padToSize.IfSome(size => videoFilterQueue.Add($"pad={size.Width}:{size.Height}:(ow-iw)/2:(oh-ih)/2")); - if (acceleration == HardwareAccelerationKind.Nvenc && _watermark.IsSome) - { - if (_scaleToSize.IsSome) - { - videoFilterQueue.Add("hwdownload,format=nv12,format=yuv420p"); - videoFilterQueue.Add("hwupload_cuda"); - } - else if (_padToSize.IsNone) - { - videoFilterQueue.Add("scale_cuda=format=yuv420p"); - } - else - { - videoFilterQueue.Add("format=yuv420p"); - videoFilterQueue.Add("hwupload_cuda"); - } - } - foreach (string subtitle in _subtitle) { videoFilterQueue.Add(subtitle); } - if (usesSoftwareFilters && acceleration != HardwareAccelerationKind.None && - string.IsNullOrWhiteSpace(watermarkOverlay)) - { - string upload = acceleration switch - { - HardwareAccelerationKind.Qsv => "hwupload=extra_hw_frames=64", - _ => "hwupload" - }; - videoFilterQueue.Add(upload); - } - - bool hasAudioFilters = audioFilterQueue.Any(); - if (hasAudioFilters) - { - complexFilter.Append($"[{audioLabel}]"); - complexFilter.Append(string.Join(",", audioFilterQueue)); - audioLabel = "[a]"; - complexFilter.Append(audioLabel); - } - - // vaapi downsample 10bit hevc to 8bit h264 - if (acceleration == HardwareAccelerationKind.Vaapi && !videoFilterQueue.Any() && - _pixelFormat == "yuv420p10le" && _videoFormat == FFmpegProfileVideoFormat.H264) - { - videoFilterQueue.Add("scale_vaapi=format=nv12"); - } - if (videoFilterQueue.Any() || !string.IsNullOrWhiteSpace(watermarkOverlay)) { - if (hasAudioFilters) - { - complexFilter.Append(';'); - } - if (videoFilterQueue.Any()) { complexFilter.Append($"[{videoLabel}]"); @@ -538,25 +246,6 @@ public class FFmpegComplexFilterBuilder videoFilterQueue.Any() ? $"[vt]{watermarkLabel}{watermarkOverlay}" : $"[{videoLabel}]{watermarkLabel}{watermarkOverlay}"); - - if (usesSoftwareFilters && acceleration != HardwareAccelerationKind.None) - { - switch (isSong, acceleration) - { - case (true, HardwareAccelerationKind.Nvenc): - complexFilter.Append(",hwupload_cuda"); - break; - // no need to upload since we're already in the GPU with overlay_cuda - case (_, HardwareAccelerationKind.Nvenc) when scaleOrPad == false && _watermark.IsSome: - break; - case (_, HardwareAccelerationKind.Qsv): - complexFilter.Append(",format=yuv420p,hwupload=extra_hw_frames=64"); - break; - default: - complexFilter.Append(",hwupload"); - break; - } - } } videoLabel = "[v]"; diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index 3583a0c87..5b28cdfeb 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -442,8 +442,37 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService return GetCommand(ffmpegPath, None, None, None, concatInputFile, pipeline); } - public Command WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host) => - _ffmpegProcessService.WrapSegmenter(ffmpegPath, saveReports, channel, scheme, host); + public async Task WrapSegmenter( + string ffmpegPath, + bool saveReports, + Channel channel, + string scheme, + string host) + { + var resolution = new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height); + + var concatInputFile = new ConcatInputFile( + $"http://localhost:{Settings.ListenPort}/iptv/channel/{channel.Number}.m3u8?mode=segmenter", + resolution); + + IPipelineBuilder pipelineBuilder = await _pipelineBuilderFactory.GetBuilder( + HardwareAccelerationMode.None, + None, + None, + None, + None, + None, + None, + FileSystemLayout.FFmpegReportsFolder, + FileSystemLayout.FontsCacheFolder, + ffmpegPath); + + FFmpegPipeline pipeline = pipelineBuilder.WrapSegmenter( + concatInputFile, + FFmpegState.Concat(saveReports, channel.Name)); + + return GetCommand(ffmpegPath, None, None, None, concatInputFile, pipeline); + } public async Task ResizeImage(string ffmpegPath, string inputFile, string outputFile, int height) { diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs b/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs index c65e46a0b..df9da2aa1 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs @@ -19,12 +19,9 @@ // 3. This notice may not be removed or altered from any source distribution. using System.Diagnostics; -using System.Globalization; -using System.Runtime.InteropServices; using System.Text; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.FFmpeg; -using Microsoft.Extensions.Logging; namespace ErsatzTV.Core.FFmpeg; @@ -32,36 +29,11 @@ internal class FFmpegProcessBuilder { private readonly List _arguments = new(); private readonly string _ffmpegPath; - private readonly ILogger _logger; - private readonly bool _saveReports; private FFmpegComplexFilterBuilder _complexFilterBuilder = new(); - private HardwareAccelerationKind _hwAccel; - private bool _isConcat; - private bool _noAutoScale; - private Option _outputFramerate; - private string _outputPixelFormat; - private string _vaapiDevice; - private VaapiDriver _vaapiDriver; - - public FFmpegProcessBuilder(string ffmpegPath, bool saveReports, ILogger logger) - { - _ffmpegPath = ffmpegPath; - _saveReports = saveReports; - _logger = logger; - } - public FFmpegProcessBuilder WithVaapiDriver(VaapiDriver vaapiDriver, string vaapiDevice) + public FFmpegProcessBuilder(string ffmpegPath) { - if (vaapiDriver != VaapiDriver.Default) - { - _vaapiDriver = vaapiDriver; - } - - _vaapiDevice = string.IsNullOrWhiteSpace(vaapiDevice) - ? "/dev/dri/renderD128" - : vaapiDevice; - - return this; + _ffmpegPath = ffmpegPath; } public FFmpegProcessBuilder WithThreads(int threads) @@ -71,162 +43,6 @@ internal class FFmpegProcessBuilder return this; } - public FFmpegProcessBuilder WithHardwareAcceleration( - HardwareAccelerationKind hwAccel, - Option pixelFormat, - FFmpegProfileVideoFormat videoFormat) - { - _hwAccel = hwAccel; - - switch (hwAccel) - { - case HardwareAccelerationKind.Qsv: - _arguments.Add("-hwaccel"); - _arguments.Add("qsv"); - _arguments.Add("-init_hw_device"); - _arguments.Add("qsv=qsv:MFX_IMPL_hw_any"); - break; - case HardwareAccelerationKind.Nvenc: - string outputFormat = (videoFormat, pixelFormat.IfNone("")) switch - { - (FFmpegProfileVideoFormat.Hevc, "yuv420p10le") => "p010le", - (FFmpegProfileVideoFormat.H264, "yuv420p10le") => "p010le", - // ("hevc_nvenc", "yuv444p10le") => "p016le", - _ => "cuda" - }; - - _arguments.Add("-hwaccel"); - _arguments.Add("cuda"); - _arguments.Add("-hwaccel_output_format"); - _arguments.Add(outputFormat); - break; - case HardwareAccelerationKind.Vaapi: - _arguments.Add("-hwaccel"); - _arguments.Add("vaapi"); - _arguments.Add("-vaapi_device"); - _arguments.Add(_vaapiDevice); - _arguments.Add("-hwaccel_output_format"); - _arguments.Add("vaapi"); - break; - case HardwareAccelerationKind.VideoToolbox: - _arguments.Add("-hwaccel"); - _arguments.Add("videotoolbox"); - break; - } - - _complexFilterBuilder = _complexFilterBuilder.WithHardwareAcceleration(hwAccel); - - return this; - } - - public FFmpegProcessBuilder WithRealtimeOutput(bool realtimeOutput) - { - if (realtimeOutput) - { - if (!_arguments.Contains("-re")) - { - _arguments.Add("-re"); - } - } - else - { - _arguments.RemoveAll(s => s == "-re"); - } - - return this; - } - - public FFmpegProcessBuilder WithSeek(Option maybeStart) - { - maybeStart.IfSome( - start => - { - _arguments.Add("-ss"); - _arguments.Add($"{start:c}"); - }); - - return this; - } - - public FFmpegProcessBuilder WithInfiniteLoop(bool loop = true) - { - if (loop) - { - _arguments.Add("-stream_loop"); - _arguments.Add("-1"); - - if (_hwAccel is HardwareAccelerationKind.Qsv or HardwareAccelerationKind.Vaapi) - { - _noAutoScale = true; - } - } - - return this; - } - - public FFmpegProcessBuilder WithLoopedImage(string input) - { - _arguments.Add("-loop"); - _arguments.Add("1"); - return WithInput(input); - } - - - public FFmpegProcessBuilder WithPipe() - { - _arguments.Add("pipe:1"); - return this; - } - - public FFmpegProcessBuilder WithPixfmt(string pixfmt) - { - _arguments.Add("-pix_fmt"); - _arguments.Add(pixfmt); - return this; - } - - public FFmpegProcessBuilder WithLibavfilter() - { - _arguments.Add("-f"); - _arguments.Add("lavfi"); - return this; - } - - public FFmpegProcessBuilder WithInput(string input) - { - _arguments.Add("-i"); - _arguments.Add(input); - return this; - } - - public FFmpegProcessBuilder WithMap(string map) - { - _arguments.Add("-map"); - _arguments.Add(map); - return this; - } - - public FFmpegProcessBuilder WithCopyCodec() - { - _arguments.Add("-c"); - _arguments.Add("copy"); - return this; - } - - public FFmpegProcessBuilder WithFrameRate(Option frameRate) - { - foreach (int fr in frameRate) - { - _arguments.Add("-r"); - _arguments.Add($"{fr}"); - - _arguments.Add("-vsync"); - _arguments.Add("cfr"); - } - - return this; - } - public FFmpegProcessBuilder WithWatermark( Option watermarkOptions, Option> maybeFadePoints, @@ -279,74 +95,12 @@ internal class FFmpegProcessBuilder return this; } - public FFmpegProcessBuilder WithInputCodec( - Option maybeStart, - bool loop, - string videoPath, - string audioPath, - string decoder, - Option codec, - Option pixelFormat, - bool deinterlace) - { - if (audioPath == videoPath) - { - WithSeek(maybeStart); - WithInfiniteLoop(loop); - } - else - { - _noAutoScale = true; - _outputFramerate = 30; - - _arguments.Add("-loop"); - _arguments.Add("1"); - } - - if (!string.IsNullOrWhiteSpace(decoder)) - { - _arguments.Add("-c:v"); - _arguments.Add(decoder); - - if (decoder == "mpeg2_cuvid" && deinterlace) - { - _arguments.Add("-deint"); - _arguments.Add("2"); - } - - _complexFilterBuilder = _complexFilterBuilder - .WithDecoder(decoder); - } - - _complexFilterBuilder = _complexFilterBuilder - .WithInputCodec(codec) - .WithInputPixelFormat(pixelFormat); - - _arguments.Add("-i"); - _arguments.Add(videoPath); - - if (audioPath != videoPath) - { - WithSeek(maybeStart); - - _arguments.Add("-i"); - _arguments.Add(audioPath); - } - - return this; - } - public FFmpegProcessBuilder WithSongInput( string videoPath, - Option codec, Option pixelFormat, bool boxBlur) { - _noAutoScale = true; - _outputFramerate = 30; - _complexFilterBuilder = _complexFilterBuilder - .WithInputCodec(codec) .WithInputPixelFormat(pixelFormat) .WithBoxBlur(boxBlur); @@ -356,52 +110,6 @@ internal class FFmpegProcessBuilder return this; } - public FFmpegProcessBuilder WithConcat(string concatPlaylist) - { - _isConcat = true; - - var arguments = new List - { - "-f", "concat", - "-safe", "0", - "-protocol_whitelist", "file,http,tcp,https,tcp,tls", - "-probesize", "32", - "-i", concatPlaylist, - "-c", "copy", - "-muxdelay", "0", - "-muxpreload", "0" - // "-avoid_negative_ts", "make_zero" - }; - _arguments.AddRange(arguments); - return this; - } - - public FFmpegProcessBuilder WithMetadata(Channel channel, Option maybeAudioStream) - { - if (channel.StreamingMode == StreamingMode.TransportStream) - { - _arguments.AddRange(new[] { "-map_metadata", "-1" }); - } - - foreach (MediaStream audioStream in maybeAudioStream) - { - if (!string.IsNullOrWhiteSpace(audioStream.Language)) - { - _arguments.AddRange(new[] { "-metadata:s:a:0", $"language={audioStream.Language}" }); - } - } - - var arguments = new List - { - "-metadata", "service_provider=\"ErsatzTV\"", - "-metadata", $"service_name=\"{channel.Name}\"" - }; - - _arguments.AddRange(arguments); - - return this; - } - public FFmpegProcessBuilder WithFormatFlags(IEnumerable formatFlags) { _arguments.Add("-fflags"); @@ -409,138 +117,6 @@ internal class FFmpegProcessBuilder return this; } - public FFmpegProcessBuilder WithDuration(TimeSpan duration) - { - _arguments.Add("-t"); - _arguments.Add($"{duration:c}"); - return this; - } - - public FFmpegProcessBuilder WithFormat(string format) - { - _arguments.Add("-f"); - _arguments.Add($"{format}"); - return this; - } - - public FFmpegProcessBuilder WithInitialDiscontinuity() - { - _arguments.Add("-mpegts_flags"); - _arguments.Add("+initial_discontinuity"); - return this; - } - - public FFmpegProcessBuilder WithHls( - string channelNumber, - Option mediaVersion, - long ptsOffset, - Option maybeTimeScale, - Option maybeFrameRate) - { - const int SEGMENT_SECONDS = 4; - int frameRate = maybeFrameRate.IfNone(GetFrameRateFromMediaVersion(mediaVersion)); - - foreach (int timescale in maybeTimeScale) - { - _arguments.Add("-output_ts_offset"); - _arguments.Add($"{(ptsOffset / (double)timescale).ToString(NumberFormatInfo.InvariantInfo)}"); - } - - _arguments.AddRange( - new[] - { - "-g", $"{frameRate * SEGMENT_SECONDS}", - "-keyint_min", $"{frameRate * SEGMENT_SECONDS}", - "-force_key_frames", $"expr:gte(t,n_forced*{SEGMENT_SECONDS})", - "-f", "hls", - "-hls_time", $"{SEGMENT_SECONDS}", - "-hls_list_size", "0", - "-segment_list_flags", "+live", - "-hls_segment_filename", - Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber, "live%06d.ts"), - "-hls_flags", "program_date_time+append_list+discont_start+omit_endlist+independent_segments", - "-mpegts_flags", "+initial_discontinuity", - Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber, "live.m3u8") - }); - - return this; - } - - public FFmpegProcessBuilder WithPlaybackArgs( - FFmpegPlaybackSettings playbackSettings, - string videoCodec, - string audioCodec) - { - var arguments = new List - { - "-c:v", videoCodec, - "-flags", "cgop", - // disable scene change detection except with mpeg2video - "-sc_threshold", playbackSettings.VideoFormat == FFmpegProfileVideoFormat.Mpeg2Video ? "1000000000" : "0" - }; - - if (!string.IsNullOrWhiteSpace(_outputPixelFormat)) - { - arguments.AddRange(new[] { "-pix_fmt", _outputPixelFormat }); - } - - string[] videoBitrateArgs = playbackSettings.VideoBitrate.Match( - bitrate => - new[] - { - "-b:v", $"{bitrate}k", - "-maxrate:v", $"{bitrate}k" - }, - Array.Empty()); - arguments.AddRange(videoBitrateArgs); - - playbackSettings.VideoBufferSize - .IfSome(bufferSize => arguments.AddRange(new[] { "-bufsize:v", $"{bufferSize}k" })); - - string[] audioBitrateArgs = playbackSettings.AudioBitrate.Match( - bitrate => - new[] - { - "-b:a", $"{bitrate}k", - "-maxrate:a", $"{bitrate}k" - }, - Array.Empty()); - arguments.AddRange(audioBitrateArgs); - - playbackSettings.AudioBufferSize - .IfSome(bufferSize => arguments.AddRange(new[] { "-bufsize:a", $"{bufferSize}k" })); - - playbackSettings.AudioChannels - .IfSome(channels => arguments.AddRange(new[] { "-ac", $"{channels}" })); - - playbackSettings.AudioSampleRate - .IfSome(sampleRate => arguments.AddRange(new[] { "-ar", $"{sampleRate}k" })); - - arguments.AddRange( - new[] - { - "-c:a", audioCodec, - "-movflags", "+faststart", - "-muxdelay", "0", - "-muxpreload", "0" - }); - - _arguments.AddRange(arguments); - - if (_noAutoScale) - { - _arguments.Add("-noautoscale"); - } - - foreach (int framerate in _outputFramerate) - { - _arguments.Add("-r"); - _arguments.Add(framerate.ToString()); - } - - return this; - } - public FFmpegProcessBuilder WithScaling(IDisplaySize displaySize) { _complexFilterBuilder = _complexFilterBuilder.WithScaling(displaySize); @@ -553,35 +129,6 @@ internal class FFmpegProcessBuilder return this; } - public FFmpegProcessBuilder WithAlignedAudio(Option audioDuration) - { - _complexFilterBuilder = _complexFilterBuilder.WithAlignedAudio(audioDuration); - return this; - } - - public FFmpegProcessBuilder WithNormalizeLoudness(bool normalizeLoudness) - { - _complexFilterBuilder = _complexFilterBuilder.WithNormalizeLoudness(normalizeLoudness); - return this; - } - - public FFmpegProcessBuilder WithVideoTrackTimeScale(Option videoTrackTimeScale) - { - videoTrackTimeScale.IfSome( - timeScale => - { - _arguments.Add("-video_track_timescale"); - _arguments.Add($"{timeScale}"); - }); - return this; - } - - public FFmpegProcessBuilder WithDeinterlace(bool deinterlace) - { - _complexFilterBuilder = _complexFilterBuilder.WithDeinterlace(deinterlace); - return this; - } - public FFmpegProcessBuilder WithOutputFormat(string format, string output) { _arguments.Add("-f"); @@ -597,11 +144,8 @@ internal class FFmpegProcessBuilder MediaStream videoStream, Option maybeAudioStream, string videoPath, - Option audioPath, - FFmpegProfileVideoFormat videoFormat) + Option audioPath) { - _complexFilterBuilder = _complexFilterBuilder.WithVideoFormat(videoFormat); - int videoStreamIndex = videoStream.Index; Option maybeIndex = maybeAudioStream.Map(ms => ms.Index); @@ -615,10 +159,6 @@ internal class FFmpegProcessBuilder else if (audioPath.IfNone("NotARealPath") != videoPath) { audioIndex = 1; - if (_hwAccel == HardwareAccelerationKind.None) - { - _outputPixelFormat = "yuv420p"; - } } string videoLabel = $"{videoIndex}:{videoStreamIndex}"; @@ -639,11 +179,6 @@ internal class FFmpegProcessBuilder _arguments.Add(filter.ComplexFilter); videoLabel = filter.VideoLabel; audioLabel = filter.AudioLabel; - - if (!string.IsNullOrWhiteSpace(filter.PixelFormat)) - { - _outputPixelFormat = filter.PixelFormat; - } }); foreach (string _ in audioPath) @@ -676,44 +211,6 @@ internal class FFmpegProcessBuilder StandardOutputEncoding = Encoding.UTF8 }; - if (_hwAccel == HardwareAccelerationKind.Vaapi) - { - switch (_vaapiDriver) - { - case VaapiDriver.i965: - startInfo.EnvironmentVariables["LIBVA_DRIVER_NAME"] = "i965"; - break; - case VaapiDriver.iHD: - startInfo.EnvironmentVariables["LIBVA_DRIVER_NAME"] = "iHD"; - break; - case VaapiDriver.RadeonSI: - startInfo.EnvironmentVariables["LIBVA_DRIVER_NAME"] = "radeonsi"; - break; - case VaapiDriver.Nouveau: - startInfo.EnvironmentVariables["LIBVA_DRIVER_NAME"] = "nouveau"; - break; - } - } - - if (_saveReports) - { - string fileName = _isConcat - ? Path.Combine(FileSystemLayout.FFmpegReportsFolder, "ffmpeg-%t-concat.log") - : Path.Combine(FileSystemLayout.FFmpegReportsFolder, "ffmpeg-%t-transcode.log"); - - // rework filename in a format that works on windows - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - // \ is escape, so use / for directory separators - fileName = fileName.Replace(@"\", @"/"); - - // colon after drive letter needs to be escaped - fileName = fileName.Replace(@":/", @"\:/"); - } - - startInfo.EnvironmentVariables["FFREPORT"] = $"file={fileName}:level=32"; - } - startInfo.ArgumentList.Add("-nostdin"); foreach (string argument in _arguments) { @@ -725,30 +222,4 @@ internal class FFmpegProcessBuilder StartInfo = startInfo }; } - - private int GetFrameRateFromMediaVersion(Option mediaVersion) - { - var frameRate = 24; - - foreach (MediaVersion version in mediaVersion) - { - if (!int.TryParse(version.RFrameRate, out int fr)) - { - string[] split = (version.RFrameRate ?? string.Empty).Split("/"); - if (int.TryParse(split[0], out int left) && int.TryParse(split[1], out int right)) - { - fr = (int)Math.Round(left / (double)right); - } - else - { - _logger.LogInformation("Unable to detect framerate, using {FrameRate}", 24); - fr = 24; - } - } - - frameRate = fr; - } - - return frameRate; - } } diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs index ea38eb7fb..091bb9d5f 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs @@ -40,30 +40,6 @@ public class FFmpegProcessService _logger = logger; } - public Command WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host) - { - FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.ConcatSettings; - - Process process = new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger) - .WithThreads(1) - .WithQuiet() - .WithFormatFlags(playbackSettings.FormatFlags) - .WithRealtimeOutput(true) - .WithInput($"http://localhost:{Settings.ListenPort}/iptv/channel/{channel.Number}.m3u8?mode=segmenter") - .WithMap("0") - .WithCopyCodec() - .WithMetadata(channel, None) - .WithFormat("mpegts") - .WithPipe() - .Build(); - - return Cli.Wrap(process.StartInfo.FileName) - .WithArguments(process.StartInfo.ArgumentList) - .WithValidation(CommandResultValidation.None) - .WithEnvironmentVariables(process.StartInfo.Environment.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)) - .WithStandardErrorPipe(PipeTarget.ToStream(Stream.Null)); - } - public async Task> GenerateSongImage( string ffmpegPath, string ffprobePath, @@ -130,11 +106,11 @@ public class FFmpegProcessService false, Option.None); - FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, false, _logger) + FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath) .WithThreads(1) .WithQuiet() .WithFormatFlags(playbackSettings.FormatFlags) - .WithSongInput(videoPath, videoStream.Codec, videoStream.PixelFormat, boxBlur) + .WithSongInput(videoPath, videoStream.PixelFormat, boxBlur) .WithWatermark(watermarkOptions, None, channel.FFmpegProfile.Resolution) .WithSubtitleFile(subtitleFile); @@ -153,8 +129,7 @@ public class FFmpegProcessService videoStream, None, videoPath, - None, - playbackSettings.VideoFormat) + None) .WithOutputFormat("apng", outputFile) .Build(); diff --git a/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs b/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs index b7f6c154f..5c6d2884d 100644 --- a/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs +++ b/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs @@ -53,7 +53,7 @@ public interface IFFmpegProcessService Task ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host); - Command WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host); + Task WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host); Task ResizeImage(string ffmpegPath, string inputFile, string outputFile, int height); diff --git a/ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs b/ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs index 519d9bb26..6e55852fa 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs @@ -11,5 +11,4 @@ public interface IFallbackMetadataProvider Option GetFallbackMetadata(MusicVideo musicVideo); Option GetFallbackMetadata(OtherVideo otherVideo); Option GetFallbackMetadata(Song song); - string GetSortTitle(string title); } diff --git a/ErsatzTV.Core/Interfaces/Metadata/Nfo/IArtistNfoReader.cs b/ErsatzTV.Core/Interfaces/Metadata/Nfo/IArtistNfoReader.cs deleted file mode 100644 index cf3678863..000000000 --- a/ErsatzTV.Core/Interfaces/Metadata/Nfo/IArtistNfoReader.cs +++ /dev/null @@ -1,8 +0,0 @@ -using ErsatzTV.Core.Metadata.Nfo; - -namespace ErsatzTV.Core.Interfaces.Metadata.Nfo; - -public interface IArtistNfoReader -{ - Task> ReadFromFile(string fileName); -} diff --git a/ErsatzTV.Core/Interfaces/Metadata/Nfo/IEpisodeNfoReader.cs b/ErsatzTV.Core/Interfaces/Metadata/Nfo/IEpisodeNfoReader.cs deleted file mode 100644 index 19d142840..000000000 --- a/ErsatzTV.Core/Interfaces/Metadata/Nfo/IEpisodeNfoReader.cs +++ /dev/null @@ -1,8 +0,0 @@ -using ErsatzTV.Core.Metadata.Nfo; - -namespace ErsatzTV.Core.Interfaces.Metadata.Nfo; - -public interface IEpisodeNfoReader -{ - Task>> ReadFromFile(string fileName); -} diff --git a/ErsatzTV.Core/Interfaces/Metadata/Nfo/IMovieNfoReader.cs b/ErsatzTV.Core/Interfaces/Metadata/Nfo/IMovieNfoReader.cs deleted file mode 100644 index 67e4d0ead..000000000 --- a/ErsatzTV.Core/Interfaces/Metadata/Nfo/IMovieNfoReader.cs +++ /dev/null @@ -1,8 +0,0 @@ -using ErsatzTV.Core.Metadata.Nfo; - -namespace ErsatzTV.Core.Interfaces.Metadata.Nfo; - -public interface IMovieNfoReader -{ - Task> ReadFromFile(string fileName); -} diff --git a/ErsatzTV.Core/Interfaces/Metadata/Nfo/IMusicVideoNfoReader.cs b/ErsatzTV.Core/Interfaces/Metadata/Nfo/IMusicVideoNfoReader.cs deleted file mode 100644 index 8da91b7c8..000000000 --- a/ErsatzTV.Core/Interfaces/Metadata/Nfo/IMusicVideoNfoReader.cs +++ /dev/null @@ -1,8 +0,0 @@ -using ErsatzTV.Core.Metadata.Nfo; - -namespace ErsatzTV.Core.Interfaces.Metadata.Nfo; - -public interface IMusicVideoNfoReader -{ - Task> ReadFromFile(string fileName); -} diff --git a/ErsatzTV.Core/Interfaces/Metadata/Nfo/IOtherVideoNfoReader.cs b/ErsatzTV.Core/Interfaces/Metadata/Nfo/IOtherVideoNfoReader.cs deleted file mode 100644 index a678ffd03..000000000 --- a/ErsatzTV.Core/Interfaces/Metadata/Nfo/IOtherVideoNfoReader.cs +++ /dev/null @@ -1,8 +0,0 @@ -using ErsatzTV.Core.Metadata.Nfo; - -namespace ErsatzTV.Core.Interfaces.Metadata.Nfo; - -public interface IOtherVideoNfoReader -{ - Task> ReadFromFile(string fileName); -} diff --git a/ErsatzTV.Core/Interfaces/Metadata/Nfo/ITvShowNfoReader.cs b/ErsatzTV.Core/Interfaces/Metadata/Nfo/ITvShowNfoReader.cs deleted file mode 100644 index 839efaee3..000000000 --- a/ErsatzTV.Core/Interfaces/Metadata/Nfo/ITvShowNfoReader.cs +++ /dev/null @@ -1,8 +0,0 @@ -using ErsatzTV.Core.Metadata.Nfo; - -namespace ErsatzTV.Core.Interfaces.Metadata.Nfo; - -public interface ITvShowNfoReader -{ - Task> ReadFromFile(string fileName); -} diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs index f9e846131..d7d540928 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs @@ -13,7 +13,6 @@ public interface IMetadataRepository Task Update(Domain.Metadata metadata); Task Add(Domain.Metadata metadata); Task UpdateLocalStatistics(MediaItem mediaItem, MediaVersion incoming, bool updateVersion = true); - Task UpdatePlexStatistics(int mediaVersionId, MediaVersion incoming); Task UpdateArtworkPath(Artwork artwork); Task AddArtwork(Domain.Metadata metadata, Artwork artwork); Task RemoveArtwork(Domain.Metadata metadata, ArtworkKind artworkKind); diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs index 6a71fc4db..54885d163 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs @@ -18,5 +18,4 @@ public interface IMovieRepository Task UpdateSortTitle(MovieMetadata movieMetadata); Task AddDirector(MovieMetadata metadata, Director director); Task AddWriter(MovieMetadata metadata, Writer writer); - Task UpdatePath(int mediaFileId, string path); } diff --git a/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs b/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs index f4e01e999..55120cbc1 100644 --- a/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs @@ -125,36 +125,6 @@ public class FallbackMetadataProvider : IFallbackMetadataProvider return GetSongMetadata(path, metadata); } - public string GetSortTitle(string title) - { - if (string.IsNullOrWhiteSpace(title)) - { - return title; - } - - if (title.StartsWith("the ", StringComparison.OrdinalIgnoreCase)) - { - return title.Substring(4); - } - - if (title.StartsWith("a ", StringComparison.OrdinalIgnoreCase)) - { - return title.Substring(2); - } - - if (title.StartsWith("an ", StringComparison.OrdinalIgnoreCase)) - { - return title.Substring(3); - } - - if (title.StartsWith("Æ")) - { - return title.Replace("Æ", "E"); - } - - return title; - } - private List GetEpisodeMetadata(string fileName, EpisodeMetadata baseMetadata) { var result = new List(); diff --git a/ErsatzTV.Core/Metadata/LocalFileSystem.cs b/ErsatzTV.Core/Metadata/LocalFileSystem.cs index 96e95fd2f..b92f8ef71 100644 --- a/ErsatzTV.Core/Metadata/LocalFileSystem.cs +++ b/ErsatzTV.Core/Metadata/LocalFileSystem.cs @@ -94,7 +94,7 @@ public class LocalFileSystem : ILocalFileSystem public IEnumerable ListFiles(string folder, string searchPattern) { - if (Directory.Exists(folder)) + if (folder is not null && Directory.Exists(folder)) { try { diff --git a/ErsatzTV.Core/Metadata/Nfo/ActorNfo.cs b/ErsatzTV.Core/Metadata/Nfo/ActorNfo.cs deleted file mode 100644 index f0b0ecf59..000000000 --- a/ErsatzTV.Core/Metadata/Nfo/ActorNfo.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Xml.Serialization; - -namespace ErsatzTV.Core.Metadata.Nfo; - -public class ActorNfo -{ - [XmlElement("name")] - public string Name { get; set; } - - [XmlElement("role")] - public string Role { get; set; } - - [XmlElement("order")] - public int? Order { get; set; } - - [XmlElement("thumb")] - public string Thumb { get; set; } -} diff --git a/ErsatzTV.Core/Metadata/Nfo/ArtistNfo.cs b/ErsatzTV.Core/Metadata/Nfo/ArtistNfo.cs deleted file mode 100644 index d4d27e6c9..000000000 --- a/ErsatzTV.Core/Metadata/Nfo/ArtistNfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Xml.Serialization; - -namespace ErsatzTV.Core.Metadata.Nfo; - -[XmlRoot("artist")] -public class ArtistNfo -{ - [XmlElement("name")] - public string Name { get; set; } - - [XmlElement("disambiguation")] - public string Disambiguation { get; set; } - - [XmlElement("genre")] - public List Genres { get; set; } - - [XmlElement("style")] - public List Styles { get; set; } - - [XmlElement("mood")] - public List Moods { get; set; } - - [XmlElement("biography")] - public string Biography { get; set; } -} diff --git a/ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs b/ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs deleted file mode 100644 index 36ba4e3f1..000000000 --- a/ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Xml.Serialization; - -namespace ErsatzTV.Core.Metadata.Nfo; - -[XmlRoot("movie")] -public class MovieNfo -{ - [XmlElement("title")] - public string Title { get; set; } - - [XmlElement("sorttitle")] - public string SortTitle { get; set; } - - [XmlElement("outline")] - public string Outline { get; set; } - - [XmlElement("year")] - public int Year { get; set; } - - [XmlElement("mpaa")] - public string ContentRating { get; set; } - - [XmlElement("premiered")] - public Option Premiered { get; set; } - - [XmlElement("plot")] - public string Plot { get; set; } - - [XmlElement("tagline")] - public string Tagline { get; set; } - - [XmlElement("genre")] - public List Genres { get; set; } - - [XmlElement("tag")] - public List Tags { get; set; } - - [XmlElement("studio")] - public List Studios { get; set; } - - [XmlElement("actor")] - public List Actors { get; set; } - - [XmlElement("credits")] - public List Writers { get; set; } - - [XmlElement("director")] - public List Directors { get; set; } - - [XmlElement("uniqueid")] - public List UniqueIds { get; set; } -} diff --git a/ErsatzTV.Core/Metadata/Nfo/MusicVideoNfo.cs b/ErsatzTV.Core/Metadata/Nfo/MusicVideoNfo.cs deleted file mode 100644 index 45ac9817d..000000000 --- a/ErsatzTV.Core/Metadata/Nfo/MusicVideoNfo.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Xml.Serialization; - -namespace ErsatzTV.Core.Metadata.Nfo; - -[XmlRoot("musicvideo")] -public class MusicVideoNfo -{ - [XmlElement("artist")] - public List Artists { get; set; } - - [XmlElement("title")] - public string Title { get; set; } - - [XmlElement("album")] - public string Album { get; set; } - - [XmlElement("plot")] - public string Plot { get; set; } - - [XmlElement("track")] - public int Track { get; set; } - - [XmlElement("aired")] - public Option Aired { get; set; } - - [XmlElement("year")] - public int Year { get; set; } - - [XmlElement("genre")] - public List Genres { get; set; } - - [XmlElement("tag")] - public List Tags { get; set; } - - [XmlElement("studio")] - public List Studios { get; set; } -} diff --git a/ErsatzTV.Core/Metadata/Nfo/OtherVideoNfo.cs b/ErsatzTV.Core/Metadata/Nfo/OtherVideoNfo.cs deleted file mode 100644 index bc5276b0d..000000000 --- a/ErsatzTV.Core/Metadata/Nfo/OtherVideoNfo.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Xml.Serialization; - -namespace ErsatzTV.Core.Metadata.Nfo; - -[XmlRoot("movie")] -public class OtherVideoNfo -{ - [XmlElement("title")] - public string Title { get; set; } - - [XmlElement("sorttitle")] - public string SortTitle { get; set; } - - [XmlElement("outline")] - public string Outline { get; set; } - - [XmlElement("year")] - public int Year { get; set; } - - [XmlElement("mpaa")] - public string ContentRating { get; set; } - - [XmlElement("premiered")] - public Option Premiered { get; set; } - - [XmlElement("plot")] - public string Plot { get; set; } - - [XmlElement("tagline")] - public string Tagline { get; set; } - - [XmlElement("genre")] - public List Genres { get; set; } - - [XmlElement("tag")] - public List Tags { get; set; } - - [XmlElement("studio")] - public List Studios { get; set; } - - [XmlElement("actor")] - public List Actors { get; set; } - - [XmlElement("credits")] - public List Writers { get; set; } - - [XmlElement("director")] - public List Directors { get; set; } - - [XmlElement("uniqueid")] - public List UniqueIds { get; set; } -} diff --git a/ErsatzTV.Core/Metadata/Nfo/TvShowEpisodeNfo.cs b/ErsatzTV.Core/Metadata/Nfo/TvShowEpisodeNfo.cs deleted file mode 100644 index 579a1ab26..000000000 --- a/ErsatzTV.Core/Metadata/Nfo/TvShowEpisodeNfo.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Xml.Serialization; - -namespace ErsatzTV.Core.Metadata.Nfo; - -[XmlRoot("episodedetails")] -public class TvShowEpisodeNfo -{ - [XmlElement("showtitle")] - public string ShowTitle { get; set; } - - [XmlElement("title")] - public string Title { get; set; } - - [XmlElement("episode")] - public int Episode { get; set; } - - [XmlElement("season")] - public int Season { get; set; } - - [XmlElement("mpaa")] - public string ContentRating { get; set; } - - [XmlElement("aired")] - public Option Aired { get; set; } - - [XmlElement("plot")] - public string Plot { get; set; } - - [XmlElement("actor")] - public List Actors { get; set; } - - [XmlElement("credits")] - public List Writers { get; set; } - - [XmlElement("director")] - public List Directors { get; set; } - - [XmlElement("uniqueid")] - public List UniqueIds { get; set; } -} diff --git a/ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs b/ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs deleted file mode 100644 index f52ebc265..000000000 --- a/ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Xml.Serialization; - -namespace ErsatzTV.Core.Metadata.Nfo; - -[XmlRoot("tvshow")] -public class TvShowNfo -{ - [XmlElement("title")] - public string Title { get; set; } - - [XmlIgnore] - public int? Year { get; set; } - - [XmlElement("year")] - public string YearAsText - { - get => Year.HasValue ? Year.ToString() : null; - set => Year = !string.IsNullOrWhiteSpace(value) ? int.Parse(value) : default(int?); - } - - [XmlElement("plot")] - public string Plot { get; set; } - - [XmlElement("outline")] - public string Outline { get; set; } - - [XmlElement("tagline")] - public string Tagline { get; set; } - - [XmlElement("mpaa")] - public string ContentRating { get; set; } - - [XmlElement("premiered")] - public Option Premiered { get; set; } - - [XmlElement("genre")] - public List Genres { get; set; } - - [XmlElement("tag")] - public List Tags { get; set; } - - [XmlElement("studio")] - public List Studios { get; set; } - - [XmlElement("actor")] - public List Actors { get; set; } - - [XmlElement("uniqueid")] - public List UniqueIds { get; set; } -} diff --git a/ErsatzTV.Core/Metadata/Nfo/UniqueIdNfo.cs b/ErsatzTV.Core/Metadata/Nfo/UniqueIdNfo.cs deleted file mode 100644 index 3d0b6774f..000000000 --- a/ErsatzTV.Core/Metadata/Nfo/UniqueIdNfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Xml.Serialization; - -namespace ErsatzTV.Core.Metadata.Nfo; - -public class UniqueIdNfo -{ - [XmlAttribute("default")] - public bool Default { get; set; } - - [XmlAttribute("type")] - public string Type { get; set; } - - [XmlText] - public string Guid { get; set; } -} diff --git a/ErsatzTV.Core/Metadata/SortTitle.cs b/ErsatzTV.Core/Metadata/SortTitle.cs new file mode 100644 index 000000000..4810a94f1 --- /dev/null +++ b/ErsatzTV.Core/Metadata/SortTitle.cs @@ -0,0 +1,34 @@ +namespace ErsatzTV.Core.Metadata; + +public static class SortTitle +{ + public static string GetSortTitle(string title) + { + if (string.IsNullOrWhiteSpace(title)) + { + return title; + } + + if (title.StartsWith("the ", StringComparison.OrdinalIgnoreCase)) + { + return title[4..]; + } + + if (title.StartsWith("a ", StringComparison.OrdinalIgnoreCase)) + { + return title[2..]; + } + + if (title.StartsWith("an ", StringComparison.OrdinalIgnoreCase)) + { + return title[3..]; + } + + if (title.StartsWith("Æ")) + { + return title.Replace("Æ", "E"); + } + + return title; + } +} diff --git a/ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs index 8705a5f2c..90ea63433 100644 --- a/ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs @@ -5,7 +5,6 @@ namespace ErsatzTV.Core.Scheduling; public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator { - private readonly Collection _collection; private readonly IList _sortedMediaItems; public CustomOrderCollectionEnumerator( @@ -13,8 +12,6 @@ public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator IList mediaItems, CollectionEnumeratorState state) { - _collection = collection; - // TODO: this will break if we allow shows and seasons _sortedMediaItems = collection.CollectionItems .OrderBy(ci => ci.CustomIndex) diff --git a/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs b/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs index 9d6a6b3e2..2579245b8 100644 --- a/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs +++ b/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs @@ -213,6 +213,31 @@ public class PipelineBuilderBaseTests command.Should().Be( "-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -f concat -safe 0 -protocol_whitelist file,http,tcp,https,tcp,tls -probesize 32 -re -stream_loop -1 -i http://localhost:8080/ffmpeg/concat/1 -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -sc_threshold 0 -c copy -map_metadata -1 -metadata service_provider=\"ErsatzTV\" -metadata service_name=\"Some Channel\" -f mpegts -mpegts_flags +initial_discontinuity pipe:1"); } + + [Test] + public void Wrap_Segmenter_Test() + { + var resolution = new FrameSize(1920, 1080); + var concatInputFile = new ConcatInputFile("http://localhost:8080/iptv/channel/1.m3u8?mode=segmenter", resolution); + + var builder = new SoftwarePipelineBuilder( + HardwareAccelerationMode.None, + None, + None, + None, + None, + "", + "", + _logger); + FFmpegPipeline result = builder.WrapSegmenter(concatInputFile, FFmpegState.Concat(false, "Some Channel")); + + result.PipelineSteps.Should().HaveCountGreaterThan(0); + + string command = PrintCommand(None, None, None, concatInputFile, result); + + command.Should().Be( + "-nostdin -threads 1 -hide_banner -loglevel error -nostats -fflags +genpts+discardcorrupt+igndts -re -i http://localhost:8080/iptv/channel/1.m3u8?mode=segmenter -map 0 -c copy -metadata service_provider=\"ErsatzTV\" -metadata service_name=\"Some Channel\" -f mpegts pipe:1"); + } [Test] public void HlsDirect_Test() diff --git a/ErsatzTV.FFmpeg/CommandGenerator.cs b/ErsatzTV.FFmpeg/CommandGenerator.cs index dc2c06bfa..0af3a3189 100644 --- a/ErsatzTV.FFmpeg/CommandGenerator.cs +++ b/ErsatzTV.FFmpeg/CommandGenerator.cs @@ -97,7 +97,6 @@ public static class CommandGenerator foreach (IPipelineStep step in sortedSteps) { - arguments.AddRange(step.OutputOptions); } diff --git a/ErsatzTV.FFmpeg/Filter/ComplexFilter.cs b/ErsatzTV.FFmpeg/Filter/ComplexFilter.cs index 519279d67..d2c198040 100644 --- a/ErsatzTV.FFmpeg/Filter/ComplexFilter.cs +++ b/ErsatzTV.FFmpeg/Filter/ComplexFilter.cs @@ -1,50 +1,35 @@ using ErsatzTV.FFmpeg.Environment; -using ErsatzTV.FFmpeg.Format; -using Microsoft.Extensions.Logging; +using ErsatzTV.FFmpeg.Pipeline; namespace ErsatzTV.FFmpeg.Filter; public class ComplexFilter : IPipelineStep { - private readonly FrameState _currentState; - private readonly FFmpegState _ffmpegState; - private readonly string _fontsDir; - private readonly ILogger _logger; private readonly Option _maybeAudioInputFile; private readonly Option _maybeSubtitleInputFile; - private readonly Option _desiredPixelFormat; + private readonly PipelineContext _pipelineContext; + private readonly FilterChain _filterChain; private readonly Option _maybeVideoInputFile; private readonly Option _maybeWatermarkInputFile; - private readonly FrameSize _resolution; private readonly List _outputOptions; - private readonly List _pipelineSteps; private readonly IList _arguments; public ComplexFilter( - FrameState currentState, - FFmpegState ffmpegState, Option maybeVideoInputFile, Option maybeAudioInputFile, Option maybeWatermarkInputFile, Option maybeSubtitleInputFile, - Option desiredPixelFormat, - FrameSize resolution, - string fontsDir, - ILogger logger) + PipelineContext pipelineContext, + FilterChain filterChain) { - _currentState = currentState; - _ffmpegState = ffmpegState; _maybeVideoInputFile = maybeVideoInputFile; _maybeAudioInputFile = maybeAudioInputFile; _maybeWatermarkInputFile = maybeWatermarkInputFile; _maybeSubtitleInputFile = maybeSubtitleInputFile; - _desiredPixelFormat = desiredPixelFormat; - _resolution = resolution; - _fontsDir = fontsDir; - _logger = logger; + _pipelineContext = pipelineContext; + _filterChain = filterChain; _outputOptions = new List(); - _pipelineSteps = new List(); _arguments = Arguments(); } @@ -54,18 +39,18 @@ public class ComplexFilter : IPipelineStep public IList InputOptions(InputFile inputFile) => Array.Empty(); public IList FilterOptions => _arguments; public IList OutputOptions => _outputOptions; - public IList PipelineSteps => _pipelineSteps; public FrameState NextState(FrameState currentState) => currentState; + // for testing + public FilterChain FilterChain => _filterChain; + private List Arguments() { - var state = _currentState; - var audioLabel = "0:a"; var videoLabel = "0:v"; - string watermarkLabel; - string subtitleLabel; + string? watermarkLabel = null; + string? subtitleLabel = null; var result = new List(); @@ -88,7 +73,10 @@ public class ComplexFilter : IPipelineStep foreach ((string path, _) in _maybeAudioInputFile) { - if (!distinctPaths.Contains(path)) + if (!distinctPaths.Contains(path) || + // use audio as a separate input with vaapi/qsv + _pipelineContext.HardwareAccelerationMode is HardwareAccelerationMode.Vaapi + or HardwareAccelerationMode.Qsv) { distinctPaths.Add(path); } @@ -116,48 +104,30 @@ public class ComplexFilter : IPipelineStep foreach ((int index, _, _) in videoInputFile.Streams) { videoLabel = $"{inputIndex}:{index}"; - if (videoInputFile.FilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) + if (_filterChain.VideoFilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) { videoFilterComplex += $"[{inputIndex}:{index}]"; videoFilterComplex += string.Join( ",", - videoInputFile.FilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); + _filterChain.VideoFilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); videoLabel = "[v]"; videoFilterComplex += videoLabel; } } } - foreach (AudioInputFile audioInputFile in _maybeAudioInputFile) - { - int inputIndex = distinctPaths.IndexOf(audioInputFile.Path); - foreach ((int index, _, _) in audioInputFile.Streams) - { - audioLabel = $"{inputIndex}:{index}"; - if (audioInputFile.FilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) - { - audioFilterComplex += $"[{inputIndex}:{index}]"; - audioFilterComplex += string.Join( - ",", - audioInputFile.FilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); - audioLabel = "[a]"; - audioFilterComplex += audioLabel; - } - } - } - foreach (WatermarkInputFile watermarkInputFile in _maybeWatermarkInputFile) { int inputIndex = distinctPaths.IndexOf(watermarkInputFile.Path); foreach ((int index, _, _) in watermarkInputFile.Streams) { watermarkLabel = $"{inputIndex}:{index}"; - if (watermarkInputFile.FilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) + if (_filterChain.WatermarkFilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) { watermarkFilterComplex += $"[{inputIndex}:{index}]"; watermarkFilterComplex += string.Join( ",", - watermarkInputFile.FilterSteps.Select(f => f.Filter) + _filterChain.WatermarkFilterSteps.Select(f => f.Filter) .Filter(s => !string.IsNullOrWhiteSpace(s))); watermarkLabel = "[wm]"; watermarkFilterComplex += watermarkLabel; @@ -166,66 +136,6 @@ public class ComplexFilter : IPipelineStep { watermarkLabel = $"[{watermarkLabel}]"; } - - foreach (VideoInputFile videoInputFile in _maybeVideoInputFile) - foreach (VideoStream stream in videoInputFile.VideoStreams) - { - IPipelineFilterStep overlayFilter = AvailableWatermarkOverlayFilters.ForAcceleration( - _ffmpegState.EncoderHardwareAccelerationMode, - watermarkInputFile.DesiredState, - _resolution, - stream.SquarePixelFrameSize(_resolution), - _logger); - - if (overlayFilter.Filter != string.Empty) - { - _pipelineSteps.Add(overlayFilter); - - state = overlayFilter.NextState(state); - - string tempVideoLabel = string.IsNullOrWhiteSpace(videoFilterComplex) - ? $"[{videoLabel}]" - : videoLabel; - - // vaapi uses software overlay and needs to upload - // videotoolbox seems to require a hwupload for hevc - // also wait to upload if a subtitle overlay is coming - string uploadDownloadFilter = string.Empty; - if (_maybeSubtitleInputFile.IsNone && - (_ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.Vaapi || - _ffmpegState.EncoderHardwareAccelerationMode == - HardwareAccelerationMode.VideoToolbox && - state.VideoFormat == VideoFormat.Hevc)) - { - var hardwareUpload = new HardwareUploadFilter(_ffmpegState); - _pipelineSteps.Add(hardwareUpload); - uploadDownloadFilter = hardwareUpload.Filter; - state = state with { FrameDataLocation = FrameDataLocation.Hardware }; - } - - if (_maybeSubtitleInputFile.Map(s => !s.IsImageBased).IfNone(false) && - _ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.Vaapi && - _ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.VideoToolbox && - _ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.Amf) - { - var hardwareDownload = new HardwareDownloadFilter(state); - _pipelineSteps.Add(hardwareDownload); - uploadDownloadFilter = hardwareDownload.Filter; - state = state with { FrameDataLocation = FrameDataLocation.Software }; - } - - if (!string.IsNullOrWhiteSpace(uploadDownloadFilter)) - { - uploadDownloadFilter = "," + uploadDownloadFilter; - } - - watermarkOverlayFilterComplex = - $"{tempVideoLabel}{watermarkLabel}{overlayFilter.Filter}{uploadDownloadFilter}[vf]"; - - // change the mapped label - videoLabel = "[vf]"; - } - } } } @@ -235,12 +145,12 @@ public class ComplexFilter : IPipelineStep foreach ((int index, _, _) in subtitleInputFile.Streams) { subtitleLabel = $"{inputIndex}:{index}"; - if (subtitleInputFile.FilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) + if (_filterChain.SubtitleFilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) { subtitleFilterComplex += $"[{inputIndex}:{index}]"; subtitleFilterComplex += string.Join( ",", - subtitleInputFile.FilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); + _filterChain.SubtitleFilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); subtitleLabel = "[st]"; subtitleFilterComplex += subtitleLabel; } @@ -248,116 +158,61 @@ public class ComplexFilter : IPipelineStep { subtitleLabel = $"[{subtitleLabel}]"; } - - string filter; - if (subtitleInputFile.IsImageBased) - { - IPipelineFilterStep overlayFilter = AvailableSubtitleOverlayFilters.ForAcceleration( - _ffmpegState.EncoderHardwareAccelerationMode); - _pipelineSteps.Add(overlayFilter); - state = overlayFilter.NextState(state); - filter = overlayFilter.Filter; - } - else - { - subtitleLabel = string.Empty; - var subtitlesFilter = new SubtitlesFilter(_fontsDir, subtitleInputFile); - _pipelineSteps.Add(subtitlesFilter); - state = subtitlesFilter.NextState(state); - filter = subtitlesFilter.Filter; - } - - if (filter != string.Empty) - { - string tempVideoLabel = videoLabel.StartsWith('[') && videoLabel.EndsWith(']') - ? videoLabel - : $"[{videoLabel}]"; - - // vaapi uses software overlay and needs to upload - // videotoolbox seems to require a hwupload for hevc - string uploadFilter = string.Empty; - if (_ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.Vaapi - || _ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.VideoToolbox && - state.VideoFormat == VideoFormat.Hevc) - { - var hardwareUpload = new HardwareUploadFilter(_ffmpegState); - _pipelineSteps.Add(hardwareUpload); - uploadFilter = hardwareUpload.Filter; - if (!string.IsNullOrWhiteSpace(uploadFilter)) - { - state = state with { FrameDataLocation = FrameDataLocation.Hardware }; - } - } - - if (!string.IsNullOrWhiteSpace(uploadFilter)) - { - uploadFilter = "," + uploadFilter; - } - - subtitleOverlayFilterComplex = $"{tempVideoLabel}{subtitleLabel}{filter}{uploadFilter}[vst]"; - - // change the mapped label - videoLabel = "[vst]"; - } } } - - foreach (VideoStream videoStream in _maybeVideoInputFile.Map(vif => vif.VideoStreams).Flatten()) - foreach (IPixelFormat pixelFormat in _desiredPixelFormat) + + // overlay subtitle + if (!string.IsNullOrWhiteSpace(subtitleLabel) && _filterChain.SubtitleOverlayFilterSteps.Any()) { - _logger.LogDebug("Desired pixel format {PixelFormat}", pixelFormat); - - string tempVideoLabel = videoLabel.StartsWith("[") && videoLabel.EndsWith("]") - ? videoLabel - : $"[{videoLabel}]"; - - // normalize pixel format and color params - string filter = string.Empty; + subtitleOverlayFilterComplex += $"{ProperLabel(videoLabel)}{ProperLabel(subtitleLabel)}"; + subtitleOverlayFilterComplex += string.Join( + ",", + _filterChain.SubtitleOverlayFilterSteps.Select(f => f.Filter) + .Filter(s => !string.IsNullOrWhiteSpace(s))); + videoLabel = "[vst]"; + subtitleOverlayFilterComplex += videoLabel; + } - if (!videoStream.ColorParams.IsBt709) - { - _logger.LogDebug("Adding colorspace filter"); - var colorspace = new ColorspaceFilter(_currentState, videoStream, pixelFormat); - _pipelineSteps.Add(colorspace); - filter = colorspace.Filter; - } + // overlay watermark + if (!string.IsNullOrWhiteSpace(watermarkLabel) && _filterChain.WatermarkOverlayFilterSteps.Any()) + { + watermarkOverlayFilterComplex += $"{ProperLabel(videoLabel)}{ProperLabel(watermarkLabel)}"; + watermarkOverlayFilterComplex += string.Join( + ",", + _filterChain.WatermarkOverlayFilterSteps.Select(f => f.Filter) + .Filter(s => !string.IsNullOrWhiteSpace(s))); + videoLabel = "[vwm]"; + watermarkOverlayFilterComplex += videoLabel; + } + + // pixel format + if (_filterChain.PixelFormatFilterSteps.Any()) + { + pixelFormatFilterComplex += $"{ProperLabel(videoLabel)}"; + pixelFormatFilterComplex += string.Join( + ",", + _filterChain.PixelFormatFilterSteps.Select(f => f.Filter) + .Filter(s => !string.IsNullOrWhiteSpace(s))); + videoLabel = "[vpf]"; + pixelFormatFilterComplex += videoLabel; + } - if (state.PixelFormat.Map(f => f.FFmpegName) != pixelFormat.FFmpegName) + foreach (AudioInputFile audioInputFile in _maybeAudioInputFile) + { + int inputIndex = distinctPaths.LastIndexOf(audioInputFile.Path); + foreach ((int index, _, _) in audioInputFile.Streams) { - _logger.LogDebug( - "Format {A} doesn't equal {B}", - state.PixelFormat.Map(f => f.FFmpegName), - pixelFormat.FFmpegName); - - if (videoStream.ColorParams.IsHdr) - { - _logger.LogWarning("HDR tone mapping is not implemented; colors may appear incorrect"); - continue; - } - - if (state.FrameDataLocation == FrameDataLocation.Software) - { - _logger.LogDebug("Frame data location is SOFTWARE"); - _outputOptions.AddRange(new[] { "-pix_fmt", pixelFormat.FFmpegName }); - } - else + audioLabel = $"{inputIndex}:{index}"; + if (audioInputFile.FilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) { - _logger.LogDebug("Frame data location is HARDWARE"); - filter = _ffmpegState.EncoderHardwareAccelerationMode switch - { - HardwareAccelerationMode.Nvenc => $"{filter},scale_cuda=format={pixelFormat.FFmpegName}", - _ => filter - }; + audioFilterComplex += $"[{inputIndex}:{index}]"; + audioFilterComplex += string.Join( + ",", + audioInputFile.FilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); + audioLabel = "[a]"; + audioFilterComplex += audioLabel; } } - - if (!string.IsNullOrWhiteSpace(filter)) - { - pixelFormatFilterComplex = $"{tempVideoLabel}{filter}[vpf]"; - - // change the mapped label - videoLabel = "[vpf]"; - } } var filterComplex = string.Join( @@ -366,13 +221,12 @@ public class ComplexFilter : IPipelineStep { audioFilterComplex, videoFilterComplex, - watermarkFilterComplex, subtitleFilterComplex, - watermarkOverlayFilterComplex, + watermarkFilterComplex, subtitleOverlayFilterComplex, + watermarkOverlayFilterComplex, pixelFormatFilterComplex - }.Where( - s => !string.IsNullOrWhiteSpace(s))); + }.Where(s => !string.IsNullOrWhiteSpace(s))); if (!string.IsNullOrWhiteSpace(filterComplex)) { @@ -393,4 +247,6 @@ public class ComplexFilter : IPipelineStep return result; } + + private string ProperLabel(string label) => label.StartsWith("[") ? label : $"[{label}]"; } diff --git a/ErsatzTV.FFmpeg/Filter/NewComplexFilter.cs b/ErsatzTV.FFmpeg/Filter/NewComplexFilter.cs deleted file mode 100644 index 9b02b0efb..000000000 --- a/ErsatzTV.FFmpeg/Filter/NewComplexFilter.cs +++ /dev/null @@ -1,252 +0,0 @@ -using ErsatzTV.FFmpeg.Environment; -using ErsatzTV.FFmpeg.Pipeline; - -namespace ErsatzTV.FFmpeg.Filter; - -public class NewComplexFilter : IPipelineStep -{ - private readonly Option _maybeAudioInputFile; - private readonly Option _maybeSubtitleInputFile; - private readonly PipelineContext _pipelineContext; - private readonly FilterChain _filterChain; - private readonly Option _maybeVideoInputFile; - private readonly Option _maybeWatermarkInputFile; - private readonly List _outputOptions; - private readonly IList _arguments; - - public NewComplexFilter( - Option maybeVideoInputFile, - Option maybeAudioInputFile, - Option maybeWatermarkInputFile, - Option maybeSubtitleInputFile, - PipelineContext pipelineContext, - FilterChain filterChain) - { - _maybeVideoInputFile = maybeVideoInputFile; - _maybeAudioInputFile = maybeAudioInputFile; - _maybeWatermarkInputFile = maybeWatermarkInputFile; - _maybeSubtitleInputFile = maybeSubtitleInputFile; - _pipelineContext = pipelineContext; - _filterChain = filterChain; - - _outputOptions = new List(); - - _arguments = Arguments(); - } - - public IList EnvironmentVariables => Array.Empty(); - public IList GlobalOptions => Array.Empty(); - public IList InputOptions(InputFile inputFile) => Array.Empty(); - public IList FilterOptions => _arguments; - public IList OutputOptions => _outputOptions; - - public FrameState NextState(FrameState currentState) => currentState; - - // for testing - public FilterChain FilterChain => _filterChain; - - private List Arguments() - { - var audioLabel = "0:a"; - var videoLabel = "0:v"; - string? watermarkLabel = null; - string? subtitleLabel = null; - - var result = new List(); - - string audioFilterComplex = string.Empty; - string videoFilterComplex = string.Empty; - string watermarkFilterComplex = string.Empty; - string watermarkOverlayFilterComplex = string.Empty; - string subtitleFilterComplex = string.Empty; - string subtitleOverlayFilterComplex = string.Empty; - string pixelFormatFilterComplex = string.Empty; - - var distinctPaths = new List(); - foreach ((string path, _) in _maybeVideoInputFile) - { - if (!distinctPaths.Contains(path)) - { - distinctPaths.Add(path); - } - } - - foreach ((string path, _) in _maybeAudioInputFile) - { - if (!distinctPaths.Contains(path) || - // use audio as a separate input with vaapi/qsv - _pipelineContext.HardwareAccelerationMode is HardwareAccelerationMode.Vaapi - or HardwareAccelerationMode.Qsv) - { - distinctPaths.Add(path); - } - } - - foreach ((string path, _) in _maybeWatermarkInputFile) - { - if (!distinctPaths.Contains(path)) - { - distinctPaths.Add(path); - } - } - - foreach ((string path, _) in _maybeSubtitleInputFile) - { - if (!distinctPaths.Contains(path)) - { - distinctPaths.Add(path); - } - } - - foreach (VideoInputFile videoInputFile in _maybeVideoInputFile) - { - int inputIndex = distinctPaths.IndexOf(videoInputFile.Path); - foreach ((int index, _, _) in videoInputFile.Streams) - { - videoLabel = $"{inputIndex}:{index}"; - if (_filterChain.VideoFilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) - { - videoFilterComplex += $"[{inputIndex}:{index}]"; - videoFilterComplex += string.Join( - ",", - _filterChain.VideoFilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); - videoLabel = "[v]"; - videoFilterComplex += videoLabel; - } - } - } - - foreach (WatermarkInputFile watermarkInputFile in _maybeWatermarkInputFile) - { - int inputIndex = distinctPaths.IndexOf(watermarkInputFile.Path); - foreach ((int index, _, _) in watermarkInputFile.Streams) - { - watermarkLabel = $"{inputIndex}:{index}"; - if (_filterChain.WatermarkFilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) - { - watermarkFilterComplex += $"[{inputIndex}:{index}]"; - watermarkFilterComplex += string.Join( - ",", - _filterChain.WatermarkFilterSteps.Select(f => f.Filter) - .Filter(s => !string.IsNullOrWhiteSpace(s))); - watermarkLabel = "[wm]"; - watermarkFilterComplex += watermarkLabel; - } - else - { - watermarkLabel = $"[{watermarkLabel}]"; - } - } - } - - foreach (SubtitleInputFile subtitleInputFile in _maybeSubtitleInputFile.Filter(s => !s.Copy)) - { - int inputIndex = distinctPaths.IndexOf(subtitleInputFile.Path); - foreach ((int index, _, _) in subtitleInputFile.Streams) - { - subtitleLabel = $"{inputIndex}:{index}"; - if (_filterChain.SubtitleFilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) - { - subtitleFilterComplex += $"[{inputIndex}:{index}]"; - subtitleFilterComplex += string.Join( - ",", - _filterChain.SubtitleFilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); - subtitleLabel = "[st]"; - subtitleFilterComplex += subtitleLabel; - } - else - { - subtitleLabel = $"[{subtitleLabel}]"; - } - } - } - - // overlay subtitle - if (!string.IsNullOrWhiteSpace(subtitleLabel) && _filterChain.SubtitleOverlayFilterSteps.Any()) - { - subtitleOverlayFilterComplex += $"{ProperLabel(videoLabel)}{ProperLabel(subtitleLabel)}"; - subtitleOverlayFilterComplex += string.Join( - ",", - _filterChain.SubtitleOverlayFilterSteps.Select(f => f.Filter) - .Filter(s => !string.IsNullOrWhiteSpace(s))); - videoLabel = "[vst]"; - subtitleOverlayFilterComplex += videoLabel; - } - - // overlay watermark - if (!string.IsNullOrWhiteSpace(watermarkLabel) && _filterChain.WatermarkOverlayFilterSteps.Any()) - { - watermarkOverlayFilterComplex += $"{ProperLabel(videoLabel)}{ProperLabel(watermarkLabel)}"; - watermarkOverlayFilterComplex += string.Join( - ",", - _filterChain.WatermarkOverlayFilterSteps.Select(f => f.Filter) - .Filter(s => !string.IsNullOrWhiteSpace(s))); - videoLabel = "[vwm]"; - watermarkOverlayFilterComplex += videoLabel; - } - - // pixel format - if (_filterChain.PixelFormatFilterSteps.Any()) - { - pixelFormatFilterComplex += $"{ProperLabel(videoLabel)}"; - pixelFormatFilterComplex += string.Join( - ",", - _filterChain.PixelFormatFilterSteps.Select(f => f.Filter) - .Filter(s => !string.IsNullOrWhiteSpace(s))); - videoLabel = "[vpf]"; - pixelFormatFilterComplex += videoLabel; - } - - foreach (AudioInputFile audioInputFile in _maybeAudioInputFile) - { - int inputIndex = distinctPaths.LastIndexOf(audioInputFile.Path); - foreach ((int index, _, _) in audioInputFile.Streams) - { - audioLabel = $"{inputIndex}:{index}"; - if (audioInputFile.FilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) - { - audioFilterComplex += $"[{inputIndex}:{index}]"; - audioFilterComplex += string.Join( - ",", - audioInputFile.FilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); - audioLabel = "[a]"; - audioFilterComplex += audioLabel; - } - } - } - - var filterComplex = string.Join( - ";", - new[] - { - audioFilterComplex, - videoFilterComplex, - subtitleFilterComplex, - watermarkFilterComplex, - subtitleOverlayFilterComplex, - watermarkOverlayFilterComplex, - pixelFormatFilterComplex - }.Where(s => !string.IsNullOrWhiteSpace(s))); - - if (!string.IsNullOrWhiteSpace(filterComplex)) - { - result.AddRange(new[] { "-filter_complex", filterComplex }); - } - - result.AddRange(new[] { "-map", audioLabel, "-map", videoLabel }); - - foreach (SubtitleInputFile subtitleInputFile in _maybeSubtitleInputFile.Filter(s => s.Copy)) - { - int inputIndex = distinctPaths.IndexOf(subtitleInputFile.Path); - foreach ((int index, _, _) in subtitleInputFile.Streams) - { - subtitleLabel = $"{inputIndex}:{index}"; - result.AddRange(new[] { "-map", subtitleLabel }); - } - } - - return result; - } - - private string ProperLabel(string label) => label.StartsWith("[") ? label : $"[{label}]"; -} diff --git a/ErsatzTV.FFmpeg/Option/MapAllStreamsOutputOption.cs b/ErsatzTV.FFmpeg/Option/MapAllStreamsOutputOption.cs new file mode 100644 index 000000000..775d29d67 --- /dev/null +++ b/ErsatzTV.FFmpeg/Option/MapAllStreamsOutputOption.cs @@ -0,0 +1,6 @@ +namespace ErsatzTV.FFmpeg.Option; + +public class MapAllStreamsOutputOption : OutputOption +{ + public override IList OutputOptions => new[] { "-map", "0" }; +} diff --git a/ErsatzTV.FFmpeg/OutputFormat/OutputFormatMpegTs.cs b/ErsatzTV.FFmpeg/OutputFormat/OutputFormatMpegTs.cs index ea2e9010b..8b22a30e9 100644 --- a/ErsatzTV.FFmpeg/OutputFormat/OutputFormatMpegTs.cs +++ b/ErsatzTV.FFmpeg/OutputFormat/OutputFormatMpegTs.cs @@ -4,14 +4,21 @@ namespace ErsatzTV.FFmpeg.OutputFormat; public class OutputFormatMpegTs : IPipelineStep { + private readonly bool _initialDiscontinuity; + + public OutputFormatMpegTs(bool initialDiscontinuity = true) + { + _initialDiscontinuity = initialDiscontinuity; + } + public IList EnvironmentVariables => Array.Empty(); public IList GlobalOptions => Array.Empty(); public IList InputOptions(InputFile inputFile) => Array.Empty(); public IList FilterOptions => Array.Empty(); - // always force an initial discontinuity - public IList OutputOptions => - new List { "-f", "mpegts", "-mpegts_flags", "+initial_discontinuity" }; + public IList OutputOptions => _initialDiscontinuity + ? new List { "-f", "mpegts", "-mpegts_flags", "+initial_discontinuity" } + : new List { "-f", "mpegts" }; public FrameState NextState(FrameState currentState) => currentState; } diff --git a/ErsatzTV.FFmpeg/Pipeline/IPipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/IPipelineBuilder.cs index 34ef796a3..894232aca 100644 --- a/ErsatzTV.FFmpeg/Pipeline/IPipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/IPipelineBuilder.cs @@ -4,5 +4,6 @@ public interface IPipelineBuilder { FFmpegPipeline Resize(string outputFile, FrameSize scaledSize); FFmpegPipeline Concat(ConcatInputFile concatInputFile, FFmpegState ffmpegState); + FFmpegPipeline WrapSegmenter(ConcatInputFile concatInputFile, FFmpegState ffmpegState); FFmpegPipeline Build(FFmpegState ffmpegState, FrameState desiredState); } diff --git a/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs b/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs index 52c516735..7b3937dec 100644 --- a/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs +++ b/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs @@ -110,6 +110,31 @@ public abstract class PipelineBuilderBase : IPipelineBuilder return new FFmpegPipeline(pipelineSteps); } + public FFmpegPipeline WrapSegmenter(ConcatInputFile concatInputFile, FFmpegState ffmpegState) + { + var pipelineSteps = new List + { + new NoStandardInputOption(), + new ThreadCountOption(1), + new HideBannerOption(), + new LoglevelErrorOption(), + new NoStatsOption(), + new StandardFormatFlags(), + new MapAllStreamsOutputOption(), + new EncoderCopyAll() + }; + + concatInputFile.AddOption(new RealtimeInputOption()); + + SetMetadataServiceProvider(ffmpegState, pipelineSteps); + SetMetadataServiceName(ffmpegState, pipelineSteps); + + pipelineSteps.Add(new OutputFormatMpegTs(initialDiscontinuity: false)); + pipelineSteps.Add(new PipeProtocol()); + + return new FFmpegPipeline(pipelineSteps); + } + public FFmpegPipeline Build(FFmpegState ffmpegState, FrameState desiredState) { var pipelineSteps = new List @@ -180,7 +205,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder SetMetadataAudioLanguage(ffmpegState, pipelineSteps); SetOutputFormat(ffmpegState, desiredState, pipelineSteps, videoStream); - var complexFilter = new NewComplexFilter( + var complexFilter = new ComplexFilter( _videoInputFile, _audioInputFile, _watermarkInputFile, @@ -193,7 +218,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder return new FFmpegPipeline(pipelineSteps); } - protected Option LogUnknownDecoder( + private Option LogUnknownDecoder( HardwareAccelerationMode hardwareAccelerationMode, string videoFormat, string pixelFormat) @@ -206,7 +231,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder return Option.None; } - protected Option LogUnknownEncoder(HardwareAccelerationMode hardwareAccelerationMode, string videoFormat) + private Option LogUnknownEncoder(HardwareAccelerationMode hardwareAccelerationMode, string videoFormat) { _logger.LogWarning( "Unable to determine video encoder for {AccelMode} - {VideoFormat}; may have playback issues", diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs index 6151a6329..e2aeca936 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs @@ -239,20 +239,6 @@ public class MetadataRepository : IMetadataRepository () => Task.FromResult(false)); } - public async Task UpdatePlexStatistics(int mediaVersionId, MediaVersion incoming) - { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); - return await dbContext.Connection.ExecuteAsync( - @"UPDATE MediaVersion SET - DateUpdated = @DateUpdated - WHERE Id = @MediaVersionId", - new - { - incoming.DateUpdated, - MediaVersionId = mediaVersionId - }).Map(result => result > 0); - } - public async Task UpdateArtworkPath(Artwork artwork) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs index 5f0f8f939..8990d1966 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs @@ -219,14 +219,6 @@ public class MovieRepository : IMovieRepository new { writer.Name, MetadataId = metadata.Id }).Map(result => result > 0); } - public async Task UpdatePath(int mediaFileId, string path) - { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); - return await dbContext.Connection.ExecuteAsync( - "UPDATE MediaFile SET Path = @Path WHERE Id = @MediaFileId", - new { Path = path, MediaFileId = mediaFileId }).Map(_ => Unit.Default); - } - private static async Task>> AddMovie( TvContext dbContext, int libraryPathId, diff --git a/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs b/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs index 54410c1c5..ebe19d572 100644 --- a/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs +++ b/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs @@ -2,7 +2,7 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Emby; using ErsatzTV.Core.Interfaces.Emby; -using ErsatzTV.Core.Interfaces.Metadata; +using ErsatzTV.Core.Metadata; using ErsatzTV.Infrastructure.Emby.Models; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; @@ -13,17 +13,14 @@ namespace ErsatzTV.Infrastructure.Emby; public class EmbyApiClient : IEmbyApiClient { private readonly IEmbyPathReplacementService _embyPathReplacementService; - private readonly IFallbackMetadataProvider _fallbackMetadataProvider; private readonly ILogger _logger; private readonly IMemoryCache _memoryCache; public EmbyApiClient( - IFallbackMetadataProvider fallbackMetadataProvider, IMemoryCache memoryCache, IEmbyPathReplacementService embyPathReplacementService, ILogger logger) { - _fallbackMetadataProvider = fallbackMetadataProvider; _memoryCache = memoryCache; _embyPathReplacementService = embyPathReplacementService; _logger = logger; @@ -375,7 +372,7 @@ public class EmbyApiClient : IEmbyApiClient { MetadataKind = MetadataKind.External, Title = item.Name, - SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name), + SortTitle = SortTitle.GetSortTitle(item.Name), Plot = item.Overview, Year = item.ProductionYear, Tagline = Optional(item.Taglines).Flatten().HeadOrNone().IfNone(string.Empty), @@ -500,7 +497,7 @@ public class EmbyApiClient : IEmbyApiClient { MetadataKind = MetadataKind.External, Title = item.Name, - SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name), + SortTitle = SortTitle.GetSortTitle(item.Name), Plot = item.Overview, Year = item.ProductionYear, Tagline = Optional(item.Taglines).Flatten().HeadOrNone().IfNone(string.Empty), @@ -572,7 +569,7 @@ public class EmbyApiClient : IEmbyApiClient { MetadataKind = MetadataKind.External, Title = item.Name, - SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name), + SortTitle = SortTitle.GetSortTitle(item.Name), Year = item.ProductionYear, DateAdded = dateAdded, Artwork = new List(), @@ -691,7 +688,7 @@ public class EmbyApiClient : IEmbyApiClient { MetadataKind = MetadataKind.External, Title = item.Name, - SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name), + SortTitle = SortTitle.GetSortTitle(item.Name), Plot = item.Overview, Year = item.ProductionYear, DateAdded = dateAdded, diff --git a/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs b/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs index f4442e41f..862661fc4 100644 --- a/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs +++ b/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs @@ -3,6 +3,7 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Jellyfin; +using ErsatzTV.Core.Metadata; using ErsatzTV.Infrastructure.Jellyfin.Models; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; @@ -12,19 +13,16 @@ namespace ErsatzTV.Infrastructure.Jellyfin; public class JellyfinApiClient : IJellyfinApiClient { - private readonly IFallbackMetadataProvider _fallbackMetadataProvider; private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService; private readonly ILogger _logger; private readonly IMemoryCache _memoryCache; public JellyfinApiClient( IMemoryCache memoryCache, - IFallbackMetadataProvider fallbackMetadataProvider, IJellyfinPathReplacementService jellyfinPathReplacementService, ILogger logger) { _memoryCache = memoryCache; - _fallbackMetadataProvider = fallbackMetadataProvider; _jellyfinPathReplacementService = jellyfinPathReplacementService; _logger = logger; } @@ -433,7 +431,7 @@ public class JellyfinApiClient : IJellyfinApiClient { MetadataKind = MetadataKind.External, Title = item.Name, - SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name), + SortTitle = SortTitle.GetSortTitle(item.Name), Plot = item.Overview, Year = item.ProductionYear, Tagline = Optional(item.Taglines).Flatten().HeadOrNone().IfNone(string.Empty), @@ -558,7 +556,7 @@ public class JellyfinApiClient : IJellyfinApiClient { MetadataKind = MetadataKind.External, Title = item.Name, - SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name), + SortTitle = SortTitle.GetSortTitle(item.Name), Plot = item.Overview, Year = item.ProductionYear, Tagline = Optional(item.Taglines).Flatten().HeadOrNone().IfNone(string.Empty), @@ -630,7 +628,7 @@ public class JellyfinApiClient : IJellyfinApiClient { MetadataKind = MetadataKind.External, Title = item.Name, - SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name), + SortTitle = SortTitle.GetSortTitle(item.Name), Year = item.ProductionYear, DateAdded = dateAdded, Artwork = new List(), @@ -773,7 +771,7 @@ public class JellyfinApiClient : IJellyfinApiClient { MetadataKind = MetadataKind.External, Title = item.Name, - SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name), + SortTitle = SortTitle.GetSortTitle(item.Name), Plot = item.Overview, Year = item.ProductionYear, DateAdded = dateAdded, diff --git a/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs b/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs index d8009ffa2..5291a4ae9 100644 --- a/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs +++ b/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs @@ -3,6 +3,7 @@ using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Plex; +using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Plex; using ErsatzTV.Infrastructure.Plex.Models; using Microsoft.Extensions.Logging; @@ -12,16 +13,11 @@ namespace ErsatzTV.Infrastructure.Plex; public class PlexServerApiClient : IPlexServerApiClient { - private readonly IFallbackMetadataProvider _fallbackMetadataProvider; private readonly ILogger _logger; private readonly PlexEtag _plexEtag; - public PlexServerApiClient( - IFallbackMetadataProvider fallbackMetadataProvider, - PlexEtag plexEtag, - ILogger logger) + public PlexServerApiClient(PlexEtag plexEtag, ILogger logger) { - _fallbackMetadataProvider = fallbackMetadataProvider; _plexEtag = plexEtag; _logger = logger; } @@ -454,7 +450,7 @@ public class PlexServerApiClient : IPlexServerApiClient { MetadataKind = MetadataKind.External, Title = response.Title, - SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title), + SortTitle = SortTitle.GetSortTitle(response.Title), Plot = response.Summary, Year = response.Year, Tagline = response.Tagline, @@ -638,7 +634,7 @@ public class PlexServerApiClient : IPlexServerApiClient { MetadataKind = MetadataKind.External, Title = response.Title, - SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title), + SortTitle = SortTitle.GetSortTitle(response.Title), Plot = response.Summary, Year = response.Year, Tagline = response.Tagline, @@ -734,7 +730,7 @@ public class PlexServerApiClient : IPlexServerApiClient { MetadataKind = MetadataKind.External, Title = response.Title, - SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title), + SortTitle = SortTitle.GetSortTitle(response.Title), Year = response.Year, DateAdded = dateAdded, DateUpdated = lastWriteTime, @@ -851,7 +847,7 @@ public class PlexServerApiClient : IPlexServerApiClient { MetadataKind = MetadataKind.External, Title = response.Title, - SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title), + SortTitle = SortTitle.GetSortTitle(response.Title), EpisodeNumber = response.Index, Plot = response.Summary, Year = response.Year, diff --git a/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs similarity index 98% rename from ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs rename to ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs index b14e36fa7..7c7bb9325 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs @@ -3,6 +3,7 @@ using System.Security.Cryptography; using System.Text; using Bugsnag; using CliWrap; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.FFmpeg; @@ -20,6 +21,7 @@ using ErsatzTV.FFmpeg.Format; using ErsatzTV.FFmpeg.Pipeline; using ErsatzTV.FFmpeg.State; using ErsatzTV.Infrastructure.Runtime; +using ErsatzTV.Scanner.Core.Metadata; using FluentAssertions; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; @@ -28,7 +30,7 @@ using NUnit.Framework; using Serilog; using MediaStream = ErsatzTV.Core.Domain.MediaStream; -namespace ErsatzTV.Core.Tests.FFmpeg; +namespace ErsatzTV.Scanner.Tests.Core.FFmpeg; [TestFixture] [Explicit] @@ -316,13 +318,13 @@ public class TranscodingTests .Filter(s => s.MediaStreamKind == MediaStreamKind.Subtitle) .ToList(); - var subtitles = new List(); + var subtitles = new List(); if (subtitle != Subtitle.None) { foreach (MediaStream stream in subtitleStreams) { - var s = new Domain.Subtitle + var s = new ErsatzTV.Core.Domain.Subtitle { Codec = stream.Codec, Default = stream.Default, @@ -427,7 +429,7 @@ public class TranscodingTests { // validate pipeline matches expectations (at a high level) - NewComplexFilter complexFilter = pipeline.PipelineSteps.OfType().First(); + ComplexFilter complexFilter = pipeline.PipelineSteps.OfType().First(); FilterChain filterChain = complexFilter.FilterChain; if (profileBitDepth == FFmpegProfileBitDepth.TenBit) @@ -800,8 +802,8 @@ public class TranscodingTests string preferredAudioTitle) => Optional(version.MediaVersion.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Audio)).AsTask(); - public Task> SelectSubtitleStream( - List subtitles, + public Task> SelectSubtitleStream( + List subtitles, Channel channel, string preferredSubtitleLanguage, ChannelSubtitleMode subtitleMode) => diff --git a/ErsatzTV.Core.Tests/Metadata/LocalStatisticsProviderTests.cs b/ErsatzTV.Scanner.Tests/Core/Metadata/LocalStatisticsProviderTests.cs similarity index 64% rename from ErsatzTV.Core.Tests/Metadata/LocalStatisticsProviderTests.cs rename to ErsatzTV.Scanner.Tests/Core/Metadata/LocalStatisticsProviderTests.cs index 02493589c..f6e2debfc 100644 --- a/ErsatzTV.Core.Tests/Metadata/LocalStatisticsProviderTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/Metadata/LocalStatisticsProviderTests.cs @@ -2,13 +2,13 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Metadata; using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; -namespace ErsatzTV.Core.Tests.Metadata; +namespace ErsatzTV.Scanner.Tests.Core.Metadata; [TestFixture] public class LocalStatisticsProviderTests @@ -25,7 +25,19 @@ public class LocalStatisticsProviderTests new Mock>().Object); var input = new LocalStatisticsProvider.FFprobe( - new LocalStatisticsProvider.FFprobeFormat("123.45", null), + new LocalStatisticsProvider.FFprobeFormat( + "123.45", + new LocalStatisticsProvider.FFprobeTags( + string.Empty, + string.Empty, + string.Empty, + string.Empty, + string.Empty, + string.Empty, + string.Empty, + string.Empty, + string.Empty, + string.Empty)), new List(), new List()); diff --git a/ErsatzTV.Core.Tests/Metadata/LocalSubtitlesProviderTests.cs b/ErsatzTV.Scanner.Tests/Core/Metadata/LocalSubtitlesProviderTests.cs similarity index 97% rename from ErsatzTV.Core.Tests/Metadata/LocalSubtitlesProviderTests.cs rename to ErsatzTV.Scanner.Tests/Core/Metadata/LocalSubtitlesProviderTests.cs index e9ce4da6f..599712376 100644 --- a/ErsatzTV.Core.Tests/Metadata/LocalSubtitlesProviderTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/Metadata/LocalSubtitlesProviderTests.cs @@ -1,14 +1,14 @@ using System.Globalization; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.Metadata; -using ErsatzTV.Core.Tests.Fakes; +using ErsatzTV.Scanner.Core.Metadata; +using ErsatzTV.Scanner.Tests.Core.Fakes; using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; -namespace ErsatzTV.Core.Tests.Metadata; +namespace ErsatzTV.Scanner.Tests.Core.Metadata; [TestFixture] public class LocalSubtitlesProviderTests diff --git a/ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs b/ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs index 62b7e789b..794a87b01 100644 --- a/ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs @@ -8,6 +8,7 @@ using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; using ErsatzTV.Scanner.Core.Interfaces.FFmpeg; +using ErsatzTV.Scanner.Core.Interfaces.Metadata; using ErsatzTV.Scanner.Tests.Core.Fakes; using ErsatzTV.Scanner.Core.Metadata; using FluentAssertions; diff --git a/ErsatzTV.Core.Tests/Metadata/Nfo/ArtistNfoReaderTests.cs b/ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/ArtistNfoReaderTests.cs similarity index 99% rename from ErsatzTV.Core.Tests/Metadata/Nfo/ArtistNfoReaderTests.cs rename to ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/ArtistNfoReaderTests.cs index be326c9f7..f816025d7 100644 --- a/ErsatzTV.Core.Tests/Metadata/Nfo/ArtistNfoReaderTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/ArtistNfoReaderTests.cs @@ -1,6 +1,7 @@ using System.Text; using Bugsnag; -using ErsatzTV.Core.Metadata.Nfo; +using ErsatzTV.Core; +using ErsatzTV.Scanner.Core.Metadata.Nfo; using FluentAssertions; using Microsoft.Extensions.Logging; using Microsoft.IO; @@ -8,7 +9,7 @@ using Moq; using NUnit.Framework; using Serilog; -namespace ErsatzTV.Core.Tests.Metadata.Nfo; +namespace ErsatzTV.Scanner.Tests.Core.Metadata.Nfo; [TestFixture] public class ArtistNfoReaderTests diff --git a/ErsatzTV.Core.Tests/Metadata/Nfo/EpisodeNfoReaderTests.cs b/ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/EpisodeNfoReaderTests.cs similarity index 86% rename from ErsatzTV.Core.Tests/Metadata/Nfo/EpisodeNfoReaderTests.cs rename to ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/EpisodeNfoReaderTests.cs index 1782c9cd9..70b357816 100644 --- a/ErsatzTV.Core.Tests/Metadata/Nfo/EpisodeNfoReaderTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/EpisodeNfoReaderTests.cs @@ -1,6 +1,7 @@ using System.Text; using Bugsnag; -using ErsatzTV.Core.Metadata.Nfo; +using ErsatzTV.Core; +using ErsatzTV.Scanner.Core.Metadata.Nfo; using FluentAssertions; using Microsoft.Extensions.Logging; using Microsoft.IO; @@ -8,7 +9,7 @@ using Moq; using NUnit.Framework; using Serilog; -namespace ErsatzTV.Core.Tests.Metadata.Nfo; +namespace ErsatzTV.Scanner.Tests.Core.Metadata.Nfo; [TestFixture] public class EpisodeNfoReaderTests @@ -45,10 +46,10 @@ public class EpisodeNfoReaderTests ")); - Either> result = await _episodeNfoReader.Read(stream); + Either> result = await _episodeNfoReader.Read(stream); result.IsRight.Should().BeTrue(); - foreach (List list in result.RightToSeq()) + foreach (List list in result.RightToSeq()) { list.Count.Should().Be(1); } @@ -74,10 +75,10 @@ public class EpisodeNfoReaderTests 1 ")); - Either> result = await _episodeNfoReader.Read(stream); + Either> result = await _episodeNfoReader.Read(stream); result.IsRight.Should().BeTrue(); - foreach (List list in result.RightToSeq()) + foreach (List list in result.RightToSeq()) { list.Count.Should().Be(2); list.All(nfo => nfo.ShowTitle == "show").Should().BeTrue(); @@ -99,10 +100,10 @@ public class EpisodeNfoReaderTests tt54321 ")); - Either> result = await _episodeNfoReader.Read(stream); + Either> result = await _episodeNfoReader.Read(stream); result.IsRight.Should().BeTrue(); - foreach (List list in result.RightToSeq()) + foreach (List list in result.RightToSeq()) { list.Count.Should().Be(1); list[0].UniqueIds.Count.Should().Be(2); @@ -122,10 +123,10 @@ public class EpisodeNfoReaderTests ")); - Either> result = await _episodeNfoReader.Read(stream); + Either> result = await _episodeNfoReader.Read(stream); result.IsRight.Should().BeTrue(); - foreach (List list in result.RightToSeq()) + foreach (List list in result.RightToSeq()) { list.Count.Should().Be(1); list[0].ContentRating.Should().BeNullOrEmpty(); @@ -146,10 +147,10 @@ public class EpisodeNfoReaderTests US:Something / US:SomethingElse ")); - Either> result = await _episodeNfoReader.Read(stream); + Either> result = await _episodeNfoReader.Read(stream); result.IsRight.Should().BeTrue(); - foreach (List list in result.RightToSeq()) + foreach (List list in result.RightToSeq()) { list.Count.Should().Be(2); list.Count(nfo => nfo.ContentRating == "US:Something").Should().Be(1); @@ -168,10 +169,10 @@ public class EpisodeNfoReaderTests ")); - Either> result = await _episodeNfoReader.Read(stream); + Either> result = await _episodeNfoReader.Read(stream); result.IsRight.Should().BeTrue(); - foreach (List list in result.RightToSeq()) + foreach (List list in result.RightToSeq()) { list.Count.Should().Be(1); list[0].Plot.Should().BeNullOrEmpty(); @@ -189,10 +190,10 @@ public class EpisodeNfoReaderTests Some Plot ")); - Either> result = await _episodeNfoReader.Read(stream); + Either> result = await _episodeNfoReader.Read(stream); result.IsRight.Should().BeTrue(); - foreach (List list in result.RightToSeq()) + foreach (List list in result.RightToSeq()) { list.Count.Should().Be(1); list[0].Plot.Should().Be("Some Plot"); @@ -219,10 +220,10 @@ public class EpisodeNfoReaderTests ")); - Either> result = await _episodeNfoReader.Read(stream); + Either> result = await _episodeNfoReader.Read(stream); result.IsRight.Should().BeTrue(); - foreach (List list in result.RightToSeq()) + foreach (List list in result.RightToSeq()) { list.Count.Should().Be(1); list[0].Actors.Count.Should().Be(2); @@ -248,10 +249,10 @@ public class EpisodeNfoReaderTests Writer 3 ")); - Either> result = await _episodeNfoReader.Read(stream); + Either> result = await _episodeNfoReader.Read(stream); result.IsRight.Should().BeTrue(); - foreach (List list in result.RightToSeq()) + foreach (List list in result.RightToSeq()) { list.Count.Should().Be(2); list.Count(nfo => nfo.Writers.Count == 1 && nfo.Writers[0] == "Writer 1").Should().Be(1); @@ -275,10 +276,10 @@ public class EpisodeNfoReaderTests Director 3 ")); - Either> result = await _episodeNfoReader.Read(stream); + Either> result = await _episodeNfoReader.Read(stream); result.IsRight.Should().BeTrue(); - foreach (List list in result.RightToSeq()) + foreach (List list in result.RightToSeq()) { list.Count.Should().Be(2); list.Count(nfo => nfo.Directors.Count == 1 && nfo.Directors[0] == "Director 1").Should().Be(1); @@ -379,11 +380,11 @@ public class EpisodeNfoReaderTests 2021-02-02 11:57:44 ")); - Either> result = await _episodeNfoReader.Read(stream); + Either> result = await _episodeNfoReader.Read(stream); result.IsRight.Should().BeTrue(); - foreach (TvShowEpisodeNfo nfo in result.RightToSeq().Flatten()) + foreach (EpisodeNfo nfo in result.RightToSeq().Flatten()) { nfo.ShowTitle.Should().Be("WandaVision"); nfo.Title.Should().Be("Filmed Before a Live Studio Audience"); @@ -433,10 +434,10 @@ public class EpisodeNfoReaderTests "Resources", "Nfo", "EpisodeInvalidCharacters.nfo"); - Either> result = await _episodeNfoReader.ReadFromFile(sourceFile); + Either> result = await _episodeNfoReader.ReadFromFile(sourceFile); result.IsRight.Should().BeTrue(); - foreach (List list in result.RightToSeq()) + foreach (List list in result.RightToSeq()) { list.Count.Should().Be(1); list[0].Title.Should().Be("Test Title"); diff --git a/ErsatzTV.Core.Tests/Metadata/Nfo/MovieNfoReaderTests.cs b/ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/MovieNfoReaderTests.cs similarity index 98% rename from ErsatzTV.Core.Tests/Metadata/Nfo/MovieNfoReaderTests.cs rename to ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/MovieNfoReaderTests.cs index 4619ba560..5e4c2edc1 100644 --- a/ErsatzTV.Core.Tests/Metadata/Nfo/MovieNfoReaderTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/MovieNfoReaderTests.cs @@ -1,13 +1,14 @@ using System.Text; using Bugsnag; -using ErsatzTV.Core.Metadata.Nfo; +using ErsatzTV.Core; +using ErsatzTV.Scanner.Core.Metadata.Nfo; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.IO; using Moq; using NUnit.Framework; -namespace ErsatzTV.Core.Tests.Metadata.Nfo; +namespace ErsatzTV.Scanner.Tests.Core.Metadata.Nfo; [TestFixture] public class MovieNfoReaderTests @@ -187,7 +188,7 @@ https://www.themoviedb.org/movie/11-star-wars")); nfo.Plot.Should().Be( "Determined to ensure Superman's ultimate sacrifice was not in vain, Bruce Wayne aligns forces with Diana Prince with plans to recruit a team of metahumans to protect the world from an approaching threat of catastrophic proportions."); - nfo.Tagline.Should().BeNullOrEmpty(); + // nfo.Tagline.Should().BeNullOrEmpty(); nfo.Genres.Should().BeEquivalentTo(new List { "SuperHero" }); nfo.Tags.Should().BeEquivalentTo(new List { "TV Recording" }); nfo.Studios.Should().BeEquivalentTo(new List { "Warner Bros. Pictures" }); diff --git a/ErsatzTV.Core.Tests/Metadata/Nfo/MusicVideoNfoReaderTests.cs b/ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/MusicVideoNfoReaderTests.cs similarity index 98% rename from ErsatzTV.Core.Tests/Metadata/Nfo/MusicVideoNfoReaderTests.cs rename to ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/MusicVideoNfoReaderTests.cs index ea05e3718..f769a2ec2 100644 --- a/ErsatzTV.Core.Tests/Metadata/Nfo/MusicVideoNfoReaderTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/MusicVideoNfoReaderTests.cs @@ -1,13 +1,14 @@ using System.Text; using Bugsnag; -using ErsatzTV.Core.Metadata.Nfo; +using ErsatzTV.Core; +using ErsatzTV.Scanner.Core.Metadata.Nfo; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.IO; using Moq; using NUnit.Framework; -namespace ErsatzTV.Core.Tests.Metadata.Nfo; +namespace ErsatzTV.Scanner.Tests.Core.Metadata.Nfo; [TestFixture] public class MusicVideoNfoReaderTests diff --git a/ErsatzTV.Core.Tests/Metadata/Nfo/OtherVideoNfoReaderTests.cs b/ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/OtherVideoNfoReaderTests.cs similarity index 98% rename from ErsatzTV.Core.Tests/Metadata/Nfo/OtherVideoNfoReaderTests.cs rename to ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/OtherVideoNfoReaderTests.cs index 68ebbe751..9a7f277c5 100644 --- a/ErsatzTV.Core.Tests/Metadata/Nfo/OtherVideoNfoReaderTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/OtherVideoNfoReaderTests.cs @@ -1,13 +1,14 @@ using System.Text; using Bugsnag; -using ErsatzTV.Core.Metadata.Nfo; +using ErsatzTV.Core; +using ErsatzTV.Scanner.Core.Metadata.Nfo; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.IO; using Moq; using NUnit.Framework; -namespace ErsatzTV.Core.Tests.Metadata.Nfo; +namespace ErsatzTV.Scanner.Tests.Core.Metadata.Nfo; [TestFixture] public class OtherVideoNfoReaderTests diff --git a/ErsatzTV.Core.Tests/Metadata/Nfo/TvShowNfoReaderTests.cs b/ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/ShowNfoReaderTests.cs similarity index 88% rename from ErsatzTV.Core.Tests/Metadata/Nfo/TvShowNfoReaderTests.cs rename to ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/ShowNfoReaderTests.cs index f692c817d..a44c4a2f1 100644 --- a/ErsatzTV.Core.Tests/Metadata/Nfo/TvShowNfoReaderTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/ShowNfoReaderTests.cs @@ -1,24 +1,25 @@ using System.Text; using Bugsnag; -using ErsatzTV.Core.Metadata.Nfo; +using ErsatzTV.Core; +using ErsatzTV.Scanner.Core.Metadata.Nfo; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.IO; using Moq; using NUnit.Framework; -namespace ErsatzTV.Core.Tests.Metadata.Nfo; +namespace ErsatzTV.Scanner.Tests.Core.Metadata.Nfo; [TestFixture] -public class TvShowNfoReaderTests +public class ShowNfoReaderTests { [SetUp] - public void SetUp() => _tvShowNfoReader = new TvShowNfoReader( + public void SetUp() => _showNfoReader = new ShowNfoReader( new RecyclableMemoryStreamManager(), new Mock().Object, - new NullLogger()); + new NullLogger()); - private TvShowNfoReader _tvShowNfoReader; + private ShowNfoReader _showNfoReader; [Test] public async Task ParsingNfo_Should_Return_Error() @@ -26,7 +27,7 @@ public class TvShowNfoReaderTests await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"https://www.themoviedb.org/movie/11-star-wars")); - Either result = await _tvShowNfoReader.Read(stream); + Either result = await _showNfoReader.Read(stream); result.IsLeft.Should().BeTrue(); } @@ -36,7 +37,7 @@ public class TvShowNfoReaderTests { await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"")); - Either result = await _tvShowNfoReader.Read(stream); + Either result = await _showNfoReader.Read(stream); result.IsRight.Should().BeTrue(); } @@ -49,7 +50,7 @@ public class TvShowNfoReaderTests @" https://www.themoviedb.org/movie/11-star-wars")); - Either result = await _tvShowNfoReader.Read(stream); + Either result = await _showNfoReader.Read(stream); result.IsRight.Should().BeTrue(); } @@ -133,11 +134,11 @@ https://www.themoviedb.org/movie/11-star-wars")); 2021-03-12 06:15:51 ")); - Either result = await _tvShowNfoReader.Read(stream); + Either result = await _showNfoReader.Read(stream); result.IsRight.Should().BeTrue(); - foreach (TvShowNfo nfo in result.RightToSeq()) + foreach (ShowNfo nfo in result.RightToSeq()) { nfo.Title.Should().Be("WandaVision"); nfo.Year.Should().Be(2021); @@ -181,10 +182,10 @@ https://www.themoviedb.org/movie/11-star-wars")); await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"Test Outline")); - Either result = await _tvShowNfoReader.Read(stream); + Either result = await _showNfoReader.Read(stream); result.IsRight.Should().BeTrue(); - foreach (TvShowNfo nfo in result.RightToSeq()) + foreach (ShowNfo nfo in result.RightToSeq()) { nfo.Outline.Should().Be("Test Outline"); } @@ -196,10 +197,10 @@ https://www.themoviedb.org/movie/11-star-wars")); await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"Test Tagline")); - Either result = await _tvShowNfoReader.Read(stream); + Either result = await _showNfoReader.Read(stream); result.IsRight.Should().BeTrue(); - foreach (TvShowNfo nfo in result.RightToSeq()) + foreach (ShowNfo nfo in result.RightToSeq()) { nfo.Tagline.Should().Be("Test Tagline"); } @@ -210,10 +211,10 @@ https://www.themoviedb.org/movie/11-star-wars")); { await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"Test Tag")); - Either result = await _tvShowNfoReader.Read(stream); + Either result = await _showNfoReader.Read(stream); result.IsRight.Should().BeTrue(); - foreach (TvShowNfo nfo in result.RightToSeq()) + foreach (ShowNfo nfo in result.RightToSeq()) { nfo.Tags.Should().BeEquivalentTo(new List { "Test Tag" }); } diff --git a/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj b/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj index 11cf38fc5..8b26c8372 100644 --- a/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj +++ b/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj @@ -23,4 +23,16 @@ + + + Always + + + Always + + + Always + + + diff --git a/ErsatzTV.Core.Tests/Resources/Nfo/ArtistInvalidCharacters1.nfo b/ErsatzTV.Scanner.Tests/Resources/Nfo/ArtistInvalidCharacters1.nfo similarity index 100% rename from ErsatzTV.Core.Tests/Resources/Nfo/ArtistInvalidCharacters1.nfo rename to ErsatzTV.Scanner.Tests/Resources/Nfo/ArtistInvalidCharacters1.nfo diff --git a/ErsatzTV.Core.Tests/Resources/Nfo/ArtistInvalidCharacters2.nfo b/ErsatzTV.Scanner.Tests/Resources/Nfo/ArtistInvalidCharacters2.nfo similarity index 100% rename from ErsatzTV.Core.Tests/Resources/Nfo/ArtistInvalidCharacters2.nfo rename to ErsatzTV.Scanner.Tests/Resources/Nfo/ArtistInvalidCharacters2.nfo diff --git a/ErsatzTV.Core.Tests/Resources/Nfo/EpisodeInvalidCharacters.nfo b/ErsatzTV.Scanner.Tests/Resources/Nfo/EpisodeInvalidCharacters.nfo similarity index 100% rename from ErsatzTV.Core.Tests/Resources/Nfo/EpisodeInvalidCharacters.nfo rename to ErsatzTV.Scanner.Tests/Resources/Nfo/EpisodeInvalidCharacters.nfo diff --git a/ErsatzTV.Scanner/Application/MediaSources/Commands/ScanLocalLibraryHandler.cs b/ErsatzTV.Scanner/Application/MediaSources/Commands/ScanLocalLibraryHandler.cs index 88f1f66fa..26ba26125 100644 --- a/ErsatzTV.Scanner/Application/MediaSources/Commands/ScanLocalLibraryHandler.cs +++ b/ErsatzTV.Scanner/Application/MediaSources/Commands/ScanLocalLibraryHandler.cs @@ -1,9 +1,9 @@ using System.Diagnostics; using ErsatzTV.Core; using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.MediaSources; +using ErsatzTV.Scanner.Core.Interfaces.Metadata; using Humanizer; using Microsoft.Extensions.Logging; diff --git a/ErsatzTV.Scanner/Core/Emby/EmbyCollectionScanner.cs b/ErsatzTV.Scanner/Core/Emby/EmbyCollectionScanner.cs index f0acb3f8a..9f2ac8db9 100644 --- a/ErsatzTV.Scanner/Core/Emby/EmbyCollectionScanner.cs +++ b/ErsatzTV.Scanner/Core/Emby/EmbyCollectionScanner.cs @@ -55,10 +55,6 @@ public class EmbyCollectionScanner : IEmbyCollectionScanner _logger.LogDebug("Emby collection {Name} is new", collection.Name); await _embyCollectionRepository.AddCollection(collection); } - else - { - _logger.LogDebug("Emby collection {Name} has been updated", collection.Name); - } await SyncCollectionItems(address, apiKey, collection); diff --git a/ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs b/ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs index fd8388354..e9dd28017 100644 --- a/ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs @@ -6,6 +6,8 @@ using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Interfaces.Metadata; +using ErsatzTV.Scanner.Core.Metadata; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Core.Emby; diff --git a/ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs b/ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs index ad1601931..6df24f42a 100644 --- a/ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs @@ -6,6 +6,8 @@ using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Interfaces.Metadata; +using ErsatzTV.Scanner.Core.Metadata; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Core.Emby; diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs b/ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalMetadataProvider.cs similarity index 95% rename from ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs rename to ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalMetadataProvider.cs index 2193e2a48..0bc40349f 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs +++ b/ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalMetadataProvider.cs @@ -1,6 +1,6 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Interfaces.Metadata; +namespace ErsatzTV.Scanner.Core.Interfaces.Metadata; public interface ILocalMetadataProvider { diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalStatisticsProvider.cs b/ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalStatisticsProvider.cs similarity index 80% rename from ErsatzTV.Core/Interfaces/Metadata/ILocalStatisticsProvider.cs rename to ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalStatisticsProvider.cs index e101d3fd6..83d5aa49a 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ILocalStatisticsProvider.cs +++ b/ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalStatisticsProvider.cs @@ -1,6 +1,7 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Interfaces.Metadata; +namespace ErsatzTV.Scanner.Core.Interfaces.Metadata; public interface ILocalStatisticsProvider { diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalSubtitlesProvider.cs b/ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalSubtitlesProvider.cs similarity index 76% rename from ErsatzTV.Core/Interfaces/Metadata/ILocalSubtitlesProvider.cs rename to ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalSubtitlesProvider.cs index cd6fc7a27..8c624d341 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ILocalSubtitlesProvider.cs +++ b/ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalSubtitlesProvider.cs @@ -1,6 +1,6 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Interfaces.Metadata; +namespace ErsatzTV.Scanner.Core.Interfaces.Metadata; public interface ILocalSubtitlesProvider { diff --git a/ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs b/ErsatzTV.Scanner/Core/Interfaces/Metadata/IMovieFolderScanner.cs similarity index 72% rename from ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs rename to ErsatzTV.Scanner/Core/Interfaces/Metadata/IMovieFolderScanner.cs index 43f4b915a..374626002 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Interfaces/Metadata/IMovieFolderScanner.cs @@ -1,6 +1,7 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Interfaces.Metadata; +namespace ErsatzTV.Scanner.Core.Interfaces.Metadata; public interface IMovieFolderScanner { diff --git a/ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs b/ErsatzTV.Scanner/Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs similarity index 73% rename from ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs rename to ErsatzTV.Scanner/Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs index ac33408b9..43f91f7b6 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs @@ -1,6 +1,7 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Interfaces.Metadata; +namespace ErsatzTV.Scanner.Core.Interfaces.Metadata; public interface IMusicVideoFolderScanner { diff --git a/ErsatzTV.Core/Interfaces/Metadata/IOtherVideoFolderScanner.cs b/ErsatzTV.Scanner/Core/Interfaces/Metadata/IOtherVideoFolderScanner.cs similarity index 73% rename from ErsatzTV.Core/Interfaces/Metadata/IOtherVideoFolderScanner.cs rename to ErsatzTV.Scanner/Core/Interfaces/Metadata/IOtherVideoFolderScanner.cs index c0910a98e..7e1fe610b 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/IOtherVideoFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Interfaces/Metadata/IOtherVideoFolderScanner.cs @@ -1,6 +1,7 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Interfaces.Metadata; +namespace ErsatzTV.Scanner.Core.Interfaces.Metadata; public interface IOtherVideoFolderScanner { diff --git a/ErsatzTV.Core/Interfaces/Metadata/ISongFolderScanner.cs b/ErsatzTV.Scanner/Core/Interfaces/Metadata/ISongFolderScanner.cs similarity index 72% rename from ErsatzTV.Core/Interfaces/Metadata/ISongFolderScanner.cs rename to ErsatzTV.Scanner/Core/Interfaces/Metadata/ISongFolderScanner.cs index 2687d34ed..aecdbed12 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ISongFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Interfaces/Metadata/ISongFolderScanner.cs @@ -1,6 +1,7 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Interfaces.Metadata; +namespace ErsatzTV.Scanner.Core.Interfaces.Metadata; public interface ISongFolderScanner { diff --git a/ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs b/ErsatzTV.Scanner/Core/Interfaces/Metadata/ITelevisionFolderScanner.cs similarity index 73% rename from ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs rename to ErsatzTV.Scanner/Core/Interfaces/Metadata/ITelevisionFolderScanner.cs index efd08fb94..7e489258f 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Interfaces/Metadata/ITelevisionFolderScanner.cs @@ -1,6 +1,7 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Interfaces.Metadata; +namespace ErsatzTV.Scanner.Core.Interfaces.Metadata; public interface ITelevisionFolderScanner { diff --git a/ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IArtistNfoReader.cs b/ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IArtistNfoReader.cs new file mode 100644 index 000000000..02dd6efdd --- /dev/null +++ b/ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IArtistNfoReader.cs @@ -0,0 +1,9 @@ +using ErsatzTV.Core; +using ErsatzTV.Scanner.Core.Metadata.Nfo; + +namespace ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; + +public interface IArtistNfoReader +{ + Task> ReadFromFile(string fileName); +} diff --git a/ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IEpisodeNfoReader.cs b/ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IEpisodeNfoReader.cs new file mode 100644 index 000000000..dd11ce1f2 --- /dev/null +++ b/ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IEpisodeNfoReader.cs @@ -0,0 +1,9 @@ +using ErsatzTV.Core; +using ErsatzTV.Scanner.Core.Metadata.Nfo; + +namespace ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; + +public interface IEpisodeNfoReader +{ + Task>> ReadFromFile(string fileName); +} diff --git a/ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IMovieNfoReader.cs b/ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IMovieNfoReader.cs new file mode 100644 index 000000000..939206041 --- /dev/null +++ b/ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IMovieNfoReader.cs @@ -0,0 +1,9 @@ +using ErsatzTV.Core; +using ErsatzTV.Scanner.Core.Metadata.Nfo; + +namespace ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; + +public interface IMovieNfoReader +{ + Task> ReadFromFile(string fileName); +} diff --git a/ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IMusicVideoNfoReader.cs b/ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IMusicVideoNfoReader.cs new file mode 100644 index 000000000..9dfa44bfb --- /dev/null +++ b/ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IMusicVideoNfoReader.cs @@ -0,0 +1,9 @@ +using ErsatzTV.Core; +using ErsatzTV.Scanner.Core.Metadata.Nfo; + +namespace ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; + +public interface IMusicVideoNfoReader +{ + Task> ReadFromFile(string fileName); +} diff --git a/ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IOtherVideoNfoReader.cs b/ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IOtherVideoNfoReader.cs new file mode 100644 index 000000000..7d3707862 --- /dev/null +++ b/ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IOtherVideoNfoReader.cs @@ -0,0 +1,9 @@ +using ErsatzTV.Core; +using ErsatzTV.Scanner.Core.Metadata.Nfo; + +namespace ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; + +public interface IOtherVideoNfoReader +{ + Task> ReadFromFile(string fileName); +} diff --git a/ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IShowNfoReader.cs b/ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IShowNfoReader.cs new file mode 100644 index 000000000..de56b3c3f --- /dev/null +++ b/ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IShowNfoReader.cs @@ -0,0 +1,9 @@ +using ErsatzTV.Core; +using ErsatzTV.Scanner.Core.Metadata.Nfo; + +namespace ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; + +public interface IShowNfoReader +{ + Task> ReadFromFile(string fileName); +} diff --git a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs index c9b736687..f922045ae 100644 --- a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs +++ b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs @@ -58,10 +58,6 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner _logger.LogDebug("Jellyfin collection {Name} is new", collection.Name); await _jellyfinCollectionRepository.AddCollection(collection); } - else - { - _logger.LogDebug("Updating Jellyfin collection {Name}", collection.Name); - } await SyncCollectionItems(address, apiKey, mediaSourceId, collection); diff --git a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs index b98eb7598..812f3c739 100644 --- a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs @@ -6,6 +6,8 @@ using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Jellyfin; using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Interfaces.Metadata; +using ErsatzTV.Scanner.Core.Metadata; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Core.Jellyfin; diff --git a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs index fe6c809c8..014667883 100644 --- a/ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs @@ -6,6 +6,8 @@ using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Jellyfin; using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Interfaces.Metadata; +using ErsatzTV.Scanner.Core.Metadata; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Core.Jellyfin; diff --git a/ErsatzTV.Core/Metadata/FolderEtag.cs b/ErsatzTV.Scanner/Core/Metadata/FolderEtag.cs similarity index 97% rename from ErsatzTV.Core/Metadata/FolderEtag.cs rename to ErsatzTV.Scanner/Core/Metadata/FolderEtag.cs index c58fee81c..d468d7837 100644 --- a/ErsatzTV.Core/Metadata/FolderEtag.cs +++ b/ErsatzTV.Scanner/Core/Metadata/FolderEtag.cs @@ -2,7 +2,7 @@ using System.Text; using ErsatzTV.Core.Interfaces.Metadata; -namespace ErsatzTV.Core.Metadata; +namespace ErsatzTV.Scanner.Core.Metadata; public static class FolderEtag { diff --git a/ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs index 5490a51b7..26404e792 100644 --- a/ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs @@ -11,6 +11,7 @@ using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; using ErsatzTV.Scanner.Core.Interfaces.FFmpeg; +using ErsatzTV.Scanner.Core.Interfaces.Metadata; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Core.Metadata; diff --git a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs b/ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs similarity index 95% rename from ErsatzTV.Core/Metadata/LocalMetadataProvider.cs rename to ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs index 73b1e3183..ca32d8656 100644 --- a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs +++ b/ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs @@ -1,13 +1,16 @@ using Bugsnag; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Metadata; -using ErsatzTV.Core.Interfaces.Metadata.Nfo; using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.Metadata.Nfo; +using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Interfaces.Metadata; +using ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; +using ErsatzTV.Scanner.Core.Metadata.Nfo; using Microsoft.Extensions.Logging; -namespace ErsatzTV.Core.Metadata; +namespace ErsatzTV.Scanner.Core.Metadata; public class LocalMetadataProvider : ILocalMetadataProvider { @@ -28,7 +31,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider private readonly IOtherVideoRepository _otherVideoRepository; private readonly ISongRepository _songRepository; private readonly ITelevisionRepository _televisionRepository; - private readonly ITvShowNfoReader _tvShowNfoReader; + private readonly IShowNfoReader _showNfoReader; public LocalMetadataProvider( IMetadataRepository metadataRepository, @@ -44,7 +47,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider IEpisodeNfoReader episodeNfoReader, IArtistNfoReader artistNfoReader, IMusicVideoNfoReader musicVideoNfoReader, - ITvShowNfoReader tvShowNfoReader, + IShowNfoReader showNfoReader, IOtherVideoNfoReader otherVideoNfoReader, ILocalStatisticsProvider localStatisticsProvider, IClient client, @@ -63,7 +66,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider _episodeNfoReader = episodeNfoReader; _artistNfoReader = artistNfoReader; _musicVideoNfoReader = musicVideoNfoReader; - _tvShowNfoReader = tvShowNfoReader; + _showNfoReader = showNfoReader; _otherVideoNfoReader = otherVideoNfoReader; _localStatisticsProvider = localStatisticsProvider; _client = client; @@ -81,12 +84,12 @@ public class LocalMetadataProvider : ILocalMetadataProvider foreach (ShowMetadata metadata in maybeMetadata) { - metadata.SortTitle = _fallbackMetadataProvider.GetSortTitle(metadata.Title); + metadata.SortTitle = SortTitle.GetSortTitle(metadata.Title); return metadata; } ShowMetadata fallbackMetadata = _fallbackMetadataProvider.GetFallbackMetadataForShow(showFolder); - fallbackMetadata.SortTitle = _fallbackMetadataProvider.GetSortTitle(fallbackMetadata.Title); + fallbackMetadata.SortTitle = SortTitle.GetSortTitle(fallbackMetadata.Title); return fallbackMetadata; } @@ -101,12 +104,12 @@ public class LocalMetadataProvider : ILocalMetadataProvider foreach (ArtistMetadata metadata in maybeMetadata) { - metadata.SortTitle = _fallbackMetadataProvider.GetSortTitle(metadata.Title); + metadata.SortTitle = SortTitle.GetSortTitle(metadata.Title); return metadata; } ArtistMetadata fallbackMetadata = _fallbackMetadataProvider.GetFallbackMetadataForArtist(artistFolder); - fallbackMetadata.SortTitle = _fallbackMetadataProvider.GetSortTitle(fallbackMetadata.Title); + fallbackMetadata.SortTitle = SortTitle.GetSortTitle(fallbackMetadata.Title); return fallbackMetadata; } @@ -297,37 +300,37 @@ public class LocalMetadataProvider : ILocalMetadataProvider Tags = new List() }; - if (tags.TryGetValue(MetadataSongTag.Album, out string album)) + if (tags.TryGetValue(MetadataSongTag.Album, out string? album)) { result.Album = album; } - if (tags.TryGetValue(MetadataSongTag.Artist, out string artist)) + if (tags.TryGetValue(MetadataSongTag.Artist, out string? artist)) { result.Artist = artist; } - if (tags.TryGetValue(MetadataSongTag.AlbumArtist, out string albumArtist)) + if (tags.TryGetValue(MetadataSongTag.AlbumArtist, out string? albumArtist)) { result.AlbumArtist = albumArtist; } - if (tags.TryGetValue(MetadataSongTag.Date, out string date)) + if (tags.TryGetValue(MetadataSongTag.Date, out string? date)) { result.Date = date; } - if (tags.TryGetValue(MetadataSongTag.Genre, out string genre)) + if (tags.TryGetValue(MetadataSongTag.Genre, out string? genre)) { result.Genres.AddRange(SplitGenres(genre).Map(n => new Genre { Name = n })); } - if (tags.TryGetValue(MetadataSongTag.Title, out string title)) + if (tags.TryGetValue(MetadataSongTag.Title, out string? title)) { result.Title = title; } - if (tags.TryGetValue(MetadataSongTag.Track, out string track)) + if (tags.TryGetValue(MetadataSongTag.Track, out string? track)) { result.Track = track; } @@ -384,7 +387,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider foreach (EpisodeMetadata metadata in toAdd) { metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) - ? _fallbackMetadataProvider.GetSortTitle(metadata.Title) + ? SortTitle.GetSortTitle(metadata.Title) : metadata.SortTitle; metadata.EpisodeId = episode.Id; metadata.Episode = episode; @@ -415,7 +418,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider existing.ReleaseDate = metadata.ReleaseDate; existing.Year = metadata.Year; existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) - ? _fallbackMetadataProvider.GetSortTitle(metadata.Title) + ? SortTitle.GetSortTitle(metadata.Title) : metadata.SortTitle; updated = await UpdateMetadataCollections( @@ -515,7 +518,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider existing.ReleaseDate = metadata.ReleaseDate; existing.Year = metadata.Year; existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) - ? _fallbackMetadataProvider.GetSortTitle(metadata.Title) + ? SortTitle.GetSortTitle(metadata.Title) : metadata.SortTitle; bool updated = await UpdateMetadataCollections( @@ -590,7 +593,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider } metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) - ? _fallbackMetadataProvider.GetSortTitle(metadata.Title) + ? SortTitle.GetSortTitle(metadata.Title) : metadata.SortTitle; metadata.MovieId = movie.Id; movie.MovieMetadata = new List { metadata }; @@ -620,7 +623,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider existing.ReleaseDate = metadata.ReleaseDate; existing.Year = metadata.Year; existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) - ? _fallbackMetadataProvider.GetSortTitle(metadata.Title) + ? SortTitle.GetSortTitle(metadata.Title) : metadata.SortTitle; bool updated = await UpdateMetadataCollections( @@ -655,7 +658,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider } metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) - ? _fallbackMetadataProvider.GetSortTitle(metadata.Title) + ? SortTitle.GetSortTitle(metadata.Title) : metadata.SortTitle; metadata.ShowId = show.Id; show.ShowMetadata = new List { metadata }; @@ -680,7 +683,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider existing.DateUpdated = metadata.DateUpdated; existing.MetadataKind = metadata.MetadataKind; existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) - ? _fallbackMetadataProvider.GetSortTitle(metadata.Title) + ? SortTitle.GetSortTitle(metadata.Title) : metadata.SortTitle; var updated = false; @@ -749,7 +752,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider } metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) - ? _fallbackMetadataProvider.GetSortTitle(metadata.Title) + ? SortTitle.GetSortTitle(metadata.Title) : metadata.SortTitle; metadata.ArtistId = artist.Id; artist.ArtistMetadata = new List { metadata }; @@ -778,7 +781,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider existing.OriginalTitle = metadata.OriginalTitle; existing.ReleaseDate = metadata.ReleaseDate; existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) - ? _fallbackMetadataProvider.GetSortTitle(metadata.Title) + ? SortTitle.GetSortTitle(metadata.Title) : metadata.SortTitle; bool updated = await UpdateMetadataCollections( @@ -815,7 +818,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider } metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) - ? _fallbackMetadataProvider.GetSortTitle(metadata.Title) + ? SortTitle.GetSortTitle(metadata.Title) : metadata.SortTitle; metadata.MusicVideoId = musicVideo.Id; musicVideo.MusicVideoMetadata = new List { metadata }; @@ -845,7 +848,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider existing.ReleaseDate = metadata.ReleaseDate; existing.Year = metadata.Year; existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) - ? _fallbackMetadataProvider.GetSortTitle(metadata.Title) + ? SortTitle.GetSortTitle(metadata.Title) : metadata.SortTitle; existing.OriginalTitle = metadata.OriginalTitle; @@ -921,7 +924,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider } metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) - ? _fallbackMetadataProvider.GetSortTitle(metadata.Title) + ? SortTitle.GetSortTitle(metadata.Title) : metadata.SortTitle; metadata.OtherVideoId = otherVideo.Id; otherVideo.OtherVideoMetadata = new List { metadata }; @@ -949,7 +952,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider existing.DateUpdated = metadata.DateUpdated; existing.MetadataKind = metadata.MetadataKind; existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) - ? _fallbackMetadataProvider.GetSortTitle(metadata.Title) + ? SortTitle.GetSortTitle(metadata.Title) : metadata.SortTitle; existing.OriginalTitle = metadata.OriginalTitle; @@ -965,7 +968,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider } metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) - ? _fallbackMetadataProvider.GetSortTitle(metadata.Title) + ? SortTitle.GetSortTitle(metadata.Title) : metadata.SortTitle; metadata.SongId = song.Id; song.SongMetadata = new List { metadata }; @@ -977,7 +980,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider { try { - Either maybeNfo = await _tvShowNfoReader.ReadFromFile(nfoFileName); + Either maybeNfo = await _showNfoReader.ReadFromFile(nfoFileName); foreach (BaseError error in maybeNfo.LeftToSeq()) { _logger.LogInformation( @@ -986,7 +989,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider error.ToString()); } - foreach (TvShowNfo nfo in maybeNfo.RightToSeq()) + foreach (ShowNfo nfo in maybeNfo.RightToSeq()) { DateTime dateAdded = DateTime.UtcNow; DateTime dateUpdated = File.GetLastWriteTimeUtc(nfoFileName); @@ -1066,7 +1069,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider { try { - Either> maybeNfo = await _episodeNfoReader.ReadFromFile(nfoFileName); + Either> maybeNfo = await _episodeNfoReader.ReadFromFile(nfoFileName); foreach (BaseError error in maybeNfo.LeftToSeq()) { _logger.LogInformation( @@ -1076,7 +1079,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider } var result = new List(); - foreach (TvShowEpisodeNfo nfo in maybeNfo.RightToSeq().Flatten()) + foreach (EpisodeNfo nfo in maybeNfo.RightToSeq().Flatten()) { DateTime dateAdded = DateTime.UtcNow; DateTime dateUpdated = File.GetLastWriteTimeUtc(nfoFileName); @@ -1087,7 +1090,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider DateAdded = dateAdded, DateUpdated = dateUpdated, Title = nfo.Title, - SortTitle = _fallbackMetadataProvider.GetSortTitle(nfo.Title), + SortTitle = SortTitle.GetSortTitle(nfo.Title), EpisodeNumber = nfo.Episode, Year = GetYear(0, nfo.Aired), ReleaseDate = GetAired(0, nfo.Aired), @@ -1169,7 +1172,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider ReleaseDate = releaseDate, Plot = nfo.Plot, Outline = nfo.Outline, - Tagline = nfo.Tagline, + // Tagline = nfo.Tagline, Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(), Tags = nfo.Tags.Map(t => new Tag { Name = t }).ToList(), Studios = nfo.Studios.Map(s => new Studio { Name = s }).ToList(), @@ -1300,7 +1303,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider Func> addTag, Func> addStudio, Func> addActor) - where T : Domain.Metadata + where T : ErsatzTV.Core.Domain.Metadata { var updated = false; diff --git a/ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs b/ErsatzTV.Scanner/Core/Metadata/LocalStatisticsProvider.cs similarity index 90% rename from ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs rename to ErsatzTV.Scanner/Core/Metadata/LocalStatisticsProvider.cs index a84a1b535..75c3e709f 100644 --- a/ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs +++ b/ErsatzTV.Scanner/Core/Metadata/LocalStatisticsProvider.cs @@ -5,14 +5,16 @@ using System.Text.RegularExpressions; using Bugsnag; using CliWrap; using CliWrap.Buffered; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Scanner.Core.Interfaces.Metadata; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -namespace ErsatzTV.Core.Metadata; +namespace ErsatzTV.Scanner.Core.Metadata; public class LocalStatisticsProvider : ILocalStatisticsProvider { @@ -237,9 +239,8 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider return BaseError.New($"FFprobe at {ffprobePath} exited with code {probe.ExitCode}"); } - FFprobe ffprobe = JsonConvert.DeserializeObject(probe.StandardOutput); - - if (ffprobe != null) + FFprobe? ffprobe = JsonConvert.DeserializeObject(probe.StandardOutput); + if (ffprobe is not null) { const string PATTERN = @"\[SAR\s+([0-9]+:[0-9]+)\s+DAR\s+([0-9]+:[0-9]+)\]"; Match match = Regex.Match(probe.StandardError, PATTERN); @@ -247,16 +248,18 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider { string sar = match.Groups[1].Value; string dar = match.Groups[2].Value; - foreach (FFprobeStream stream in ffprobe.streams.Where(s => s.codec_type == "video").ToList()) + foreach (FFprobeStream stream in Optional(ffprobe.streams?.Where(s => s.codec_type == "video")).Flatten()) { FFprobeStream replacement = stream with { sample_aspect_ratio = sar, display_aspect_ratio = dar }; - ffprobe.streams.Remove(stream); - ffprobe.streams.Add(replacement); + ffprobe.streams?.Remove(stream); + ffprobe.streams?.Add(replacement); } } + + return ffprobe; } - return ffprobe; + return BaseError.New("Unable to deserialize ffprobe output"); } private async Task AnalyzeDuration(string ffmpegPath, string path, MediaVersion version) @@ -322,7 +325,7 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider internal MediaVersion ProjectToMediaVersion(string path, FFprobe probeOutput) => Optional(probeOutput) - .Filter(json => json?.format != null && json.streams != null) + .Filter(json => json is { format: { }, streams: { } }) .ToValidation("Unable to parse ffprobe output") .ToEither() .Match( @@ -337,7 +340,7 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider }; if (double.TryParse( - json.format.duration, + json.format?.duration, NumberStyles.Number, CultureInfo.InvariantCulture, out double duration)) @@ -380,7 +383,7 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider version.Streams.Add(stream); } - FFprobeStream videoStream = json.streams.FirstOrDefault(s => s.codec_type == "video"); + FFprobeStream? videoStream = json.streams?.FirstOrDefault(s => s.codec_type == "video"); if (videoStream != null) { version.SampleAspectRatio = string.IsNullOrWhiteSpace(videoStream.sample_aspect_ratio) @@ -465,7 +468,7 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider version.Streams.Add(stream); } - foreach (FFprobeChapter probedChapter in json.chapters) + foreach (FFprobeChapter probedChapter in Optional(json.chapters).Flatten()) { if (double.TryParse( probedChapter.start_time, @@ -484,7 +487,7 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider ChapterId = probedChapter.id, StartTime = TimeSpan.FromSeconds(startTime), EndTime = TimeSpan.FromSeconds(endTime), - Title = probedChapter?.tags?.title + Title = probedChapter.tags?.title }; version.Chapters.Add(chapter); @@ -516,60 +519,60 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider Chapters = new List() }); - private VideoScanKind ScanKindFromFieldOrder(string fieldOrder) => + private VideoScanKind ScanKindFromFieldOrder(string? fieldOrder) => fieldOrder?.ToLowerInvariant() switch { - var x when x == "tt" || x == "bb" || x == "tb" || x == "bt" => VideoScanKind.Interlaced, + "tt" or "bb" or "tb" or "bt" => VideoScanKind.Interlaced, "progressive" => VideoScanKind.Progressive, _ => VideoScanKind.Unknown }; // ReSharper disable InconsistentNaming - public record FFprobe(FFprobeFormat format, List streams, List chapters); + public record FFprobe(FFprobeFormat? format, List? streams, List? chapters); - public record FFprobeFormat(string duration, FFprobeTags tags); + public record FFprobeFormat(string duration, FFprobeTags? tags); public record FFprobeDisposition(int @default, int forced, int attached_pic); public record FFprobeStream( int index, - string codec_name, - string profile, - string codec_type, + string? codec_name, + string? profile, + string? codec_type, int channels, int width, int height, - string sample_aspect_ratio, - string display_aspect_ratio, - string pix_fmt, - string color_range, - string color_space, - string color_transfer, - string color_primaries, - string field_order, - string r_frame_rate, - string bits_per_raw_sample, - FFprobeDisposition disposition, - FFprobeTags tags); + string? sample_aspect_ratio, + string? display_aspect_ratio, + string? pix_fmt, + string? color_range, + string? color_space, + string? color_transfer, + string? color_primaries, + string? field_order, + string? r_frame_rate, + string? bits_per_raw_sample, + FFprobeDisposition? disposition, + FFprobeTags? tags); public record FFprobeChapter( long id, - string start_time, - string end_time, - FFprobeTags tags); + string? start_time, + string? end_time, + FFprobeTags? tags); public record FFprobeTags( - string language, - string title, - string filename, - string mimetype, - string artist, + string? language, + string? title, + string? filename, + string? mimetype, + string? artist, [property: JsonProperty(PropertyName = "album_artist")] - string albumArtist, - string album, - string track, - string genre, - string date) + string? albumArtist, + string? album, + string? track, + string? genre, + string? date) { public static readonly FFprobeTags Empty = new(null, null, null, null, null, null, null, null, null, null); } diff --git a/ErsatzTV.Core/Metadata/LocalSubtitlesProvider.cs b/ErsatzTV.Scanner/Core/Metadata/LocalSubtitlesProvider.cs similarity index 87% rename from ErsatzTV.Core/Metadata/LocalSubtitlesProvider.cs rename to ErsatzTV.Scanner/Core/Metadata/LocalSubtitlesProvider.cs index 8479b1c2c..59b49a850 100644 --- a/ErsatzTV.Core/Metadata/LocalSubtitlesProvider.cs +++ b/ErsatzTV.Scanner/Core/Metadata/LocalSubtitlesProvider.cs @@ -3,9 +3,10 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Scanner.Core.Interfaces.Metadata; using Microsoft.Extensions.Logging; -namespace ErsatzTV.Core.Metadata; +namespace ErsatzTV.Scanner.Core.Metadata; public class LocalSubtitlesProvider : ILocalSubtitlesProvider { @@ -50,12 +51,12 @@ public class LocalSubtitlesProvider : ILocalSubtitlesProvider return false; } - Option maybeMetadata = mediaItem switch + Option maybeMetadata = mediaItem switch { - Episode e => e.EpisodeMetadata.OfType().HeadOrNone(), - Movie m => m.MovieMetadata.OfType().HeadOrNone(), - MusicVideo mv => mv.MusicVideoMetadata.OfType().HeadOrNone(), - OtherVideo ov => ov.OtherVideoMetadata.OfType().HeadOrNone(), + Episode e => e.EpisodeMetadata.OfType().HeadOrNone(), + Movie m => m.MovieMetadata.OfType().HeadOrNone(), + MusicVideo mv => mv.MusicVideoMetadata.OfType().HeadOrNone(), + OtherVideo ov => ov.OtherVideoMetadata.OfType().HeadOrNone(), _ => None }; @@ -67,7 +68,7 @@ public class LocalSubtitlesProvider : ILocalSubtitlesProvider .GetType().Name); } - foreach (Domain.Metadata metadata in maybeMetadata) + foreach (ErsatzTV.Core.Domain.Metadata metadata in maybeMetadata) { MediaVersion version = mediaItem.GetHeadVersion(); var subtitleStreams = version.Streams @@ -96,7 +97,7 @@ public class LocalSubtitlesProvider : ILocalSubtitlesProvider { var result = new List(); - string folder = Path.GetDirectoryName(mediaItemPath); + string? folder = Path.GetDirectoryName(mediaItemPath); string withoutExtension = Path.GetFileNameWithoutExtension(mediaItemPath); foreach (string file in _localFileSystem.ListFiles(folder, $"{withoutExtension}*")) { diff --git a/ErsatzTV.Core/Metadata/MediaServerMovieLibraryScanner.cs b/ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs similarity index 98% rename from ErsatzTV.Core/Metadata/MediaServerMovieLibraryScanner.cs rename to ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs index 70292793e..34efa33cd 100644 --- a/ErsatzTV.Core/Metadata/MediaServerMovieLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs @@ -1,14 +1,16 @@ using System.Collections.Immutable; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.MediaServer; using ErsatzTV.Core.Errors; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.MediaSources; -using MediatR; +using ErsatzTV.Core.Metadata; +using ErsatzTV.Scanner.Core.Interfaces.Metadata; using Microsoft.Extensions.Logging; -namespace ErsatzTV.Core.Metadata; +namespace ErsatzTV.Scanner.Core.Metadata; public abstract class MediaServerMovieLibraryScanner where TConnectionParameters : MediaServerConnectionParameters @@ -236,7 +238,7 @@ public abstract class MediaServerMovieLibraryScanner diff --git a/ErsatzTV.Core/Metadata/MetadataSongTag.cs b/ErsatzTV.Scanner/Core/Metadata/MetadataSongTag.cs similarity index 90% rename from ErsatzTV.Core/Metadata/MetadataSongTag.cs rename to ErsatzTV.Scanner/Core/Metadata/MetadataSongTag.cs index aa400a80e..a918d9163 100644 --- a/ErsatzTV.Core/Metadata/MetadataSongTag.cs +++ b/ErsatzTV.Scanner/Core/Metadata/MetadataSongTag.cs @@ -1,4 +1,4 @@ -namespace ErsatzTV.Core.Metadata; +namespace ErsatzTV.Scanner.Core.Metadata; public static class MetadataSongTag { diff --git a/ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs index bf6ec2f7d..9908f815b 100644 --- a/ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs @@ -10,6 +10,7 @@ using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.MediaSources; using ErsatzTV.Core.Metadata; using ErsatzTV.Scanner.Core.Interfaces.FFmpeg; +using ErsatzTV.Scanner.Core.Interfaces.Metadata; using Microsoft.Extensions.Logging; using Seq = LanguageExt.Seq; diff --git a/ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs index 7c6a5ca4f..49e5922ca 100644 --- a/ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs @@ -9,6 +9,7 @@ using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.MediaSources; using ErsatzTV.Core.Metadata; using ErsatzTV.Scanner.Core.Interfaces.FFmpeg; +using ErsatzTV.Scanner.Core.Interfaces.Metadata; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Core.Metadata; diff --git a/ErsatzTV.Scanner/Core/Metadata/Nfo/ActorNfo.cs b/ErsatzTV.Scanner/Core/Metadata/Nfo/ActorNfo.cs new file mode 100644 index 000000000..a61e4c2f9 --- /dev/null +++ b/ErsatzTV.Scanner/Core/Metadata/Nfo/ActorNfo.cs @@ -0,0 +1,9 @@ +namespace ErsatzTV.Scanner.Core.Metadata.Nfo; + +public class ActorNfo +{ + public string? Name { get; set; } + public string? Role { get; set; } + public int? Order { get; set; } + public string? Thumb { get; set; } +} diff --git a/ErsatzTV.Scanner/Core/Metadata/Nfo/ArtistNfo.cs b/ErsatzTV.Scanner/Core/Metadata/Nfo/ArtistNfo.cs new file mode 100644 index 000000000..f4ca50881 --- /dev/null +++ b/ErsatzTV.Scanner/Core/Metadata/Nfo/ArtistNfo.cs @@ -0,0 +1,18 @@ +namespace ErsatzTV.Scanner.Core.Metadata.Nfo; + +public class ArtistNfo +{ + public ArtistNfo() + { + Genres = new List(); + Styles = new List(); + Moods = new List(); + } + + public string? Name { get; set; } + public string? Disambiguation { get; set; } + public List Genres { get; } + public List Styles { get; } + public List Moods { get; } + public string? Biography { get; set; } +} diff --git a/ErsatzTV.Core/Metadata/Nfo/ArtistNfoReader.cs b/ErsatzTV.Scanner/Core/Metadata/Nfo/ArtistNfoReader.cs similarity index 89% rename from ErsatzTV.Core/Metadata/Nfo/ArtistNfoReader.cs rename to ErsatzTV.Scanner/Core/Metadata/Nfo/ArtistNfoReader.cs index 79ca8e6a4..f3fbef99d 100644 --- a/ErsatzTV.Core/Metadata/Nfo/ArtistNfoReader.cs +++ b/ErsatzTV.Scanner/Core/Metadata/Nfo/ArtistNfoReader.cs @@ -1,11 +1,12 @@ using System.Xml; using Bugsnag; +using ErsatzTV.Core; using ErsatzTV.Core.Errors; -using ErsatzTV.Core.Interfaces.Metadata.Nfo; +using ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; using Microsoft.Extensions.Logging; using Microsoft.IO; -namespace ErsatzTV.Core.Metadata.Nfo; +namespace ErsatzTV.Scanner.Core.Metadata.Nfo; public class ArtistNfoReader : NfoReader, IArtistNfoReader { @@ -33,7 +34,7 @@ public class ArtistNfoReader : NfoReader, IArtistNfoReader internal async Task> Read(Stream input) { - ArtistNfo nfo = null; + ArtistNfo? nfo = null; try { @@ -49,12 +50,7 @@ public class ArtistNfoReader : NfoReader, IArtistNfoReader switch (reader.Name.ToLowerInvariant()) { case "artist": - nfo = new ArtistNfo - { - Genres = new List(), - Styles = new List(), - Moods = new List() - }; + nfo = new ArtistNfo(); break; case "name": await ReadStringContent(reader, nfo, (artist, name) => artist.Name = name); diff --git a/ErsatzTV.Scanner/Core/Metadata/Nfo/EpisodeNfo.cs b/ErsatzTV.Scanner/Core/Metadata/Nfo/EpisodeNfo.cs new file mode 100644 index 000000000..ce4331f65 --- /dev/null +++ b/ErsatzTV.Scanner/Core/Metadata/Nfo/EpisodeNfo.cs @@ -0,0 +1,24 @@ +namespace ErsatzTV.Scanner.Core.Metadata.Nfo; + +public class EpisodeNfo +{ + public EpisodeNfo() + { + Actors = new List(); + Writers = new List(); + Directors = new List(); + UniqueIds = new List(); + } + + public string? ShowTitle { get; set; } + public string? Title { get; set; } + public int Episode { get; set; } + public int Season { get; set; } + public string? ContentRating { get; set; } + public Option Aired { get; set; } + public string? Plot { get; set; } + public List Actors { get; } + public List Writers { get; } + public List Directors { get; } + public List UniqueIds { get; } +} diff --git a/ErsatzTV.Core/Metadata/Nfo/EpisodeNfoReader.cs b/ErsatzTV.Scanner/Core/Metadata/Nfo/EpisodeNfoReader.cs similarity index 84% rename from ErsatzTV.Core/Metadata/Nfo/EpisodeNfoReader.cs rename to ErsatzTV.Scanner/Core/Metadata/Nfo/EpisodeNfoReader.cs index 6fec90bcc..707aa5120 100644 --- a/ErsatzTV.Core/Metadata/Nfo/EpisodeNfoReader.cs +++ b/ErsatzTV.Scanner/Core/Metadata/Nfo/EpisodeNfoReader.cs @@ -1,13 +1,14 @@ using System.Xml; using Bugsnag; +using ErsatzTV.Core; using ErsatzTV.Core.Errors; -using ErsatzTV.Core.Interfaces.Metadata.Nfo; +using ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; using Microsoft.Extensions.Logging; using Microsoft.IO; -namespace ErsatzTV.Core.Metadata.Nfo; +namespace ErsatzTV.Scanner.Core.Metadata.Nfo; -public class EpisodeNfoReader : NfoReader, IEpisodeNfoReader +public class EpisodeNfoReader : NfoReader, IEpisodeNfoReader { private readonly IClient _client; private readonly ILogger _logger; @@ -22,7 +23,7 @@ public class EpisodeNfoReader : NfoReader, IEpisodeNfoReader _logger = logger; } - public async Task>> ReadFromFile(string fileName) + public async Task>> ReadFromFile(string fileName) { // ReSharper disable once ConvertToUsingDeclaration await using (Stream s = await SanitizedStreamForFile(fileName)) @@ -31,14 +32,14 @@ public class EpisodeNfoReader : NfoReader, IEpisodeNfoReader } } - internal async Task>> Read(Stream input) + internal async Task>> Read(Stream input) { - var result = new List(); + var result = new List(); try { using var reader = XmlReader.Create(input, Settings); - TvShowEpisodeNfo nfo = null; + EpisodeNfo? nfo = null; while (await reader.ReadAsync()) { @@ -48,13 +49,7 @@ public class EpisodeNfoReader : NfoReader, IEpisodeNfoReader switch (reader.Name.ToLowerInvariant()) { case "episodedetails": - nfo = new TvShowEpisodeNfo - { - UniqueIds = new List(), - Actors = new List(), - Writers = new List(), - Directors = new List() - }; + nfo = new EpisodeNfo(); // immediately add so we have something to return if we encounter invalid characters result.Add(nfo); break; diff --git a/ErsatzTV.Scanner/Core/Metadata/Nfo/MovieNfo.cs b/ErsatzTV.Scanner/Core/Metadata/Nfo/MovieNfo.cs new file mode 100644 index 000000000..6daa0d923 --- /dev/null +++ b/ErsatzTV.Scanner/Core/Metadata/Nfo/MovieNfo.cs @@ -0,0 +1,31 @@ +namespace ErsatzTV.Scanner.Core.Metadata.Nfo; + +public class MovieNfo +{ + public MovieNfo() + { + Genres = new List(); + Tags = new List(); + Studios = new List(); + Actors = new List(); + Writers = new List(); + Directors = new List(); + UniqueIds = new List(); + } + + public string? Title { get; set; } + public string? SortTitle { get; set; } + public string? Outline { get; set; } + public int Year { get; set; } + public string? ContentRating { get; set; } + public Option Premiered { get; set; } + public string? Plot { get; set; } + // public string? Tagline { get; set; } + public List Genres { get; } + public List Tags { get; } + public List Studios { get; } + public List Actors { get; } + public List Writers { get; } + public List Directors { get; } + public List UniqueIds { get; } +} diff --git a/ErsatzTV.Core/Metadata/Nfo/MovieNfoReader.cs b/ErsatzTV.Scanner/Core/Metadata/Nfo/MovieNfoReader.cs similarity index 88% rename from ErsatzTV.Core/Metadata/Nfo/MovieNfoReader.cs rename to ErsatzTV.Scanner/Core/Metadata/Nfo/MovieNfoReader.cs index 1e039b9c2..01ee15205 100644 --- a/ErsatzTV.Core/Metadata/Nfo/MovieNfoReader.cs +++ b/ErsatzTV.Scanner/Core/Metadata/Nfo/MovieNfoReader.cs @@ -1,11 +1,12 @@ using System.Xml; using Bugsnag; +using ErsatzTV.Core; using ErsatzTV.Core.Errors; -using ErsatzTV.Core.Interfaces.Metadata.Nfo; +using ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; using Microsoft.Extensions.Logging; using Microsoft.IO; -namespace ErsatzTV.Core.Metadata.Nfo; +namespace ErsatzTV.Scanner.Core.Metadata.Nfo; public class MovieNfoReader : NfoReader, IMovieNfoReader { @@ -33,7 +34,7 @@ public class MovieNfoReader : NfoReader, IMovieNfoReader internal async Task> Read(Stream input) { - MovieNfo nfo = null; + MovieNfo? nfo = null; try { @@ -49,16 +50,7 @@ public class MovieNfoReader : NfoReader, IMovieNfoReader switch (reader.Name.ToLowerInvariant()) { case "movie": - nfo = new MovieNfo - { - Genres = new List(), - Tags = new List(), - Studios = new List(), - Actors = new List(), - Writers = new List(), - Directors = new List(), - UniqueIds = new List() - }; + nfo = new MovieNfo(); break; case "title": await ReadStringContent(reader, nfo, (movie, title) => movie.Title = title); diff --git a/ErsatzTV.Scanner/Core/Metadata/Nfo/MusicVideoNfo.cs b/ErsatzTV.Scanner/Core/Metadata/Nfo/MusicVideoNfo.cs new file mode 100644 index 000000000..7a4cc185c --- /dev/null +++ b/ErsatzTV.Scanner/Core/Metadata/Nfo/MusicVideoNfo.cs @@ -0,0 +1,23 @@ +namespace ErsatzTV.Scanner.Core.Metadata.Nfo; + +public class MusicVideoNfo +{ + public MusicVideoNfo() + { + Artists = new List(); + Genres = new List(); + Tags = new List(); + Studios = new List(); + } + + public List Artists { get; } + public string? Title { get; set; } + public string? Album { get; set; } + public string? Plot { get; set; } + public int Track { get; set; } + public Option Aired { get; set; } + public int Year { get; set; } + public List Genres { get; } + public List Tags { get; } + public List Studios { get; } +} diff --git a/ErsatzTV.Core/Metadata/Nfo/MusicVideoNfoReader.cs b/ErsatzTV.Scanner/Core/Metadata/Nfo/MusicVideoNfoReader.cs similarity index 90% rename from ErsatzTV.Core/Metadata/Nfo/MusicVideoNfoReader.cs rename to ErsatzTV.Scanner/Core/Metadata/Nfo/MusicVideoNfoReader.cs index 0b20ea84a..88f770b1c 100644 --- a/ErsatzTV.Core/Metadata/Nfo/MusicVideoNfoReader.cs +++ b/ErsatzTV.Scanner/Core/Metadata/Nfo/MusicVideoNfoReader.cs @@ -1,11 +1,12 @@ using System.Xml; using Bugsnag; +using ErsatzTV.Core; using ErsatzTV.Core.Errors; -using ErsatzTV.Core.Interfaces.Metadata.Nfo; +using ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; using Microsoft.Extensions.Logging; using Microsoft.IO; -namespace ErsatzTV.Core.Metadata.Nfo; +namespace ErsatzTV.Scanner.Core.Metadata.Nfo; public class MusicVideoNfoReader : NfoReader, IMusicVideoNfoReader { @@ -33,7 +34,7 @@ public class MusicVideoNfoReader : NfoReader, IMusicVideoNfoReade internal async Task> Read(Stream input) { - MusicVideoNfo nfo = null; + MusicVideoNfo? nfo = null; try { @@ -49,13 +50,7 @@ public class MusicVideoNfoReader : NfoReader, IMusicVideoNfoReade switch (reader.Name.ToLowerInvariant()) { case "musicvideo": - nfo = new MusicVideoNfo - { - Artists = new List(), - Genres = new List(), - Tags = new List(), - Studios = new List() - }; + nfo = new MusicVideoNfo(); break; case "artist": await ReadStringContent( diff --git a/ErsatzTV.Core/Metadata/Nfo/NfoReader.cs b/ErsatzTV.Scanner/Core/Metadata/Nfo/NfoReader.cs similarity index 66% rename from ErsatzTV.Core/Metadata/Nfo/NfoReader.cs rename to ErsatzTV.Scanner/Core/Metadata/Nfo/NfoReader.cs index 12184bde4..77971b239 100644 --- a/ErsatzTV.Core/Metadata/Nfo/NfoReader.cs +++ b/ErsatzTV.Scanner/Core/Metadata/Nfo/NfoReader.cs @@ -1,30 +1,14 @@ using System.Text; -using System.Text.RegularExpressions; using System.Xml; using System.Xml.Linq; using Microsoft.Extensions.Logging; using Microsoft.IO; -namespace ErsatzTV.Core.Metadata.Nfo; +namespace ErsatzTV.Scanner.Core.Metadata.Nfo; -public abstract class NfoReader +public abstract class NfoReader : NfoReaderBase { - private static readonly byte[] Buffer = new byte[8 * 1024 * 1024]; - private static readonly Regex Pattern = new(@"[\p{C}-[\r\n\t]]+"); - - protected static readonly XmlReaderSettings Settings = - new() - { - Async = true, - ConformanceLevel = ConformanceLevel.Fragment, - ValidationType = ValidationType.None, - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true - }; - private readonly ILogger _logger; - private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; protected NfoReader(RecyclableMemoryStreamManager recyclableMemoryStreamManager, ILogger logger) @@ -47,11 +31,11 @@ public abstract class NfoReader return ms; } - protected async Task ReadStringContent(XmlReader reader, T nfo, Action action) + protected async Task ReadStringContent(XmlReader reader, T? nfo, Action action) { try { - if (nfo != null) + if (nfo is not null) { string result = await reader.ReadElementContentAsStringAsync(); action(nfo, result); @@ -63,11 +47,11 @@ public abstract class NfoReader } } - protected async Task ReadIntContent(XmlReader reader, T nfo, Action action) + protected async Task ReadIntContent(XmlReader reader, T? nfo, Action action) { try { - if (nfo != null && int.TryParse(await reader.ReadElementContentAsStringAsync(), out int result)) + if (nfo is not null && int.TryParse(await reader.ReadElementContentAsStringAsync(), out int result)) { action(nfo, result); } @@ -78,11 +62,11 @@ public abstract class NfoReader } } - protected async Task ReadDateTimeContent(XmlReader reader, T nfo, Action action) + protected async Task ReadDateTimeContent(XmlReader reader, T? nfo, Action action) { try { - if (nfo != null && DateTime.TryParse(await reader.ReadElementContentAsStringAsync(), out DateTime result)) + if (nfo is not null && DateTime.TryParse(await reader.ReadElementContentAsStringAsync(), out DateTime result)) { action(nfo, result); } @@ -93,35 +77,35 @@ public abstract class NfoReader } } - protected void ReadActor(XmlReader reader, T nfo, Action action) + protected void ReadActor(XmlReader reader, T? nfo, Action action) { try { - if (nfo != null) + if (nfo is not null) { var actor = new ActorNfo(); var element = (XElement)XNode.ReadFrom(reader); - XElement name = element.Element("name"); - if (name != null) + XElement? name = element.Element("name"); + if (name is not null) { actor.Name = name.Value; } - XElement role = element.Element("role"); - if (role != null) + XElement? role = element.Element("role"); + if (role is not null) { actor.Role = role.Value; } - XElement order = element.Element("order"); - if (order != null && int.TryParse(order.Value, out int orderValue)) + XElement? order = element.Element("order"); + if (order is not null && int.TryParse(order.Value, out int orderValue)) { actor.Order = orderValue; } - XElement thumb = element.Element("thumb"); - if (thumb != null) + XElement? thumb = element.Element("thumb"); + if (thumb is not null) { actor.Thumb = thumb.Value; } @@ -135,11 +119,11 @@ public abstract class NfoReader } } - protected async Task ReadUniqueId(XmlReader reader, T nfo, Action action) + protected async Task ReadUniqueId(XmlReader reader, T? nfo, Action action) { try { - if (nfo != null) + if (nfo is not null) { var uniqueId = new UniqueIdNfo(); reader.MoveToAttribute("default"); diff --git a/ErsatzTV.Scanner/Core/Metadata/Nfo/NfoReaderBase.cs b/ErsatzTV.Scanner/Core/Metadata/Nfo/NfoReaderBase.cs new file mode 100644 index 000000000..a490b0d99 --- /dev/null +++ b/ErsatzTV.Scanner/Core/Metadata/Nfo/NfoReaderBase.cs @@ -0,0 +1,24 @@ +using System.Text.RegularExpressions; +using System.Xml; + +namespace ErsatzTV.Scanner.Core.Metadata.Nfo; + +public abstract partial class NfoReaderBase +{ + protected static readonly byte[] Buffer = new byte[8 * 1024 * 1024]; + protected static readonly Regex Pattern = ControlCharacters(); + + protected static readonly XmlReaderSettings Settings = + new() + { + Async = true, + ConformanceLevel = ConformanceLevel.Fragment, + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; + + [GeneratedRegex("[\\p{C}-[\\r\\n\\t]]+")] + private static partial Regex ControlCharacters(); +} diff --git a/ErsatzTV.Scanner/Core/Metadata/Nfo/OtherVideoNfo.cs b/ErsatzTV.Scanner/Core/Metadata/Nfo/OtherVideoNfo.cs new file mode 100644 index 000000000..63303f16c --- /dev/null +++ b/ErsatzTV.Scanner/Core/Metadata/Nfo/OtherVideoNfo.cs @@ -0,0 +1,31 @@ +namespace ErsatzTV.Scanner.Core.Metadata.Nfo; + +public class OtherVideoNfo +{ + public OtherVideoNfo() + { + Genres = new List(); + Tags = new List(); + Studios = new List(); + Actors = new List(); + Writers = new List(); + Directors = new List(); + UniqueIds = new List(); + } + + public string? Title { get; set; } + public string? SortTitle { get; set; } + public string? Outline { get; set; } + public int Year { get; set; } + public string? ContentRating { get; set; } + public Option Premiered { get; set; } + public string? Plot { get; set; } + public string? Tagline { get; set; } + public List Genres { get; } + public List Tags { get; } + public List Studios { get; } + public List Actors { get; } + public List Writers { get; } + public List Directors { get; } + public List UniqueIds { get; } +} diff --git a/ErsatzTV.Core/Metadata/Nfo/OtherVideoNfoReader.cs b/ErsatzTV.Scanner/Core/Metadata/Nfo/OtherVideoNfoReader.cs similarity index 88% rename from ErsatzTV.Core/Metadata/Nfo/OtherVideoNfoReader.cs rename to ErsatzTV.Scanner/Core/Metadata/Nfo/OtherVideoNfoReader.cs index ad69876f8..7c0adb2cc 100644 --- a/ErsatzTV.Core/Metadata/Nfo/OtherVideoNfoReader.cs +++ b/ErsatzTV.Scanner/Core/Metadata/Nfo/OtherVideoNfoReader.cs @@ -1,11 +1,12 @@ using System.Xml; using Bugsnag; +using ErsatzTV.Core; using ErsatzTV.Core.Errors; -using ErsatzTV.Core.Interfaces.Metadata.Nfo; +using ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; using Microsoft.Extensions.Logging; using Microsoft.IO; -namespace ErsatzTV.Core.Metadata.Nfo; +namespace ErsatzTV.Scanner.Core.Metadata.Nfo; public class OtherVideoNfoReader : NfoReader, IOtherVideoNfoReader { @@ -33,7 +34,7 @@ public class OtherVideoNfoReader : NfoReader, IOtherVideoNfoReade internal async Task> Read(Stream input) { - OtherVideoNfo nfo = null; + OtherVideoNfo? nfo = null; try { @@ -49,16 +50,7 @@ public class OtherVideoNfoReader : NfoReader, IOtherVideoNfoReade switch (reader.Name.ToLowerInvariant()) { case "movie": - nfo = new OtherVideoNfo - { - Genres = new List(), - Tags = new List(), - Studios = new List(), - Actors = new List(), - Writers = new List(), - Directors = new List(), - UniqueIds = new List() - }; + nfo = new OtherVideoNfo(); break; case "title": await ReadStringContent(reader, nfo, (movie, title) => movie.Title = title); diff --git a/ErsatzTV.Scanner/Core/Metadata/Nfo/ShowNfo.cs b/ErsatzTV.Scanner/Core/Metadata/Nfo/ShowNfo.cs new file mode 100644 index 000000000..f761f45a5 --- /dev/null +++ b/ErsatzTV.Scanner/Core/Metadata/Nfo/ShowNfo.cs @@ -0,0 +1,26 @@ +namespace ErsatzTV.Scanner.Core.Metadata.Nfo; + +public class ShowNfo +{ + public ShowNfo() + { + Genres = new List(); + Tags = new List(); + Studios = new List(); + Actors = new List(); + UniqueIds = new List(); + } + + public string? Title { get; set; } + public int? Year { get; set; } + public string? Plot { get; set; } + public string? Outline { get; set; } + public string? Tagline { get; set; } + public string? ContentRating { get; set; } + public Option Premiered { get; set; } + public List Genres { get; } + public List Tags { get; } + public List Studios { get; } + public List Actors { get; } + public List UniqueIds { get; } +} diff --git a/ErsatzTV.Core/Metadata/Nfo/TvShowNfoReader.cs b/ErsatzTV.Scanner/Core/Metadata/Nfo/ShowNfoReader.cs similarity index 82% rename from ErsatzTV.Core/Metadata/Nfo/TvShowNfoReader.cs rename to ErsatzTV.Scanner/Core/Metadata/Nfo/ShowNfoReader.cs index 5a18cb9c7..dddca3e2d 100644 --- a/ErsatzTV.Core/Metadata/Nfo/TvShowNfoReader.cs +++ b/ErsatzTV.Scanner/Core/Metadata/Nfo/ShowNfoReader.cs @@ -1,28 +1,29 @@ using System.Xml; using Bugsnag; +using ErsatzTV.Core; using ErsatzTV.Core.Errors; -using ErsatzTV.Core.Interfaces.Metadata.Nfo; +using ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; using Microsoft.Extensions.Logging; using Microsoft.IO; -namespace ErsatzTV.Core.Metadata.Nfo; +namespace ErsatzTV.Scanner.Core.Metadata.Nfo; -public class TvShowNfoReader : NfoReader, ITvShowNfoReader +public class ShowNfoReader : NfoReader, IShowNfoReader { private readonly IClient _client; - private readonly ILogger _logger; + private readonly ILogger _logger; - public TvShowNfoReader( + public ShowNfoReader( RecyclableMemoryStreamManager recyclableMemoryStreamManager, IClient client, - ILogger logger) + ILogger logger) : base(recyclableMemoryStreamManager, logger) { _client = client; _logger = logger; } - public async Task> ReadFromFile(string fileName) + public async Task> ReadFromFile(string fileName) { // ReSharper disable once ConvertToUsingDeclaration await using (Stream s = await SanitizedStreamForFile(fileName)) @@ -31,9 +32,9 @@ public class TvShowNfoReader : NfoReader, ITvShowNfoReader } } - internal async Task> Read(Stream input) + internal async Task> Read(Stream input) { - TvShowNfo nfo = null; + ShowNfo? nfo = null; try { @@ -49,14 +50,7 @@ public class TvShowNfoReader : NfoReader, ITvShowNfoReader switch (reader.Name.ToLowerInvariant()) { case "tvshow": - nfo = new TvShowNfo - { - Genres = new List(), - Tags = new List(), - Studios = new List(), - Actors = new List(), - UniqueIds = new List() - }; + nfo = new ShowNfo(); break; case "title": await ReadStringContent(reader, nfo, (show, title) => show.Title = title); diff --git a/ErsatzTV.Scanner/Core/Metadata/Nfo/UniqueIdNfo.cs b/ErsatzTV.Scanner/Core/Metadata/Nfo/UniqueIdNfo.cs new file mode 100644 index 000000000..07fb40728 --- /dev/null +++ b/ErsatzTV.Scanner/Core/Metadata/Nfo/UniqueIdNfo.cs @@ -0,0 +1,8 @@ +namespace ErsatzTV.Scanner.Core.Metadata.Nfo; + +public class UniqueIdNfo +{ + public bool Default { get; set; } + public string? Type { get; set; } + public string? Guid { get; set; } +} diff --git a/ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs index 0f304c54f..2371686f7 100644 --- a/ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs @@ -9,6 +9,7 @@ using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.MediaSources; using ErsatzTV.Core.Metadata; using ErsatzTV.Scanner.Core.Interfaces.FFmpeg; +using ErsatzTV.Scanner.Core.Interfaces.Metadata; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Core.Metadata; diff --git a/ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs index 52bf64b5d..8945cef18 100644 --- a/ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs @@ -10,6 +10,7 @@ using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.MediaSources; using ErsatzTV.Core.Metadata; using ErsatzTV.Scanner.Core.Interfaces.FFmpeg; +using ErsatzTV.Scanner.Core.Interfaces.Metadata; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Core.Metadata; diff --git a/ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs index 5be198d23..1bdca3c0a 100644 --- a/ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs @@ -9,6 +9,7 @@ using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.MediaSources; using ErsatzTV.Core.Metadata; using ErsatzTV.Scanner.Core.Interfaces.FFmpeg; +using ErsatzTV.Scanner.Core.Interfaces.Metadata; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Core.Metadata; diff --git a/ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs b/ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs index 7f200b3a6..6b3ba210a 100644 --- a/ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs @@ -6,6 +6,8 @@ using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Plex; +using ErsatzTV.Scanner.Core.Interfaces.Metadata; +using ErsatzTV.Scanner.Core.Metadata; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Core.Plex; diff --git a/ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs b/ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs index 70c5c1c88..bf991ecf5 100644 --- a/ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs @@ -6,6 +6,8 @@ using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Plex; +using ErsatzTV.Scanner.Core.Interfaces.Metadata; +using ErsatzTV.Scanner.Core.Metadata; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Core.Plex; diff --git a/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj b/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj index 09ed0e8ec..fa10d9047 100644 --- a/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj +++ b/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj @@ -29,4 +29,10 @@ + + + <_Parameter1>ErsatzTV.Scanner.Tests + + + diff --git a/ErsatzTV.Scanner/Program.cs b/ErsatzTV.Scanner/Program.cs index 65c993b0c..842bc4ef2 100644 --- a/ErsatzTV.Scanner/Program.cs +++ b/ErsatzTV.Scanner/Program.cs @@ -8,14 +8,12 @@ using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Metadata; -using ErsatzTV.Core.Interfaces.Metadata.Nfo; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories.Caching; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Jellyfin; using ErsatzTV.Core.Metadata; -using ErsatzTV.Core.Metadata.Nfo; using ErsatzTV.Core.Plex; using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.Infrastructure.Data; @@ -30,8 +28,11 @@ using ErsatzTV.Infrastructure.Search; using ErsatzTV.Scanner.Core.Emby; using ErsatzTV.Scanner.Core.FFmpeg; using ErsatzTV.Scanner.Core.Interfaces.FFmpeg; +using ErsatzTV.Scanner.Core.Interfaces.Metadata; +using ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; using ErsatzTV.Scanner.Core.Jellyfin; using ErsatzTV.Scanner.Core.Metadata; +using ErsatzTV.Scanner.Core.Metadata.Nfo; using ErsatzTV.Scanner.Core.Plex; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -126,7 +127,7 @@ public class Program services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/ErsatzTV.Scanner/Worker.cs b/ErsatzTV.Scanner/Worker.cs index c5505fd4e..a817c8bc9 100644 --- a/ErsatzTV.Scanner/Worker.cs +++ b/ErsatzTV.Scanner/Worker.cs @@ -6,24 +6,38 @@ using ErsatzTV.Scanner.Application.Plex; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Serilog; namespace ErsatzTV.Scanner; -public class Worker : IHostedService +public class Worker : BackgroundService { private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IHostApplicationLifetime _appLifetime; private readonly ILogger _logger; - public Worker(IServiceScopeFactory serviceScopeFactory, IHostApplicationLifetime appLifetime, ILogger logger) + public Worker( + IServiceScopeFactory serviceScopeFactory, + IHostApplicationLifetime appLifetime, + ILogger logger) { _serviceScopeFactory = serviceScopeFactory; _appLifetime = appLifetime; _logger = logger; } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + RootCommand rootCommand = ConfigureCommandLine(); + + // need to strip program name (head) from command line args + string[] arguments = Environment.GetCommandLineArgs().Skip(1).ToArray(); - public async Task StartAsync(CancellationToken cancellationToken) + await rootCommand.InvokeAsync(arguments); + + _appLifetime.StopApplication(); + } + + private RootCommand ConfigureCommandLine() { var forceOption = new System.CommandLine.Option( name: "--force", @@ -43,94 +57,111 @@ public class Worker : IHostedService Arity = ArgumentArity.Zero }; - var localOption = new System.CommandLine.Option( - name: "--local", - description: "The local library id to scan"); + var libraryIdArgument = new Argument("library-id", "The library id to scan"); + + var scanLocalCommand = new Command("scan-local", "Scan a local library"); + scanLocalCommand.AddArgument(libraryIdArgument); + scanLocalCommand.AddOption(forceOption); + + var scanPlexCommand = new Command("scan-plex", "Scan a Plex library"); + scanPlexCommand.AddArgument(libraryIdArgument); + scanPlexCommand.AddOption(forceOption); + scanPlexCommand.AddOption(deepOption); + + var scanEmbyCommand = new Command("scan-emby", "Scan an Emby library"); + scanEmbyCommand.AddArgument(libraryIdArgument); + scanEmbyCommand.AddOption(forceOption); + + var scanJellyfinCommand = new Command("scan-jellyfin", "Scan a Jellyfin library"); + scanJellyfinCommand.AddArgument(libraryIdArgument); + scanJellyfinCommand.AddOption(forceOption); + + scanLocalCommand.SetHandler( + async context => + { + if (IsScanningEnabled()) + { + bool force = context.ParseResult.GetValueForOption(forceOption); + int libraryId = context.ParseResult.GetValueForArgument(libraryIdArgument); - var plexOption = new System.CommandLine.Option( - name: "--plex", - description: "The plex library id to scan"); + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); - var embyOption = new System.CommandLine.Option( - name: "--emby", - description: "The emby library id to scan"); + var scan = new ScanLocalLibrary(libraryId, force); + await mediator.Send(scan, context.GetCancellationToken()); + } + }); + + scanPlexCommand.SetHandler( + async context => + { + if (IsScanningEnabled()) + { + bool force = context.ParseResult.GetValueForOption(forceOption); + bool deep = context.ParseResult.GetValueForOption(deepOption); + int libraryId = context.ParseResult.GetValueForArgument(libraryIdArgument); + + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); + + var scan = new SynchronizePlexLibraryById(libraryId, force, deep); + await mediator.Send(scan, context.GetCancellationToken()); + } + }); + + scanEmbyCommand.SetHandler( + async context => + { + if (IsScanningEnabled()) + { + bool force = context.ParseResult.GetValueForOption(forceOption); + int libraryId = context.ParseResult.GetValueForArgument(libraryIdArgument); - var jellyfinOption = new System.CommandLine.Option( - name: "--jellyfin", - description: "The jellyfin library id to scan"); + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); - var rootCommand = new RootCommand(); - rootCommand.AddOption(forceOption); - rootCommand.AddOption(deepOption); - rootCommand.AddOption(localOption); - rootCommand.AddOption(plexOption); - rootCommand.AddOption(embyOption); - rootCommand.AddOption(jellyfinOption); - - rootCommand.SetHandler( + var scan = new SynchronizeEmbyLibraryById(libraryId, force); + await mediator.Send(scan, context.GetCancellationToken()); + } + }); + + scanJellyfinCommand.SetHandler( async context => { - bool force = context.ParseResult.GetValueForOption(forceOption); - bool deep = context.ParseResult.GetValueForOption(deepOption); - int? local = context.ParseResult.GetValueForOption(localOption); - int? plex = context.ParseResult.GetValueForOption(plexOption); - int? emby = context.ParseResult.GetValueForOption(embyOption); - int? jellyfin = context.ParseResult.GetValueForOption(jellyfinOption); - CancellationToken token = context.GetCancellationToken(); - await Handle(force, deep, local, plex, emby, jellyfin, token); + if (IsScanningEnabled()) + { + bool force = context.ParseResult.GetValueForOption(forceOption); + int libraryId = context.ParseResult.GetValueForArgument(libraryIdArgument); + + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); + + var scan = new SynchronizeJellyfinLibraryById(libraryId, force); + await mediator.Send(scan, context.GetCancellationToken()); + } }); - // need to strip program name (head) from command line args - await rootCommand.InvokeAsync(Environment.GetCommandLineArgs().Skip(1).ToArray()); + var rootCommand = new RootCommand(); + rootCommand.AddCommand(scanLocalCommand); + rootCommand.AddCommand(scanPlexCommand); + rootCommand.AddCommand(scanEmbyCommand); + rootCommand.AddCommand(scanJellyfinCommand); - _appLifetime.StopApplication(); + return rootCommand; } - public Task StopAsync(CancellationToken cancellationToken) +#if !DEBUG_NO_SYNC + private bool IsScanningEnabled() { - return Task.CompletedTask; + // don't want to flag the logger as unused (only used when sync is disabled) + ILogger _ = _logger; + return true; } - - private async Task Handle( - bool forceOption, - bool deepOption, - int? localLibraryId, - int? plexLibraryId, - int? embyLibraryId, - int? jellyfinLibraryId, - CancellationToken cancellationToken) - { -#if !DEBUG_NO_SYNC - using IServiceScope scope = _serviceScopeFactory.CreateScope(); - - IMediator mediator = scope.ServiceProvider.GetRequiredService(); - - if (localLibraryId is not null) - { - var scanLocalLibrary = new ScanLocalLibrary(localLibraryId.Value, forceOption); - await mediator.Send(scanLocalLibrary, cancellationToken); - } - else if (plexLibraryId is not null) - { - var scanPlexLibrary = new SynchronizePlexLibraryById(plexLibraryId.Value, forceOption, deepOption); - await mediator.Send(scanPlexLibrary, cancellationToken); - } - else if (embyLibraryId is not null) - { - var scanEmbyLibrary = new SynchronizeEmbyLibraryById(embyLibraryId.Value, forceOption); - await mediator.Send(scanEmbyLibrary, cancellationToken); - } - else if (jellyfinLibraryId is not null) - { - var scanJellyfinLibrary = new SynchronizeJellyfinLibraryById(jellyfinLibraryId.Value, forceOption); - await mediator.Send(scanJellyfinLibrary, cancellationToken); - } - else - { - Log.Logger.Error("No library ids were specified; nothing to scan."); - } #else - Log.Logger.Information("Library scanning is disabled..."); -#endif + private bool IsScanningEnabled() + { + _logger.LogInformation("Scanning is disabled via DEBUG_NO_SYNC"); + return false; } +#endif } diff --git a/ErsatzTV.sln.DotSettings b/ErsatzTV.sln.DotSettings index bfde66f92..a765cc833 100644 --- a/ErsatzTV.sln.DotSettings +++ b/ErsatzTV.sln.DotSettings @@ -49,6 +49,7 @@ True True True + True True True True @@ -60,6 +61,7 @@ True True True + True True True True diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 643a14c1c..067d5a5ad 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -19,7 +19,6 @@ using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Metadata; -using ErsatzTV.Core.Interfaces.Metadata.Nfo; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories.Caching; @@ -29,7 +28,6 @@ using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Trakt; using ErsatzTV.Core.Jellyfin; using ErsatzTV.Core.Metadata; -using ErsatzTV.Core.Metadata.Nfo; using ErsatzTV.Core.Plex; using ErsatzTV.Core.Scheduling; using ErsatzTV.Core.Trakt; @@ -410,10 +408,7 @@ public class Startup services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -455,12 +450,6 @@ public class Startup }); services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped();