Browse Source

scanner refactoring and other cleanup (#1082)

* move subtitles provider into scanner

* move more stuff into scanner

* move nfo into scanner

* add scan subcommand

* fix a bunch of nfo build warnings

* more subcommands

* fix warnings

* cleanup logging

* remove unused code

* cleanup old ffmpeg stuff

* rename complex filter

* refactor wrapped segmenter
pull/1083/head
Jason Dove 4 years ago committed by GitHub
parent
commit
9479806cb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      ErsatzTV.Application/Emby/Commands/CallEmbyLibraryScannerHandler.cs
  2. 3
      ErsatzTV.Application/Jellyfin/Commands/CallJellyfinLibraryScannerHandler.cs
  3. 3
      ErsatzTV.Application/MediaSources/Commands/CallLocalLibraryScannerHandler.cs
  4. 3
      ErsatzTV.Application/MediaSources/LocalMediaSourceViewModel.cs
  5. 3
      ErsatzTV.Application/Plex/Commands/CallPlexLibraryScannerHandler.cs
  6. 2
      ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs
  7. 9
      ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj
  8. 777
      ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs
  9. 337
      ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs
  10. 33
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  11. 535
      ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs
  12. 31
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  13. 2
      ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs
  14. 1
      ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs
  15. 8
      ErsatzTV.Core/Interfaces/Metadata/Nfo/IArtistNfoReader.cs
  16. 8
      ErsatzTV.Core/Interfaces/Metadata/Nfo/IEpisodeNfoReader.cs
  17. 8
      ErsatzTV.Core/Interfaces/Metadata/Nfo/IMovieNfoReader.cs
  18. 8
      ErsatzTV.Core/Interfaces/Metadata/Nfo/IMusicVideoNfoReader.cs
  19. 8
      ErsatzTV.Core/Interfaces/Metadata/Nfo/IOtherVideoNfoReader.cs
  20. 8
      ErsatzTV.Core/Interfaces/Metadata/Nfo/ITvShowNfoReader.cs
  21. 1
      ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs
  22. 1
      ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs
  23. 30
      ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs
  24. 2
      ErsatzTV.Core/Metadata/LocalFileSystem.cs
  25. 18
      ErsatzTV.Core/Metadata/Nfo/ActorNfo.cs
  26. 25
      ErsatzTV.Core/Metadata/Nfo/ArtistNfo.cs
  27. 52
      ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs
  28. 37
      ErsatzTV.Core/Metadata/Nfo/MusicVideoNfo.cs
  29. 52
      ErsatzTV.Core/Metadata/Nfo/OtherVideoNfo.cs
  30. 40
      ErsatzTV.Core/Metadata/Nfo/TvShowEpisodeNfo.cs
  31. 50
      ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs
  32. 15
      ErsatzTV.Core/Metadata/Nfo/UniqueIdNfo.cs
  33. 34
      ErsatzTV.Core/Metadata/SortTitle.cs
  34. 3
      ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs
  35. 25
      ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs
  36. 1
      ErsatzTV.FFmpeg/CommandGenerator.cs
  37. 286
      ErsatzTV.FFmpeg/Filter/ComplexFilter.cs
  38. 252
      ErsatzTV.FFmpeg/Filter/NewComplexFilter.cs
  39. 6
      ErsatzTV.FFmpeg/Option/MapAllStreamsOutputOption.cs
  40. 13
      ErsatzTV.FFmpeg/OutputFormat/OutputFormatMpegTs.cs
  41. 1
      ErsatzTV.FFmpeg/Pipeline/IPipelineBuilder.cs
  42. 31
      ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs
  43. 14
      ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs
  44. 8
      ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs
  45. 13
      ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs
  46. 12
      ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs
  47. 16
      ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs
  48. 14
      ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs
  49. 18
      ErsatzTV.Scanner.Tests/Core/Metadata/LocalStatisticsProviderTests.cs
  50. 6
      ErsatzTV.Scanner.Tests/Core/Metadata/LocalSubtitlesProviderTests.cs
  51. 1
      ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs
  52. 5
      ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/ArtistNfoReaderTests.cs
  53. 53
      ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/EpisodeNfoReaderTests.cs
  54. 7
      ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/MovieNfoReaderTests.cs
  55. 5
      ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/MusicVideoNfoReaderTests.cs
  56. 5
      ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/OtherVideoNfoReaderTests.cs
  57. 35
      ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/ShowNfoReaderTests.cs
  58. 12
      ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj
  59. 0
      ErsatzTV.Scanner.Tests/Resources/Nfo/ArtistInvalidCharacters1.nfo
  60. 0
      ErsatzTV.Scanner.Tests/Resources/Nfo/ArtistInvalidCharacters2.nfo
  61. 0
      ErsatzTV.Scanner.Tests/Resources/Nfo/EpisodeInvalidCharacters.nfo
  62. 2
      ErsatzTV.Scanner/Application/MediaSources/Commands/ScanLocalLibraryHandler.cs
  63. 4
      ErsatzTV.Scanner/Core/Emby/EmbyCollectionScanner.cs
  64. 2
      ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs
  65. 2
      ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs
  66. 2
      ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalMetadataProvider.cs
  67. 5
      ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalStatisticsProvider.cs
  68. 2
      ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalSubtitlesProvider.cs
  69. 5
      ErsatzTV.Scanner/Core/Interfaces/Metadata/IMovieFolderScanner.cs
  70. 5
      ErsatzTV.Scanner/Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs
  71. 5
      ErsatzTV.Scanner/Core/Interfaces/Metadata/IOtherVideoFolderScanner.cs
  72. 5
      ErsatzTV.Scanner/Core/Interfaces/Metadata/ISongFolderScanner.cs
  73. 5
      ErsatzTV.Scanner/Core/Interfaces/Metadata/ITelevisionFolderScanner.cs
  74. 9
      ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IArtistNfoReader.cs
  75. 9
      ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IEpisodeNfoReader.cs
  76. 9
      ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IMovieNfoReader.cs
  77. 9
      ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IMusicVideoNfoReader.cs
  78. 9
      ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IOtherVideoNfoReader.cs
  79. 9
      ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IShowNfoReader.cs
  80. 4
      ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs
  81. 2
      ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs
  82. 2
      ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs
  83. 2
      ErsatzTV.Scanner/Core/Metadata/FolderEtag.cs
  84. 1
      ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs
  85. 79
      ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs
  86. 93
      ErsatzTV.Scanner/Core/Metadata/LocalStatisticsProvider.cs
  87. 17
      ErsatzTV.Scanner/Core/Metadata/LocalSubtitlesProvider.cs
  88. 8
      ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs
  89. 8
      ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs
  90. 2
      ErsatzTV.Scanner/Core/Metadata/MetadataSongTag.cs
  91. 1
      ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs
  92. 1
      ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs
  93. 9
      ErsatzTV.Scanner/Core/Metadata/Nfo/ActorNfo.cs
  94. 18
      ErsatzTV.Scanner/Core/Metadata/Nfo/ArtistNfo.cs
  95. 14
      ErsatzTV.Scanner/Core/Metadata/Nfo/ArtistNfoReader.cs
  96. 24
      ErsatzTV.Scanner/Core/Metadata/Nfo/EpisodeNfo.cs
  97. 23
      ErsatzTV.Scanner/Core/Metadata/Nfo/EpisodeNfoReader.cs
  98. 31
      ErsatzTV.Scanner/Core/Metadata/Nfo/MovieNfo.cs
  99. 18
      ErsatzTV.Scanner/Core/Metadata/Nfo/MovieNfoReader.cs
  100. 23
      ErsatzTV.Scanner/Core/Metadata/Nfo/MusicVideoNfo.cs
  101. Some files were not shown because too many files have changed in this diff Show More

3
ErsatzTV.Application/Emby/Commands/CallEmbyLibraryScannerHandler.cs

@ -42,8 +42,7 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler, @@ -42,8 +42,7 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler,
{
var arguments = new List<string>
{
"--emby",
request.EmbyLibraryId.ToString()
"scan-emby", request.EmbyLibraryId.ToString()
};
if (request.ForceScan)

3
ErsatzTV.Application/Jellyfin/Commands/CallJellyfinLibraryScannerHandler.cs

@ -42,8 +42,7 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler, @@ -42,8 +42,7 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler,
{
var arguments = new List<string>
{
"--jellyfin",
request.JellyfinLibraryId.ToString()
"scan-jellyfin", request.JellyfinLibraryId.ToString()
};
if (request.ForceScan)

3
ErsatzTV.Application/MediaSources/Commands/CallLocalLibraryScannerHandler.cs

@ -40,8 +40,7 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler, @@ -40,8 +40,7 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler,
{
var arguments = new List<string>
{
"--local",
request.LibraryId.ToString()
"scan-local", request.LibraryId.ToString()
};
if (request.ForceScan)

3
ErsatzTV.Application/MediaSources/LocalMediaSourceViewModel.cs

@ -1,3 +0,0 @@ @@ -1,3 +0,0 @@
namespace ErsatzTV.Application.MediaSources;
public record LocalMediaSourceViewModel(int Id) : MediaSourceViewModel(Id, "Local");

3
ErsatzTV.Application/Plex/Commands/CallPlexLibraryScannerHandler.cs

@ -42,8 +42,7 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler, @@ -42,8 +42,7 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler,
{
var arguments = new List<string>
{
"--plex",
request.PlexLibraryId.ToString()
"scan-plex", request.PlexLibraryId.ToString()
};
if (request.ForceScan)

2
ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs

@ -30,7 +30,7 @@ public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler<GetW @@ -30,7 +30,7 @@ public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler<GetW
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
.Map(result => result.IfNone(false));
Command process = _ffmpegProcessService.WrapSegmenter(
Command process = await _ffmpegProcessService.WrapSegmenter(
ffmpegPath,
saveReports,
channel,

9
ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj

@ -38,21 +38,12 @@ @@ -38,21 +38,12 @@
<Content Include="Resources\ErsatzTV.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Resources\Nfo\ArtistInvalidCharacters1.nfo">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Resources\Nfo\ArtistInvalidCharacters2.nfo">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Resources\test.sup">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Resources\test.srt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Resources\Nfo\EpisodeInvalidCharacters.nfo">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

777
ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs

@ -1,777 +0,0 @@ @@ -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<FFmpegComplexFilter> 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<FFmpegComplexFilter> 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<FFmpegComplexFilter> 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<FFmpegComplexFilter> 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<FFmpegComplexFilter> 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<List<FadePoint>> 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<FFmpegComplexFilter> 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<List<FadePoint>> 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<FFmpegComplexFilter> 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<FFmpegComplexFilter> 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<FFmpegComplexFilter> 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<FFmpegComplexFilter> 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);
});
}
}
}

337
ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs

@ -1,5 +1,4 @@ @@ -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; @@ -9,29 +8,16 @@ namespace ErsatzTV.Core.FFmpeg;
public class FFmpegComplexFilterBuilder
{
private Option<TimeSpan> _audioDuration = None;
private bool _boxBlur;
private bool _deinterlace;
private Option<HardwareAccelerationKind> _hardwareAccelerationKind = None;
private string _inputCodec;
private Option<List<FadePoint>> _maybeFadePoints = None;
private bool _normalizeLoudness;
private Option<IDisplaySize> _padToSize = None;
private string _pixelFormat;
private IDisplaySize _resolution;
private Option<IDisplaySize> _scaleToSize = None;
private Option<string> _subtitle;
private string _videoDecoder;
private FFmpegProfileVideoFormat _videoFormat;
private Option<ChannelWatermark> _watermark;
private Option<int> _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 @@ -44,40 +30,6 @@ public class FFmpegComplexFilterBuilder
return this;
}
public FFmpegComplexFilterBuilder WithDeinterlace(bool deinterlace)
{
_deinterlace = deinterlace;
return this;
}
public FFmpegComplexFilterBuilder WithAlignedAudio(Option<TimeSpan> audioDuration)
{
_audioDuration = audioDuration;
return this;
}
public FFmpegComplexFilterBuilder WithNormalizeLoudness(bool normalizeLoudness)
{
_normalizeLoudness = normalizeLoudness;
return this;
}
public FFmpegComplexFilterBuilder WithInputCodec(Option<string> maybeCodec)
{
foreach (string codec in maybeCodec)
{
_inputCodec = codec;
}
return this;
}
public FFmpegComplexFilterBuilder WithDecoder(string decoder)
{
_videoDecoder = decoder;
return this;
}
public FFmpegComplexFilterBuilder WithInputPixelFormat(Option<string> maybePixelFormat)
{
foreach (string pixelFormat in maybePixelFormat)
@ -131,12 +83,6 @@ public class FFmpegComplexFilterBuilder @@ -131,12 +83,6 @@ public class FFmpegComplexFilterBuilder
return this;
}
public FFmpegComplexFilterBuilder WithVideoFormat(FFmpegProfileVideoFormat videoFormat)
{
_videoFormat = videoFormat;
return this;
}
public Option<FFmpegComplexFilter> Build(
bool videoOnly,
int videoInput,
@ -153,121 +99,13 @@ public class FFmpegComplexFilterBuilder @@ -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<string>();
var videoFilterQueue = new List<string>();
var watermarkPreprocess = new List<string>();
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<string> 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 @@ -277,31 +115,6 @@ public class FFmpegComplexFilterBuilder
var softwareFilterQueue = new List<string>();
if (usesSoftwareFilters)
{
if (acceleration != HardwareAccelerationKind.None && (isHardwareDecode || usesHardwareFilters))
{
Option<string> 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 @@ -314,16 +127,9 @@ public class FFmpegComplexFilterBuilder
foreach (ChannelWatermark watermark in _watermark)
{
Option<string> 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<string> 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 @@ -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 @@ -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 @@ -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 @@ -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]";

33
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -442,8 +442,37 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -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<Command> 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<Command> ResizeImage(string ffmpegPath, string inputFile, string outputFile, int height)
{

535
ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs

@ -19,12 +19,9 @@ @@ -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 @@ -32,36 +29,11 @@ internal class FFmpegProcessBuilder
{
private readonly List<string> _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<int> _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 @@ -71,162 +43,6 @@ internal class FFmpegProcessBuilder
return this;
}
public FFmpegProcessBuilder WithHardwareAcceleration(
HardwareAccelerationKind hwAccel,
Option<string> 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<TimeSpan> 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<int> 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> watermarkOptions,
Option<List<FadePoint>> maybeFadePoints,
@ -279,74 +95,12 @@ internal class FFmpegProcessBuilder @@ -279,74 +95,12 @@ internal class FFmpegProcessBuilder
return this;
}
public FFmpegProcessBuilder WithInputCodec(
Option<TimeSpan> maybeStart,
bool loop,
string videoPath,
string audioPath,
string decoder,
Option<string> codec,
Option<string> 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<string> codec,
Option<string> pixelFormat,
bool boxBlur)
{
_noAutoScale = true;
_outputFramerate = 30;
_complexFilterBuilder = _complexFilterBuilder
.WithInputCodec(codec)
.WithInputPixelFormat(pixelFormat)
.WithBoxBlur(boxBlur);
@ -356,52 +110,6 @@ internal class FFmpegProcessBuilder @@ -356,52 +110,6 @@ internal class FFmpegProcessBuilder
return this;
}
public FFmpegProcessBuilder WithConcat(string concatPlaylist)
{
_isConcat = true;
var arguments = new List<string>
{
"-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<MediaStream> 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<string>
{
"-metadata", "service_provider=\"ErsatzTV\"",
"-metadata", $"service_name=\"{channel.Name}\""
};
_arguments.AddRange(arguments);
return this;
}
public FFmpegProcessBuilder WithFormatFlags(IEnumerable<string> formatFlags)
{
_arguments.Add("-fflags");
@ -409,138 +117,6 @@ internal class FFmpegProcessBuilder @@ -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> mediaVersion,
long ptsOffset,
Option<int> maybeTimeScale,
Option<int> 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<string>
{
"-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<string>());
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<string>());
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 @@ -553,35 +129,6 @@ internal class FFmpegProcessBuilder
return this;
}
public FFmpegProcessBuilder WithAlignedAudio(Option<TimeSpan> audioDuration)
{
_complexFilterBuilder = _complexFilterBuilder.WithAlignedAudio(audioDuration);
return this;
}
public FFmpegProcessBuilder WithNormalizeLoudness(bool normalizeLoudness)
{
_complexFilterBuilder = _complexFilterBuilder.WithNormalizeLoudness(normalizeLoudness);
return this;
}
public FFmpegProcessBuilder WithVideoTrackTimeScale(Option<int> 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 @@ -597,11 +144,8 @@ internal class FFmpegProcessBuilder
MediaStream videoStream,
Option<MediaStream> maybeAudioStream,
string videoPath,
Option<string> audioPath,
FFmpegProfileVideoFormat videoFormat)
Option<string> audioPath)
{
_complexFilterBuilder = _complexFilterBuilder.WithVideoFormat(videoFormat);
int videoStreamIndex = videoStream.Index;
Option<int> maybeIndex = maybeAudioStream.Map(ms => ms.Index);
@ -615,10 +159,6 @@ internal class FFmpegProcessBuilder @@ -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 @@ -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 @@ -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 @@ -725,30 +222,4 @@ internal class FFmpegProcessBuilder
StartInfo = startInfo
};
}
private int GetFrameRateFromMediaVersion(Option<MediaVersion> 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;
}
}

31
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -40,30 +40,6 @@ public class FFmpegProcessService @@ -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<Either<BaseError, string>> GenerateSongImage(
string ffmpegPath,
string ffprobePath,
@ -130,11 +106,11 @@ public class FFmpegProcessService @@ -130,11 +106,11 @@ public class FFmpegProcessService
false,
Option<int>.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 @@ -153,8 +129,7 @@ public class FFmpegProcessService
videoStream,
None,
videoPath,
None,
playbackSettings.VideoFormat)
None)
.WithOutputFormat("apng", outputFile)
.Build();

2
ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs

@ -53,7 +53,7 @@ public interface IFFmpegProcessService @@ -53,7 +53,7 @@ public interface IFFmpegProcessService
Task<Command> ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
Command WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
Task<Command> WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
Task<Command> ResizeImage(string ffmpegPath, string inputFile, string outputFile, int height);

1
ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs

@ -11,5 +11,4 @@ public interface IFallbackMetadataProvider @@ -11,5 +11,4 @@ public interface IFallbackMetadataProvider
Option<MusicVideoMetadata> GetFallbackMetadata(MusicVideo musicVideo);
Option<OtherVideoMetadata> GetFallbackMetadata(OtherVideo otherVideo);
Option<SongMetadata> GetFallbackMetadata(Song song);
string GetSortTitle(string title);
}

8
ErsatzTV.Core/Interfaces/Metadata/Nfo/IArtistNfoReader.cs

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
using ErsatzTV.Core.Metadata.Nfo;
namespace ErsatzTV.Core.Interfaces.Metadata.Nfo;
public interface IArtistNfoReader
{
Task<Either<BaseError, ArtistNfo>> ReadFromFile(string fileName);
}

8
ErsatzTV.Core/Interfaces/Metadata/Nfo/IEpisodeNfoReader.cs

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
using ErsatzTV.Core.Metadata.Nfo;
namespace ErsatzTV.Core.Interfaces.Metadata.Nfo;
public interface IEpisodeNfoReader
{
Task<Either<BaseError, List<TvShowEpisodeNfo>>> ReadFromFile(string fileName);
}

8
ErsatzTV.Core/Interfaces/Metadata/Nfo/IMovieNfoReader.cs

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
using ErsatzTV.Core.Metadata.Nfo;
namespace ErsatzTV.Core.Interfaces.Metadata.Nfo;
public interface IMovieNfoReader
{
Task<Either<BaseError, MovieNfo>> ReadFromFile(string fileName);
}

8
ErsatzTV.Core/Interfaces/Metadata/Nfo/IMusicVideoNfoReader.cs

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
using ErsatzTV.Core.Metadata.Nfo;
namespace ErsatzTV.Core.Interfaces.Metadata.Nfo;
public interface IMusicVideoNfoReader
{
Task<Either<BaseError, MusicVideoNfo>> ReadFromFile(string fileName);
}

8
ErsatzTV.Core/Interfaces/Metadata/Nfo/IOtherVideoNfoReader.cs

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
using ErsatzTV.Core.Metadata.Nfo;
namespace ErsatzTV.Core.Interfaces.Metadata.Nfo;
public interface IOtherVideoNfoReader
{
Task<Either<BaseError, OtherVideoNfo>> ReadFromFile(string fileName);
}

8
ErsatzTV.Core/Interfaces/Metadata/Nfo/ITvShowNfoReader.cs

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
using ErsatzTV.Core.Metadata.Nfo;
namespace ErsatzTV.Core.Interfaces.Metadata.Nfo;
public interface ITvShowNfoReader
{
Task<Either<BaseError, TvShowNfo>> ReadFromFile(string fileName);
}

1
ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs

@ -13,7 +13,6 @@ public interface IMetadataRepository @@ -13,7 +13,6 @@ public interface IMetadataRepository
Task<bool> Update(Domain.Metadata metadata);
Task<bool> Add(Domain.Metadata metadata);
Task<bool> UpdateLocalStatistics(MediaItem mediaItem, MediaVersion incoming, bool updateVersion = true);
Task<bool> UpdatePlexStatistics(int mediaVersionId, MediaVersion incoming);
Task<Unit> UpdateArtworkPath(Artwork artwork);
Task<Unit> AddArtwork(Domain.Metadata metadata, Artwork artwork);
Task<Unit> RemoveArtwork(Domain.Metadata metadata, ArtworkKind artworkKind);

1
ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs

@ -18,5 +18,4 @@ public interface IMovieRepository @@ -18,5 +18,4 @@ public interface IMovieRepository
Task<bool> UpdateSortTitle(MovieMetadata movieMetadata);
Task<bool> AddDirector(MovieMetadata metadata, Director director);
Task<bool> AddWriter(MovieMetadata metadata, Writer writer);
Task<Unit> UpdatePath(int mediaFileId, string path);
}

30
ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs

@ -125,36 +125,6 @@ public class FallbackMetadataProvider : IFallbackMetadataProvider @@ -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<EpisodeMetadata> GetEpisodeMetadata(string fileName, EpisodeMetadata baseMetadata)
{
var result = new List<EpisodeMetadata>();

2
ErsatzTV.Core/Metadata/LocalFileSystem.cs

@ -94,7 +94,7 @@ public class LocalFileSystem : ILocalFileSystem @@ -94,7 +94,7 @@ public class LocalFileSystem : ILocalFileSystem
public IEnumerable<string> ListFiles(string folder, string searchPattern)
{
if (Directory.Exists(folder))
if (folder is not null && Directory.Exists(folder))
{
try
{

18
ErsatzTV.Core/Metadata/Nfo/ActorNfo.cs

@ -1,18 +0,0 @@ @@ -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; }
}

25
ErsatzTV.Core/Metadata/Nfo/ArtistNfo.cs

@ -1,25 +0,0 @@ @@ -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<string> Genres { get; set; }
[XmlElement("style")]
public List<string> Styles { get; set; }
[XmlElement("mood")]
public List<string> Moods { get; set; }
[XmlElement("biography")]
public string Biography { get; set; }
}

52
ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs

@ -1,52 +0,0 @@ @@ -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<DateTime> Premiered { get; set; }
[XmlElement("plot")]
public string Plot { get; set; }
[XmlElement("tagline")]
public string Tagline { get; set; }
[XmlElement("genre")]
public List<string> Genres { get; set; }
[XmlElement("tag")]
public List<string> Tags { get; set; }
[XmlElement("studio")]
public List<string> Studios { get; set; }
[XmlElement("actor")]
public List<ActorNfo> Actors { get; set; }
[XmlElement("credits")]
public List<string> Writers { get; set; }
[XmlElement("director")]
public List<string> Directors { get; set; }
[XmlElement("uniqueid")]
public List<UniqueIdNfo> UniqueIds { get; set; }
}

37
ErsatzTV.Core/Metadata/Nfo/MusicVideoNfo.cs

@ -1,37 +0,0 @@ @@ -1,37 +0,0 @@
using System.Xml.Serialization;
namespace ErsatzTV.Core.Metadata.Nfo;
[XmlRoot("musicvideo")]
public class MusicVideoNfo
{
[XmlElement("artist")]
public List<string> 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<DateTime> Aired { get; set; }
[XmlElement("year")]
public int Year { get; set; }
[XmlElement("genre")]
public List<string> Genres { get; set; }
[XmlElement("tag")]
public List<string> Tags { get; set; }
[XmlElement("studio")]
public List<string> Studios { get; set; }
}

52
ErsatzTV.Core/Metadata/Nfo/OtherVideoNfo.cs

@ -1,52 +0,0 @@ @@ -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<DateTime> Premiered { get; set; }
[XmlElement("plot")]
public string Plot { get; set; }
[XmlElement("tagline")]
public string Tagline { get; set; }
[XmlElement("genre")]
public List<string> Genres { get; set; }
[XmlElement("tag")]
public List<string> Tags { get; set; }
[XmlElement("studio")]
public List<string> Studios { get; set; }
[XmlElement("actor")]
public List<ActorNfo> Actors { get; set; }
[XmlElement("credits")]
public List<string> Writers { get; set; }
[XmlElement("director")]
public List<string> Directors { get; set; }
[XmlElement("uniqueid")]
public List<UniqueIdNfo> UniqueIds { get; set; }
}

40
ErsatzTV.Core/Metadata/Nfo/TvShowEpisodeNfo.cs

@ -1,40 +0,0 @@ @@ -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<DateTime> Aired { get; set; }
[XmlElement("plot")]
public string Plot { get; set; }
[XmlElement("actor")]
public List<ActorNfo> Actors { get; set; }
[XmlElement("credits")]
public List<string> Writers { get; set; }
[XmlElement("director")]
public List<string> Directors { get; set; }
[XmlElement("uniqueid")]
public List<UniqueIdNfo> UniqueIds { get; set; }
}

50
ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs

@ -1,50 +0,0 @@ @@ -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<DateTime> Premiered { get; set; }
[XmlElement("genre")]
public List<string> Genres { get; set; }
[XmlElement("tag")]
public List<string> Tags { get; set; }
[XmlElement("studio")]
public List<string> Studios { get; set; }
[XmlElement("actor")]
public List<ActorNfo> Actors { get; set; }
[XmlElement("uniqueid")]
public List<UniqueIdNfo> UniqueIds { get; set; }
}

15
ErsatzTV.Core/Metadata/Nfo/UniqueIdNfo.cs

@ -1,15 +0,0 @@ @@ -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; }
}

34
ErsatzTV.Core/Metadata/SortTitle.cs

@ -0,0 +1,34 @@ @@ -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;
}
}

3
ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs

@ -5,7 +5,6 @@ namespace ErsatzTV.Core.Scheduling; @@ -5,7 +5,6 @@ namespace ErsatzTV.Core.Scheduling;
public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator
{
private readonly Collection _collection;
private readonly IList<MediaItem> _sortedMediaItems;
public CustomOrderCollectionEnumerator(
@ -13,8 +12,6 @@ public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator @@ -13,8 +12,6 @@ public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator
IList<MediaItem> mediaItems,
CollectionEnumeratorState state)
{
_collection = collection;
// TODO: this will break if we allow shows and seasons
_sortedMediaItems = collection.CollectionItems
.OrderBy(ci => ci.CustomIndex)

25
ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs

@ -214,6 +214,31 @@ public class PipelineBuilderBaseTests @@ -214,6 +214,31 @@ public class PipelineBuilderBaseTests
"-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()
{

1
ErsatzTV.FFmpeg/CommandGenerator.cs

@ -97,7 +97,6 @@ public static class CommandGenerator @@ -97,7 +97,6 @@ public static class CommandGenerator
foreach (IPipelineStep step in sortedSteps)
{
arguments.AddRange(step.OutputOptions);
}

286
ErsatzTV.FFmpeg/Filter/ComplexFilter.cs

@ -1,50 +1,35 @@ @@ -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<AudioInputFile> _maybeAudioInputFile;
private readonly Option<SubtitleInputFile> _maybeSubtitleInputFile;
private readonly Option<IPixelFormat> _desiredPixelFormat;
private readonly PipelineContext _pipelineContext;
private readonly FilterChain _filterChain;
private readonly Option<VideoInputFile> _maybeVideoInputFile;
private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile;
private readonly FrameSize _resolution;
private readonly List<string> _outputOptions;
private readonly List<IPipelineStep> _pipelineSteps;
private readonly IList<string> _arguments;
public ComplexFilter(
FrameState currentState,
FFmpegState ffmpegState,
Option<VideoInputFile> maybeVideoInputFile,
Option<AudioInputFile> maybeAudioInputFile,
Option<WatermarkInputFile> maybeWatermarkInputFile,
Option<SubtitleInputFile> maybeSubtitleInputFile,
Option<IPixelFormat> 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<string>();
_pipelineSteps = new List<IPipelineStep>();
_arguments = Arguments();
}
@ -54,18 +39,18 @@ public class ComplexFilter : IPipelineStep @@ -54,18 +39,18 @@ public class ComplexFilter : IPipelineStep
public IList<string> InputOptions(InputFile inputFile) => Array.Empty<string>();
public IList<string> FilterOptions => _arguments;
public IList<string> OutputOptions => _outputOptions;
public IList<IPipelineStep> PipelineSteps => _pipelineSteps;
public FrameState NextState(FrameState currentState) => currentState;
// for testing
public FilterChain FilterChain => _filterChain;
private List<string> 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<string>();
@ -88,7 +73,10 @@ public class ComplexFilter : IPipelineStep @@ -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 @@ -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 @@ -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 @@ -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 @@ -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}]";
subtitleOverlayFilterComplex += $"{ProperLabel(videoLabel)}{ProperLabel(subtitleLabel)}";
subtitleOverlayFilterComplex += string.Join(
",",
_filterChain.SubtitleOverlayFilterSteps.Select(f => f.Filter)
.Filter(s => !string.IsNullOrWhiteSpace(s)));
videoLabel = "[vst]";
subtitleOverlayFilterComplex += videoLabel;
}
// normalize pixel format and color params
string filter = string.Empty;
// 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;
}
if (!videoStream.ColorParams.IsBt709)
{
_logger.LogDebug("Adding colorspace filter");
var colorspace = new ColorspaceFilter(_currentState, videoStream, pixelFormat);
_pipelineSteps.Add(colorspace);
filter = colorspace.Filter;
}
// 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 @@ -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 @@ -393,4 +247,6 @@ public class ComplexFilter : IPipelineStep
return result;
}
private string ProperLabel(string label) => label.StartsWith("[") ? label : $"[{label}]";
}

252
ErsatzTV.FFmpeg/Filter/NewComplexFilter.cs

@ -1,252 +0,0 @@ @@ -1,252 +0,0 @@
using ErsatzTV.FFmpeg.Environment;
using ErsatzTV.FFmpeg.Pipeline;
namespace ErsatzTV.FFmpeg.Filter;
public class NewComplexFilter : IPipelineStep
{
private readonly Option<AudioInputFile> _maybeAudioInputFile;
private readonly Option<SubtitleInputFile> _maybeSubtitleInputFile;
private readonly PipelineContext _pipelineContext;
private readonly FilterChain _filterChain;
private readonly Option<VideoInputFile> _maybeVideoInputFile;
private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile;
private readonly List<string> _outputOptions;
private readonly IList<string> _arguments;
public NewComplexFilter(
Option<VideoInputFile> maybeVideoInputFile,
Option<AudioInputFile> maybeAudioInputFile,
Option<WatermarkInputFile> maybeWatermarkInputFile,
Option<SubtitleInputFile> maybeSubtitleInputFile,
PipelineContext pipelineContext,
FilterChain filterChain)
{
_maybeVideoInputFile = maybeVideoInputFile;
_maybeAudioInputFile = maybeAudioInputFile;
_maybeWatermarkInputFile = maybeWatermarkInputFile;
_maybeSubtitleInputFile = maybeSubtitleInputFile;
_pipelineContext = pipelineContext;
_filterChain = filterChain;
_outputOptions = new List<string>();
_arguments = Arguments();
}
public IList<EnvironmentVariable> EnvironmentVariables => Array.Empty<EnvironmentVariable>();
public IList<string> GlobalOptions => Array.Empty<string>();
public IList<string> InputOptions(InputFile inputFile) => Array.Empty<string>();
public IList<string> FilterOptions => _arguments;
public IList<string> OutputOptions => _outputOptions;
public FrameState NextState(FrameState currentState) => currentState;
// for testing
public FilterChain FilterChain => _filterChain;
private List<string> Arguments()
{
var audioLabel = "0:a";
var videoLabel = "0:v";
string? watermarkLabel = null;
string? subtitleLabel = null;
var result = new List<string>();
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<string>();
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}]";
}

6
ErsatzTV.FFmpeg/Option/MapAllStreamsOutputOption.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
namespace ErsatzTV.FFmpeg.Option;
public class MapAllStreamsOutputOption : OutputOption
{
public override IList<string> OutputOptions => new[] { "-map", "0" };
}

13
ErsatzTV.FFmpeg/OutputFormat/OutputFormatMpegTs.cs

@ -4,14 +4,21 @@ namespace ErsatzTV.FFmpeg.OutputFormat; @@ -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<EnvironmentVariable> EnvironmentVariables => Array.Empty<EnvironmentVariable>();
public IList<string> GlobalOptions => Array.Empty<string>();
public IList<string> InputOptions(InputFile inputFile) => Array.Empty<string>();
public IList<string> FilterOptions => Array.Empty<string>();
// always force an initial discontinuity
public IList<string> OutputOptions =>
new List<string> { "-f", "mpegts", "-mpegts_flags", "+initial_discontinuity" };
public IList<string> OutputOptions => _initialDiscontinuity
? new List<string> { "-f", "mpegts", "-mpegts_flags", "+initial_discontinuity" }
: new List<string> { "-f", "mpegts" };
public FrameState NextState(FrameState currentState) => currentState;
}

1
ErsatzTV.FFmpeg/Pipeline/IPipelineBuilder.cs

@ -4,5 +4,6 @@ public interface IPipelineBuilder @@ -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);
}

31
ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs

@ -110,6 +110,31 @@ public abstract class PipelineBuilderBase : IPipelineBuilder @@ -110,6 +110,31 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
return new FFmpegPipeline(pipelineSteps);
}
public FFmpegPipeline WrapSegmenter(ConcatInputFile concatInputFile, FFmpegState ffmpegState)
{
var pipelineSteps = new List<IPipelineStep>
{
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<IPipelineStep>
@ -180,7 +205,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder @@ -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 @@ -193,7 +218,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
return new FFmpegPipeline(pipelineSteps);
}
protected Option<IDecoder> LogUnknownDecoder(
private Option<IDecoder> LogUnknownDecoder(
HardwareAccelerationMode hardwareAccelerationMode,
string videoFormat,
string pixelFormat)
@ -206,7 +231,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder @@ -206,7 +231,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
return Option<IDecoder>.None;
}
protected Option<IEncoder> LogUnknownEncoder(HardwareAccelerationMode hardwareAccelerationMode, string videoFormat)
private Option<IEncoder> LogUnknownEncoder(HardwareAccelerationMode hardwareAccelerationMode, string videoFormat)
{
_logger.LogWarning(
"Unable to determine video encoder for {AccelMode} - {VideoFormat}; may have playback issues",

14
ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs

@ -239,20 +239,6 @@ public class MetadataRepository : IMetadataRepository @@ -239,20 +239,6 @@ public class MetadataRepository : IMetadataRepository
() => Task.FromResult(false));
}
public async Task<bool> 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<Unit> UpdateArtworkPath(Artwork artwork)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();

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

@ -219,14 +219,6 @@ public class MovieRepository : IMovieRepository @@ -219,14 +219,6 @@ public class MovieRepository : IMovieRepository
new { writer.Name, MetadataId = metadata.Id }).Map(result => result > 0);
}
public async Task<Unit> 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<Either<BaseError, MediaItemScanResult<Movie>>> AddMovie(
TvContext dbContext,
int libraryPathId,

13
ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs

@ -2,7 +2,7 @@ @@ -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; @@ -13,17 +13,14 @@ namespace ErsatzTV.Infrastructure.Emby;
public class EmbyApiClient : IEmbyApiClient
{
private readonly IEmbyPathReplacementService _embyPathReplacementService;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILogger<EmbyApiClient> _logger;
private readonly IMemoryCache _memoryCache;
public EmbyApiClient(
IFallbackMetadataProvider fallbackMetadataProvider,
IMemoryCache memoryCache,
IEmbyPathReplacementService embyPathReplacementService,
ILogger<EmbyApiClient> logger)
{
_fallbackMetadataProvider = fallbackMetadataProvider;
_memoryCache = memoryCache;
_embyPathReplacementService = embyPathReplacementService;
_logger = logger;
@ -375,7 +372,7 @@ public class EmbyApiClient : IEmbyApiClient @@ -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 @@ -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 @@ -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<Artwork>(),
@ -691,7 +688,7 @@ public class EmbyApiClient : IEmbyApiClient @@ -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,

12
ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs

@ -3,6 +3,7 @@ using ErsatzTV.Core.Domain; @@ -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; @@ -12,19 +13,16 @@ namespace ErsatzTV.Infrastructure.Jellyfin;
public class JellyfinApiClient : IJellyfinApiClient
{
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
private readonly ILogger<JellyfinApiClient> _logger;
private readonly IMemoryCache _memoryCache;
public JellyfinApiClient(
IMemoryCache memoryCache,
IFallbackMetadataProvider fallbackMetadataProvider,
IJellyfinPathReplacementService jellyfinPathReplacementService,
ILogger<JellyfinApiClient> logger)
{
_memoryCache = memoryCache;
_fallbackMetadataProvider = fallbackMetadataProvider;
_jellyfinPathReplacementService = jellyfinPathReplacementService;
_logger = logger;
}
@ -433,7 +431,7 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -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 @@ -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 @@ -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<Artwork>(),
@ -773,7 +771,7 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -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,

16
ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs

@ -3,6 +3,7 @@ using ErsatzTV.Core; @@ -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; @@ -12,16 +13,11 @@ namespace ErsatzTV.Infrastructure.Plex;
public class PlexServerApiClient : IPlexServerApiClient
{
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILogger<PlexServerApiClient> _logger;
private readonly PlexEtag _plexEtag;
public PlexServerApiClient(
IFallbackMetadataProvider fallbackMetadataProvider,
PlexEtag plexEtag,
ILogger<PlexServerApiClient> logger)
public PlexServerApiClient(PlexEtag plexEtag, ILogger<PlexServerApiClient> logger)
{
_fallbackMetadataProvider = fallbackMetadataProvider;
_plexEtag = plexEtag;
_logger = logger;
}
@ -454,7 +450,7 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -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 @@ -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 @@ -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 @@ -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,

14
ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs → ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs

@ -3,6 +3,7 @@ using System.Security.Cryptography; @@ -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; @@ -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; @@ -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 @@ -316,13 +318,13 @@ public class TranscodingTests
.Filter(s => s.MediaStreamKind == MediaStreamKind.Subtitle)
.ToList();
var subtitles = new List<Domain.Subtitle>();
var subtitles = new List<ErsatzTV.Core.Domain.Subtitle>();
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 @@ -427,7 +429,7 @@ public class TranscodingTests
{
// validate pipeline matches expectations (at a high level)
NewComplexFilter complexFilter = pipeline.PipelineSteps.OfType<NewComplexFilter>().First();
ComplexFilter complexFilter = pipeline.PipelineSteps.OfType<ComplexFilter>().First();
FilterChain filterChain = complexFilter.FilterChain;
if (profileBitDepth == FFmpegProfileBitDepth.TenBit)
@ -800,8 +802,8 @@ public class TranscodingTests @@ -800,8 +802,8 @@ public class TranscodingTests
string preferredAudioTitle) =>
Optional(version.MediaVersion.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Audio)).AsTask();
public Task<Option<Domain.Subtitle>> SelectSubtitleStream(
List<Domain.Subtitle> subtitles,
public Task<Option<ErsatzTV.Core.Domain.Subtitle>> SelectSubtitleStream(
List<ErsatzTV.Core.Domain.Subtitle> subtitles,
Channel channel,
string preferredSubtitleLanguage,
ChannelSubtitleMode subtitleMode) =>

18
ErsatzTV.Core.Tests/Metadata/LocalStatisticsProviderTests.cs → ErsatzTV.Scanner.Tests/Core/Metadata/LocalStatisticsProviderTests.cs

@ -2,13 +2,13 @@ @@ -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 @@ -25,7 +25,19 @@ public class LocalStatisticsProviderTests
new Mock<ILogger<LocalStatisticsProvider>>().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<LocalStatisticsProvider.FFprobeStream>(),
new List<LocalStatisticsProvider.FFprobeChapter>());

6
ErsatzTV.Core.Tests/Metadata/LocalSubtitlesProviderTests.cs → ErsatzTV.Scanner.Tests/Core/Metadata/LocalSubtitlesProviderTests.cs

@ -1,14 +1,14 @@ @@ -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

1
ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs

@ -8,6 +8,7 @@ using ErsatzTV.Core.Interfaces.Metadata; @@ -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;

5
ErsatzTV.Core.Tests/Metadata/Nfo/ArtistNfoReaderTests.cs → ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/ArtistNfoReaderTests.cs

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

53
ErsatzTV.Core.Tests/Metadata/Nfo/EpisodeNfoReaderTests.cs → ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/EpisodeNfoReaderTests.cs

@ -1,6 +1,7 @@ @@ -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; @@ -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 @@ -45,10 +46,10 @@ public class EpisodeNfoReaderTests
<episodedetails>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
Either<BaseError, List<EpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
foreach (List<EpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
}
@ -74,10 +75,10 @@ public class EpisodeNfoReaderTests @@ -74,10 +75,10 @@ public class EpisodeNfoReaderTests
<season>1</season>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
Either<BaseError, List<EpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
foreach (List<EpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(2);
list.All(nfo => nfo.ShowTitle == "show").Should().BeTrue();
@ -99,10 +100,10 @@ public class EpisodeNfoReaderTests @@ -99,10 +100,10 @@ public class EpisodeNfoReaderTests
<uniqueid default=""false"" type=""imdb"">tt54321</uniqueid>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
Either<BaseError, List<EpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
foreach (List<EpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
list[0].UniqueIds.Count.Should().Be(2);
@ -122,10 +123,10 @@ public class EpisodeNfoReaderTests @@ -122,10 +123,10 @@ public class EpisodeNfoReaderTests
<mpaa/>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
Either<BaseError, List<EpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
foreach (List<EpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
list[0].ContentRating.Should().BeNullOrEmpty();
@ -146,10 +147,10 @@ public class EpisodeNfoReaderTests @@ -146,10 +147,10 @@ public class EpisodeNfoReaderTests
<mpaa>US:Something / US:SomethingElse</mpaa>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
Either<BaseError, List<EpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
foreach (List<EpisodeNfo> 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 @@ -168,10 +169,10 @@ public class EpisodeNfoReaderTests
<plot/>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
Either<BaseError, List<EpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
foreach (List<EpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
list[0].Plot.Should().BeNullOrEmpty();
@ -189,10 +190,10 @@ public class EpisodeNfoReaderTests @@ -189,10 +190,10 @@ public class EpisodeNfoReaderTests
<plot>Some Plot</plot>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
Either<BaseError, List<EpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
foreach (List<EpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
list[0].Plot.Should().Be("Some Plot");
@ -219,10 +220,10 @@ public class EpisodeNfoReaderTests @@ -219,10 +220,10 @@ public class EpisodeNfoReaderTests
</actor>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
Either<BaseError, List<EpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
foreach (List<EpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
list[0].Actors.Count.Should().Be(2);
@ -248,10 +249,10 @@ public class EpisodeNfoReaderTests @@ -248,10 +249,10 @@ public class EpisodeNfoReaderTests
<credits>Writer 3</credits>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
Either<BaseError, List<EpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
foreach (List<EpisodeNfo> 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 @@ -275,10 +276,10 @@ public class EpisodeNfoReaderTests
<director>Director 3</director>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
Either<BaseError, List<EpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
foreach (List<EpisodeNfo> 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 @@ -379,11 +380,11 @@ public class EpisodeNfoReaderTests
<dateadded>2021-02-02 11:57:44</dateadded>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
Either<BaseError, List<EpisodeNfo>> 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 @@ -433,10 +434,10 @@ public class EpisodeNfoReaderTests
"Resources",
"Nfo",
"EpisodeInvalidCharacters.nfo");
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.ReadFromFile(sourceFile);
Either<BaseError, List<EpisodeNfo>> result = await _episodeNfoReader.ReadFromFile(sourceFile);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
foreach (List<EpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
list[0].Title.Should().Be("Test Title");

7
ErsatzTV.Core.Tests/Metadata/Nfo/MovieNfoReaderTests.cs → ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/MovieNfoReaderTests.cs

@ -1,13 +1,14 @@ @@ -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")); @@ -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<string> { "SuperHero" });
nfo.Tags.Should().BeEquivalentTo(new List<string> { "TV Recording" });
nfo.Studios.Should().BeEquivalentTo(new List<string> { "Warner Bros. Pictures" });

5
ErsatzTV.Core.Tests/Metadata/Nfo/MusicVideoNfoReaderTests.cs → ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/MusicVideoNfoReaderTests.cs

@ -1,13 +1,14 @@ @@ -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

5
ErsatzTV.Core.Tests/Metadata/Nfo/OtherVideoNfoReaderTests.cs → ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/OtherVideoNfoReaderTests.cs

@ -1,13 +1,14 @@ @@ -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

35
ErsatzTV.Core.Tests/Metadata/Nfo/TvShowNfoReaderTests.cs → ErsatzTV.Scanner.Tests/Core/Metadata/Nfo/ShowNfoReaderTests.cs

@ -1,24 +1,25 @@ @@ -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<IClient>().Object,
new NullLogger<TvShowNfoReader>());
new NullLogger<ShowNfoReader>());
private TvShowNfoReader _tvShowNfoReader;
private ShowNfoReader _showNfoReader;
[Test]
public async Task ParsingNfo_Should_Return_Error()
@ -26,7 +27,7 @@ public class TvShowNfoReaderTests @@ -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<BaseError, TvShowNfo> result = await _tvShowNfoReader.Read(stream);
Either<BaseError, ShowNfo> result = await _showNfoReader.Read(stream);
result.IsLeft.Should().BeTrue();
}
@ -36,7 +37,7 @@ public class TvShowNfoReaderTests @@ -36,7 +37,7 @@ public class TvShowNfoReaderTests
{
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"<tvshow></tvshow>"));
Either<BaseError, TvShowNfo> result = await _tvShowNfoReader.Read(stream);
Either<BaseError, ShowNfo> result = await _showNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
}
@ -49,7 +50,7 @@ public class TvShowNfoReaderTests @@ -49,7 +50,7 @@ public class TvShowNfoReaderTests
@"<tvshow></tvshow>
https://www.themoviedb.org/movie/11-star-wars"));
Either<BaseError, TvShowNfo> result = await _tvShowNfoReader.Read(stream);
Either<BaseError, ShowNfo> result = await _showNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
}
@ -133,11 +134,11 @@ https://www.themoviedb.org/movie/11-star-wars")); @@ -133,11 +134,11 @@ https://www.themoviedb.org/movie/11-star-wars"));
<dateadded>2021-03-12 06:15:51</dateadded>
</tvshow>"));
Either<BaseError, TvShowNfo> result = await _tvShowNfoReader.Read(stream);
Either<BaseError, ShowNfo> 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")); @@ -181,10 +182,10 @@ https://www.themoviedb.org/movie/11-star-wars"));
await using var stream =
new MemoryStream(Encoding.UTF8.GetBytes(@"<tvshow><outline>Test Outline</outline></tvshow>"));
Either<BaseError, TvShowNfo> result = await _tvShowNfoReader.Read(stream);
Either<BaseError, ShowNfo> 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")); @@ -196,10 +197,10 @@ https://www.themoviedb.org/movie/11-star-wars"));
await using var stream =
new MemoryStream(Encoding.UTF8.GetBytes(@"<tvshow><tagline>Test Tagline</tagline></tvshow>"));
Either<BaseError, TvShowNfo> result = await _tvShowNfoReader.Read(stream);
Either<BaseError, ShowNfo> 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")); @@ -210,10 +211,10 @@ https://www.themoviedb.org/movie/11-star-wars"));
{
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"<tvshow><tag>Test Tag</tag></tvshow>"));
Either<BaseError, TvShowNfo> result = await _tvShowNfoReader.Read(stream);
Either<BaseError, ShowNfo> 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<string> { "Test Tag" });
}

12
ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj

@ -23,4 +23,16 @@ @@ -23,4 +23,16 @@
<ProjectReference Include="..\ErsatzTV.Scanner\ErsatzTV.Scanner.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="Resources\Nfo\ArtistInvalidCharacters1.nfo">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Resources\Nfo\ArtistInvalidCharacters2.nfo">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Resources\Nfo\EpisodeInvalidCharacters.nfo">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

0
ErsatzTV.Core.Tests/Resources/Nfo/ArtistInvalidCharacters1.nfo → ErsatzTV.Scanner.Tests/Resources/Nfo/ArtistInvalidCharacters1.nfo

0
ErsatzTV.Core.Tests/Resources/Nfo/ArtistInvalidCharacters2.nfo → ErsatzTV.Scanner.Tests/Resources/Nfo/ArtistInvalidCharacters2.nfo

0
ErsatzTV.Core.Tests/Resources/Nfo/EpisodeInvalidCharacters.nfo → ErsatzTV.Scanner.Tests/Resources/Nfo/EpisodeInvalidCharacters.nfo

2
ErsatzTV.Scanner/Application/MediaSources/Commands/ScanLocalLibraryHandler.cs

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

4
ErsatzTV.Scanner/Core/Emby/EmbyCollectionScanner.cs

@ -55,10 +55,6 @@ public class EmbyCollectionScanner : IEmbyCollectionScanner @@ -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);

2
ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs

@ -6,6 +6,8 @@ using ErsatzTV.Core.Interfaces.Emby; @@ -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;

2
ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs

@ -6,6 +6,8 @@ using ErsatzTV.Core.Interfaces.Emby; @@ -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;

2
ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs → ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalMetadataProvider.cs

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Interfaces.Metadata;
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata;
public interface ILocalMetadataProvider
{

5
ErsatzTV.Core/Interfaces/Metadata/ILocalStatisticsProvider.cs → ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalStatisticsProvider.cs

@ -1,6 +1,7 @@ @@ -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
{

2
ErsatzTV.Core/Interfaces/Metadata/ILocalSubtitlesProvider.cs → ErsatzTV.Scanner/Core/Interfaces/Metadata/ILocalSubtitlesProvider.cs

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Interfaces.Metadata;
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata;
public interface ILocalSubtitlesProvider
{

5
ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs → ErsatzTV.Scanner/Core/Interfaces/Metadata/IMovieFolderScanner.cs

@ -1,6 +1,7 @@ @@ -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
{

5
ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs → ErsatzTV.Scanner/Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs

@ -1,6 +1,7 @@ @@ -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
{

5
ErsatzTV.Core/Interfaces/Metadata/IOtherVideoFolderScanner.cs → ErsatzTV.Scanner/Core/Interfaces/Metadata/IOtherVideoFolderScanner.cs

@ -1,6 +1,7 @@ @@ -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
{

5
ErsatzTV.Core/Interfaces/Metadata/ISongFolderScanner.cs → ErsatzTV.Scanner/Core/Interfaces/Metadata/ISongFolderScanner.cs

@ -1,6 +1,7 @@ @@ -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
{

5
ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs → ErsatzTV.Scanner/Core/Interfaces/Metadata/ITelevisionFolderScanner.cs

@ -1,6 +1,7 @@ @@ -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
{

9
ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IArtistNfoReader.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
using ErsatzTV.Core;
using ErsatzTV.Scanner.Core.Metadata.Nfo;
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo;
public interface IArtistNfoReader
{
Task<Either<BaseError, ArtistNfo>> ReadFromFile(string fileName);
}

9
ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IEpisodeNfoReader.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
using ErsatzTV.Core;
using ErsatzTV.Scanner.Core.Metadata.Nfo;
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo;
public interface IEpisodeNfoReader
{
Task<Either<BaseError, List<EpisodeNfo>>> ReadFromFile(string fileName);
}

9
ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IMovieNfoReader.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
using ErsatzTV.Core;
using ErsatzTV.Scanner.Core.Metadata.Nfo;
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo;
public interface IMovieNfoReader
{
Task<Either<BaseError, MovieNfo>> ReadFromFile(string fileName);
}

9
ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IMusicVideoNfoReader.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
using ErsatzTV.Core;
using ErsatzTV.Scanner.Core.Metadata.Nfo;
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo;
public interface IMusicVideoNfoReader
{
Task<Either<BaseError, MusicVideoNfo>> ReadFromFile(string fileName);
}

9
ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IOtherVideoNfoReader.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
using ErsatzTV.Core;
using ErsatzTV.Scanner.Core.Metadata.Nfo;
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo;
public interface IOtherVideoNfoReader
{
Task<Either<BaseError, OtherVideoNfo>> ReadFromFile(string fileName);
}

9
ErsatzTV.Scanner/Core/Interfaces/Metadata/Nfo/IShowNfoReader.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
using ErsatzTV.Core;
using ErsatzTV.Scanner.Core.Metadata.Nfo;
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo;
public interface IShowNfoReader
{
Task<Either<BaseError, ShowNfo>> ReadFromFile(string fileName);
}

4
ErsatzTV.Scanner/Core/Jellyfin/JellyfinCollectionScanner.cs

@ -58,10 +58,6 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner @@ -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);

2
ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs

@ -6,6 +6,8 @@ using ErsatzTV.Core.Interfaces.Metadata; @@ -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;

2
ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs

@ -6,6 +6,8 @@ using ErsatzTV.Core.Interfaces.Metadata; @@ -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;

2
ErsatzTV.Core/Metadata/FolderEtag.cs → ErsatzTV.Scanner/Core/Metadata/FolderEtag.cs

@ -2,7 +2,7 @@ @@ -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
{

1
ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs

@ -11,6 +11,7 @@ using ErsatzTV.Core.Interfaces.Metadata; @@ -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;

79
ErsatzTV.Core/Metadata/LocalMetadataProvider.cs → ErsatzTV.Scanner/Core/Metadata/LocalMetadataProvider.cs

@ -1,13 +1,16 @@ @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -297,37 +300,37 @@ public class LocalMetadataProvider : ILocalMetadataProvider
Tags = new List<Tag>()
};
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 @@ -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 @@ -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 @@ -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 @@ -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<MovieMetadata> { metadata };
@ -620,7 +623,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -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 @@ -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<ShowMetadata> { metadata };
@ -680,7 +683,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -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 @@ -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<ArtistMetadata> { metadata };
@ -778,7 +781,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -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 @@ -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<MusicVideoMetadata> { metadata };
@ -845,7 +848,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -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 @@ -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<OtherVideoMetadata> { metadata };
@ -949,7 +952,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -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 @@ -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<SongMetadata> { metadata };
@ -977,7 +980,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -977,7 +980,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
{
try
{
Either<BaseError, TvShowNfo> maybeNfo = await _tvShowNfoReader.ReadFromFile(nfoFileName);
Either<BaseError, ShowNfo> maybeNfo = await _showNfoReader.ReadFromFile(nfoFileName);
foreach (BaseError error in maybeNfo.LeftToSeq())
{
_logger.LogInformation(
@ -986,7 +989,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -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 @@ -1066,7 +1069,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
{
try
{
Either<BaseError, List<TvShowEpisodeNfo>> maybeNfo = await _episodeNfoReader.ReadFromFile(nfoFileName);
Either<BaseError, List<EpisodeNfo>> maybeNfo = await _episodeNfoReader.ReadFromFile(nfoFileName);
foreach (BaseError error in maybeNfo.LeftToSeq())
{
_logger.LogInformation(
@ -1076,7 +1079,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider @@ -1076,7 +1079,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
}
var result = new List<EpisodeMetadata>();
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 @@ -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 @@ -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 @@ -1300,7 +1303,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
Func<T, Tag, Task<bool>> addTag,
Func<T, Studio, Task<bool>> addStudio,
Func<T, Actor, Task<bool>> addActor)
where T : Domain.Metadata
where T : ErsatzTV.Core.Domain.Metadata
{
var updated = false;

93
ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs → ErsatzTV.Scanner/Core/Metadata/LocalStatisticsProvider.cs

@ -5,14 +5,16 @@ using System.Text.RegularExpressions; @@ -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 @@ -237,9 +239,8 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
return BaseError.New($"FFprobe at {ffprobePath} exited with code {probe.ExitCode}");
}
FFprobe ffprobe = JsonConvert.DeserializeObject<FFprobe>(probe.StandardOutput);
if (ffprobe != null)
FFprobe? ffprobe = JsonConvert.DeserializeObject<FFprobe>(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 @@ -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 @@ -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<BaseError>("Unable to parse ffprobe output")
.ToEither<FFprobe>()
.Match(
@ -337,7 +340,7 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider @@ -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 @@ -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 @@ -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 @@ -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 @@ -516,60 +519,60 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
Chapters = new List<MediaChapter>()
});
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<FFprobeStream> streams, List<FFprobeChapter> chapters);
public record FFprobe(FFprobeFormat? format, List<FFprobeStream>? streams, List<FFprobeChapter>? 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);
}

17
ErsatzTV.Core/Metadata/LocalSubtitlesProvider.cs → ErsatzTV.Scanner/Core/Metadata/LocalSubtitlesProvider.cs

@ -3,9 +3,10 @@ using ErsatzTV.Core.Domain; @@ -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 @@ -50,12 +51,12 @@ public class LocalSubtitlesProvider : ILocalSubtitlesProvider
return false;
}
Option<Domain.Metadata> maybeMetadata = mediaItem switch
Option<ErsatzTV.Core.Domain.Metadata> maybeMetadata = mediaItem switch
{
Episode e => e.EpisodeMetadata.OfType<Domain.Metadata>().HeadOrNone(),
Movie m => m.MovieMetadata.OfType<Domain.Metadata>().HeadOrNone(),
MusicVideo mv => mv.MusicVideoMetadata.OfType<Domain.Metadata>().HeadOrNone(),
OtherVideo ov => ov.OtherVideoMetadata.OfType<Domain.Metadata>().HeadOrNone(),
Episode e => e.EpisodeMetadata.OfType<ErsatzTV.Core.Domain.Metadata>().HeadOrNone(),
Movie m => m.MovieMetadata.OfType<ErsatzTV.Core.Domain.Metadata>().HeadOrNone(),
MusicVideo mv => mv.MusicVideoMetadata.OfType<ErsatzTV.Core.Domain.Metadata>().HeadOrNone(),
OtherVideo ov => ov.OtherVideoMetadata.OfType<ErsatzTV.Core.Domain.Metadata>().HeadOrNone(),
_ => None
};
@ -67,7 +68,7 @@ public class LocalSubtitlesProvider : ILocalSubtitlesProvider @@ -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 @@ -96,7 +97,7 @@ public class LocalSubtitlesProvider : ILocalSubtitlesProvider
{
var result = new List<Subtitle>();
string folder = Path.GetDirectoryName(mediaItemPath);
string? folder = Path.GetDirectoryName(mediaItemPath);
string withoutExtension = Path.GetFileNameWithoutExtension(mediaItemPath);
foreach (string file in _localFileSystem.ListFiles(folder, $"{withoutExtension}*"))
{

8
ErsatzTV.Core/Metadata/MediaServerMovieLibraryScanner.cs → ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs

@ -1,14 +1,16 @@ @@ -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<TConnectionParameters, TLibrary, TMovie, TEtag>
where TConnectionParameters : MediaServerConnectionParameters
@ -236,7 +238,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib @@ -236,7 +238,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
string existingEtag = string.Empty;
MediaItemState existingState = MediaItemState.Normal;
if (existingMovies.TryGetValue(MediaServerItemId(incoming), out TEtag existingEntry))
if (existingMovies.TryGetValue(MediaServerItemId(incoming), out TEtag? existingEntry))
{
existingEtag = existingEntry.Etag;
existingState = existingEntry.State;

8
ErsatzTV.Core/Metadata/MediaServerTelevisionLibraryScanner.cs → ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs

@ -1,13 +1,15 @@ @@ -1,13 +1,15 @@
using ErsatzTV.Core.Domain;
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 MediaServerTelevisionLibraryScanner<TConnectionParameters, TLibrary, TShow, TSeason, TEpisode,
TEtag>

2
ErsatzTV.Core/Metadata/MetadataSongTag.cs → ErsatzTV.Scanner/Core/Metadata/MetadataSongTag.cs

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
namespace ErsatzTV.Core.Metadata;
namespace ErsatzTV.Scanner.Core.Metadata;
public static class MetadataSongTag
{

1
ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs

@ -10,6 +10,7 @@ using ErsatzTV.Core.Interfaces.Repositories; @@ -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;

1
ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs

@ -9,6 +9,7 @@ using ErsatzTV.Core.Interfaces.Repositories; @@ -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;

9
ErsatzTV.Scanner/Core/Metadata/Nfo/ActorNfo.cs

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

18
ErsatzTV.Scanner/Core/Metadata/Nfo/ArtistNfo.cs

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
namespace ErsatzTV.Scanner.Core.Metadata.Nfo;
public class ArtistNfo
{
public ArtistNfo()
{
Genres = new List<string>();
Styles = new List<string>();
Moods = new List<string>();
}
public string? Name { get; set; }
public string? Disambiguation { get; set; }
public List<string> Genres { get; }
public List<string> Styles { get; }
public List<string> Moods { get; }
public string? Biography { get; set; }
}

14
ErsatzTV.Core/Metadata/Nfo/ArtistNfoReader.cs → ErsatzTV.Scanner/Core/Metadata/Nfo/ArtistNfoReader.cs

@ -1,11 +1,12 @@ @@ -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<ArtistNfo>, IArtistNfoReader
{
@ -33,7 +34,7 @@ public class ArtistNfoReader : NfoReader<ArtistNfo>, IArtistNfoReader @@ -33,7 +34,7 @@ public class ArtistNfoReader : NfoReader<ArtistNfo>, IArtistNfoReader
internal async Task<Either<BaseError, ArtistNfo>> Read(Stream input)
{
ArtistNfo nfo = null;
ArtistNfo? nfo = null;
try
{
@ -49,12 +50,7 @@ public class ArtistNfoReader : NfoReader<ArtistNfo>, IArtistNfoReader @@ -49,12 +50,7 @@ public class ArtistNfoReader : NfoReader<ArtistNfo>, IArtistNfoReader
switch (reader.Name.ToLowerInvariant())
{
case "artist":
nfo = new ArtistNfo
{
Genres = new List<string>(),
Styles = new List<string>(),
Moods = new List<string>()
};
nfo = new ArtistNfo();
break;
case "name":
await ReadStringContent(reader, nfo, (artist, name) => artist.Name = name);

24
ErsatzTV.Scanner/Core/Metadata/Nfo/EpisodeNfo.cs

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
namespace ErsatzTV.Scanner.Core.Metadata.Nfo;
public class EpisodeNfo
{
public EpisodeNfo()
{
Actors = new List<ActorNfo>();
Writers = new List<string>();
Directors = new List<string>();
UniqueIds = new List<UniqueIdNfo>();
}
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<DateTime> Aired { get; set; }
public string? Plot { get; set; }
public List<ActorNfo> Actors { get; }
public List<string> Writers { get; }
public List<string> Directors { get; }
public List<UniqueIdNfo> UniqueIds { get; }
}

23
ErsatzTV.Core/Metadata/Nfo/EpisodeNfoReader.cs → ErsatzTV.Scanner/Core/Metadata/Nfo/EpisodeNfoReader.cs

@ -1,13 +1,14 @@ @@ -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<TvShowEpisodeNfo>, IEpisodeNfoReader
public class EpisodeNfoReader : NfoReader<EpisodeNfo>, IEpisodeNfoReader
{
private readonly IClient _client;
private readonly ILogger<EpisodeNfoReader> _logger;
@ -22,7 +23,7 @@ public class EpisodeNfoReader : NfoReader<TvShowEpisodeNfo>, IEpisodeNfoReader @@ -22,7 +23,7 @@ public class EpisodeNfoReader : NfoReader<TvShowEpisodeNfo>, IEpisodeNfoReader
_logger = logger;
}
public async Task<Either<BaseError, List<TvShowEpisodeNfo>>> ReadFromFile(string fileName)
public async Task<Either<BaseError, List<EpisodeNfo>>> ReadFromFile(string fileName)
{
// ReSharper disable once ConvertToUsingDeclaration
await using (Stream s = await SanitizedStreamForFile(fileName))
@ -31,14 +32,14 @@ public class EpisodeNfoReader : NfoReader<TvShowEpisodeNfo>, IEpisodeNfoReader @@ -31,14 +32,14 @@ public class EpisodeNfoReader : NfoReader<TvShowEpisodeNfo>, IEpisodeNfoReader
}
}
internal async Task<Either<BaseError, List<TvShowEpisodeNfo>>> Read(Stream input)
internal async Task<Either<BaseError, List<EpisodeNfo>>> Read(Stream input)
{
var result = new List<TvShowEpisodeNfo>();
var result = new List<EpisodeNfo>();
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<TvShowEpisodeNfo>, IEpisodeNfoReader @@ -48,13 +49,7 @@ public class EpisodeNfoReader : NfoReader<TvShowEpisodeNfo>, IEpisodeNfoReader
switch (reader.Name.ToLowerInvariant())
{
case "episodedetails":
nfo = new TvShowEpisodeNfo
{
UniqueIds = new List<UniqueIdNfo>(),
Actors = new List<ActorNfo>(),
Writers = new List<string>(),
Directors = new List<string>()
};
nfo = new EpisodeNfo();
// immediately add so we have something to return if we encounter invalid characters
result.Add(nfo);
break;

31
ErsatzTV.Scanner/Core/Metadata/Nfo/MovieNfo.cs

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
namespace ErsatzTV.Scanner.Core.Metadata.Nfo;
public class MovieNfo
{
public MovieNfo()
{
Genres = new List<string>();
Tags = new List<string>();
Studios = new List<string>();
Actors = new List<ActorNfo>();
Writers = new List<string>();
Directors = new List<string>();
UniqueIds = new List<UniqueIdNfo>();
}
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<DateTime> Premiered { get; set; }
public string? Plot { get; set; }
// public string? Tagline { get; set; }
public List<string> Genres { get; }
public List<string> Tags { get; }
public List<string> Studios { get; }
public List<ActorNfo> Actors { get; }
public List<string> Writers { get; }
public List<string> Directors { get; }
public List<UniqueIdNfo> UniqueIds { get; }
}

18
ErsatzTV.Core/Metadata/Nfo/MovieNfoReader.cs → ErsatzTV.Scanner/Core/Metadata/Nfo/MovieNfoReader.cs

@ -1,11 +1,12 @@ @@ -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<MovieNfo>, IMovieNfoReader
{
@ -33,7 +34,7 @@ public class MovieNfoReader : NfoReader<MovieNfo>, IMovieNfoReader @@ -33,7 +34,7 @@ public class MovieNfoReader : NfoReader<MovieNfo>, IMovieNfoReader
internal async Task<Either<BaseError, MovieNfo>> Read(Stream input)
{
MovieNfo nfo = null;
MovieNfo? nfo = null;
try
{
@ -49,16 +50,7 @@ public class MovieNfoReader : NfoReader<MovieNfo>, IMovieNfoReader @@ -49,16 +50,7 @@ public class MovieNfoReader : NfoReader<MovieNfo>, IMovieNfoReader
switch (reader.Name.ToLowerInvariant())
{
case "movie":
nfo = new MovieNfo
{
Genres = new List<string>(),
Tags = new List<string>(),
Studios = new List<string>(),
Actors = new List<ActorNfo>(),
Writers = new List<string>(),
Directors = new List<string>(),
UniqueIds = new List<UniqueIdNfo>()
};
nfo = new MovieNfo();
break;
case "title":
await ReadStringContent(reader, nfo, (movie, title) => movie.Title = title);

23
ErsatzTV.Scanner/Core/Metadata/Nfo/MusicVideoNfo.cs

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
namespace ErsatzTV.Scanner.Core.Metadata.Nfo;
public class MusicVideoNfo
{
public MusicVideoNfo()
{
Artists = new List<string>();
Genres = new List<string>();
Tags = new List<string>();
Studios = new List<string>();
}
public List<string> Artists { get; }
public string? Title { get; set; }
public string? Album { get; set; }
public string? Plot { get; set; }
public int Track { get; set; }
public Option<DateTime> Aired { get; set; }
public int Year { get; set; }
public List<string> Genres { get; }
public List<string> Tags { get; }
public List<string> Studios { get; }
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save