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
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace ErsatzTV.Application.MediaSources;
|
||||
|
||||
public record LocalMediaSourceViewModel(int Id) : MediaSourceViewModel(Id, "Local");
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
@@ -9,29 +8,16 @@ namespace ErsatzTV.Core.FFmpeg;
|
||||
|
||||
public class FFmpegComplexFilterBuilder
|
||||
{
|
||||
private Option<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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
watermarkPreprocess.AddRange(fadePoints.Map(fp => fp.ToFilter()));
|
||||
}
|
||||
|
||||
if (acceleration == HardwareAccelerationKind.Nvenc)
|
||||
{
|
||||
watermarkPreprocess.Add("hwupload_cuda");
|
||||
}
|
||||
watermarkOverlay = $"overlay={position}";
|
||||
|
||||
watermarkOverlay = acceleration switch
|
||||
if (hasFadePoints)
|
||||
{
|
||||
HardwareAccelerationKind.Nvenc => $"overlay_cuda={position}",
|
||||
_ => $"overlay={position}"
|
||||
};
|
||||
|
||||
if (hasFadePoints && acceleration != HardwareAccelerationKind.Nvenc)
|
||||
{
|
||||
watermarkOverlay += "," + acceleration switch
|
||||
watermarkOverlay += "," + isSong switch
|
||||
{
|
||||
HardwareAccelerationKind.Vaapi => "format=nv12|vaapi",
|
||||
_ when isSong => "format=yuv420p",
|
||||
_ => "format=nv12"
|
||||
true => "format=yuv420p",
|
||||
false => "format=nv12"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string outputPixelFormat = null;
|
||||
if (!usesSoftwareFilters && string.IsNullOrWhiteSpace(watermarkOverlay))
|
||||
{
|
||||
switch (acceleration, _videoFormat, _pixelFormat)
|
||||
{
|
||||
case (HardwareAccelerationKind.Nvenc, FFmpegProfileVideoFormat.H264, "yuv420p10le"):
|
||||
outputPixelFormat = "yuv420p";
|
||||
break;
|
||||
case (HardwareAccelerationKind.Nvenc, FFmpegProfileVideoFormat.H264, "yuv444p10le"):
|
||||
outputPixelFormat = "yuv444p";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
string outputFormat = (acceleration, _videoFormat, _pixelFormat) switch
|
||||
{
|
||||
(HardwareAccelerationKind.Nvenc, FFmpegProfileVideoFormat.Hevc, "yuv420p10le") => "p010le",
|
||||
(HardwareAccelerationKind.Nvenc, FFmpegProfileVideoFormat.H264, "yuv420p10le") => "p010le",
|
||||
_ => null
|
||||
};
|
||||
|
||||
_scaleToSize.IfSome(
|
||||
size =>
|
||||
{
|
||||
string filter = acceleration switch
|
||||
string filter = videoOnly switch
|
||||
{
|
||||
HardwareAccelerationKind.Qsv => $"scale_qsv=w={size.Width}:h={size.Height}",
|
||||
HardwareAccelerationKind.Nvenc when _watermark.IsSome && _scaleToSize.IsNone =>
|
||||
$"format=yuv420p,hwupload_cuda,scale_cuda={size.Width}:{size.Height}",
|
||||
HardwareAccelerationKind.Nvenc when _watermark.IsSome && _padToSize.IsNone =>
|
||||
$"scale_cuda={size.Width}:{size.Height}",
|
||||
HardwareAccelerationKind.Nvenc when _watermark.IsNone && !string.IsNullOrEmpty(outputFormat) =>
|
||||
$"scale_cuda={size.Width}:{size.Height}:format={outputFormat}",
|
||||
HardwareAccelerationKind.Nvenc when _pixelFormat is "yuv420p10le" && usesHardwareFilters == false =>
|
||||
$"hwupload_cuda,scale_cuda={size.Width}:{size.Height}",
|
||||
HardwareAccelerationKind.Nvenc => $"scale_cuda={size.Width}:{size.Height}",
|
||||
HardwareAccelerationKind.Vaapi => $"scale_vaapi=format=nv12:w={size.Width}:h={size.Height}",
|
||||
_ when videoOnly =>
|
||||
true =>
|
||||
$"scale={size.Width}:{size.Height}:force_original_aspect_ratio=increase,crop={size.Width}:{size.Height}",
|
||||
_ => $"scale={size.Width}:{size.Height}:flags=fast_bilinear"
|
||||
false => $"scale={size.Width}:{size.Height}:flags=fast_bilinear"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter))
|
||||
@@ -435,14 +201,6 @@ public class FFmpegComplexFilterBuilder
|
||||
|
||||
if (scaleOrPad && _boxBlur == false)
|
||||
{
|
||||
if (acceleration == HardwareAccelerationKind.Nvenc)
|
||||
{
|
||||
if (!isHardwareDecode && !string.IsNullOrWhiteSpace(outputPixelFormat))
|
||||
{
|
||||
videoFilterQueue.Add($"hwdownload,format={outputPixelFormat}");
|
||||
}
|
||||
}
|
||||
|
||||
videoFilterQueue.Add("setsar=1");
|
||||
}
|
||||
|
||||
@@ -450,63 +208,13 @@ public class FFmpegComplexFilterBuilder
|
||||
|
||||
_padToSize.IfSome(size => videoFilterQueue.Add($"pad={size.Width}:{size.Height}:(ow-iw)/2:(oh-ih)/2"));
|
||||
|
||||
if (acceleration == HardwareAccelerationKind.Nvenc && _watermark.IsSome)
|
||||
{
|
||||
if (_scaleToSize.IsSome)
|
||||
{
|
||||
videoFilterQueue.Add("hwdownload,format=nv12,format=yuv420p");
|
||||
videoFilterQueue.Add("hwupload_cuda");
|
||||
}
|
||||
else if (_padToSize.IsNone)
|
||||
{
|
||||
videoFilterQueue.Add("scale_cuda=format=yuv420p");
|
||||
}
|
||||
else
|
||||
{
|
||||
videoFilterQueue.Add("format=yuv420p");
|
||||
videoFilterQueue.Add("hwupload_cuda");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string subtitle in _subtitle)
|
||||
{
|
||||
videoFilterQueue.Add(subtitle);
|
||||
}
|
||||
|
||||
if (usesSoftwareFilters && acceleration != HardwareAccelerationKind.None &&
|
||||
string.IsNullOrWhiteSpace(watermarkOverlay))
|
||||
{
|
||||
string upload = acceleration switch
|
||||
{
|
||||
HardwareAccelerationKind.Qsv => "hwupload=extra_hw_frames=64",
|
||||
_ => "hwupload"
|
||||
};
|
||||
videoFilterQueue.Add(upload);
|
||||
}
|
||||
|
||||
bool hasAudioFilters = audioFilterQueue.Any();
|
||||
if (hasAudioFilters)
|
||||
{
|
||||
complexFilter.Append($"[{audioLabel}]");
|
||||
complexFilter.Append(string.Join(",", audioFilterQueue));
|
||||
audioLabel = "[a]";
|
||||
complexFilter.Append(audioLabel);
|
||||
}
|
||||
|
||||
// vaapi downsample 10bit hevc to 8bit h264
|
||||
if (acceleration == HardwareAccelerationKind.Vaapi && !videoFilterQueue.Any() &&
|
||||
_pixelFormat == "yuv420p10le" && _videoFormat == FFmpegProfileVideoFormat.H264)
|
||||
{
|
||||
videoFilterQueue.Add("scale_vaapi=format=nv12");
|
||||
}
|
||||
|
||||
if (videoFilterQueue.Any() || !string.IsNullOrWhiteSpace(watermarkOverlay))
|
||||
{
|
||||
if (hasAudioFilters)
|
||||
{
|
||||
complexFilter.Append(';');
|
||||
}
|
||||
|
||||
if (videoFilterQueue.Any())
|
||||
{
|
||||
complexFilter.Append($"[{videoLabel}]");
|
||||
@@ -538,25 +246,6 @@ public class FFmpegComplexFilterBuilder
|
||||
videoFilterQueue.Any()
|
||||
? $"[vt]{watermarkLabel}{watermarkOverlay}"
|
||||
: $"[{videoLabel}]{watermarkLabel}{watermarkOverlay}");
|
||||
|
||||
if (usesSoftwareFilters && acceleration != HardwareAccelerationKind.None)
|
||||
{
|
||||
switch (isSong, acceleration)
|
||||
{
|
||||
case (true, HardwareAccelerationKind.Nvenc):
|
||||
complexFilter.Append(",hwupload_cuda");
|
||||
break;
|
||||
// no need to upload since we're already in the GPU with overlay_cuda
|
||||
case (_, HardwareAccelerationKind.Nvenc) when scaleOrPad == false && _watermark.IsSome:
|
||||
break;
|
||||
case (_, HardwareAccelerationKind.Qsv):
|
||||
complexFilter.Append(",format=yuv420p,hwupload=extra_hw_frames=64");
|
||||
break;
|
||||
default:
|
||||
complexFilter.Append(",hwupload");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
videoLabel = "[v]";
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -19,12 +19,9 @@
|
||||
// 3. This notice may not be removed or altered from any source distribution.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Core.FFmpeg;
|
||||
|
||||
@@ -32,36 +29,11 @@ internal class FFmpegProcessBuilder
|
||||
{
|
||||
private readonly List<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)
|
||||
public FFmpegProcessBuilder(string ffmpegPath)
|
||||
{
|
||||
_ffmpegPath = ffmpegPath;
|
||||
_saveReports = saveReports;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public FFmpegProcessBuilder WithVaapiDriver(VaapiDriver vaapiDriver, string vaapiDevice)
|
||||
{
|
||||
if (vaapiDriver != VaapiDriver.Default)
|
||||
{
|
||||
_vaapiDriver = vaapiDriver;
|
||||
}
|
||||
|
||||
_vaapiDevice = string.IsNullOrWhiteSpace(vaapiDevice)
|
||||
? "/dev/dri/renderD128"
|
||||
: vaapiDevice;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public FFmpegProcessBuilder WithThreads(int threads)
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
else if (audioPath.IfNone("NotARealPath") != videoPath)
|
||||
{
|
||||
audioIndex = 1;
|
||||
if (_hwAccel == HardwareAccelerationKind.None)
|
||||
{
|
||||
_outputPixelFormat = "yuv420p";
|
||||
}
|
||||
}
|
||||
|
||||
string videoLabel = $"{videoIndex}:{videoStreamIndex}";
|
||||
@@ -639,11 +179,6 @@ internal class FFmpegProcessBuilder
|
||||
_arguments.Add(filter.ComplexFilter);
|
||||
videoLabel = filter.VideoLabel;
|
||||
audioLabel = filter.AudioLabel;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.PixelFormat))
|
||||
{
|
||||
_outputPixelFormat = filter.PixelFormat;
|
||||
}
|
||||
});
|
||||
|
||||
foreach (string _ in audioPath)
|
||||
@@ -676,44 +211,6 @@ internal class FFmpegProcessBuilder
|
||||
StandardOutputEncoding = Encoding.UTF8
|
||||
};
|
||||
|
||||
if (_hwAccel == HardwareAccelerationKind.Vaapi)
|
||||
{
|
||||
switch (_vaapiDriver)
|
||||
{
|
||||
case VaapiDriver.i965:
|
||||
startInfo.EnvironmentVariables["LIBVA_DRIVER_NAME"] = "i965";
|
||||
break;
|
||||
case VaapiDriver.iHD:
|
||||
startInfo.EnvironmentVariables["LIBVA_DRIVER_NAME"] = "iHD";
|
||||
break;
|
||||
case VaapiDriver.RadeonSI:
|
||||
startInfo.EnvironmentVariables["LIBVA_DRIVER_NAME"] = "radeonsi";
|
||||
break;
|
||||
case VaapiDriver.Nouveau:
|
||||
startInfo.EnvironmentVariables["LIBVA_DRIVER_NAME"] = "nouveau";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_saveReports)
|
||||
{
|
||||
string fileName = _isConcat
|
||||
? Path.Combine(FileSystemLayout.FFmpegReportsFolder, "ffmpeg-%t-concat.log")
|
||||
: Path.Combine(FileSystemLayout.FFmpegReportsFolder, "ffmpeg-%t-transcode.log");
|
||||
|
||||
// rework filename in a format that works on windows
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// \ is escape, so use / for directory separators
|
||||
fileName = fileName.Replace(@"\", @"/");
|
||||
|
||||
// colon after drive letter needs to be escaped
|
||||
fileName = fileName.Replace(@":/", @"\:/");
|
||||
}
|
||||
|
||||
startInfo.EnvironmentVariables["FFREPORT"] = $"file={fileName}:level=32";
|
||||
}
|
||||
|
||||
startInfo.ArgumentList.Add("-nostdin");
|
||||
foreach (string argument in _arguments)
|
||||
{
|
||||
@@ -725,30 +222,4 @@ internal class FFmpegProcessBuilder
|
||||
StartInfo = startInfo
|
||||
};
|
||||
}
|
||||
|
||||
private int GetFrameRateFromMediaVersion(Option<MediaVersion> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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
|
||||
videoStream,
|
||||
None,
|
||||
videoPath,
|
||||
None,
|
||||
playbackSettings.VideoFormat)
|
||||
None)
|
||||
.WithOutputFormat("apng", outputFile)
|
||||
.Build();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
IList<MediaItem> mediaItems,
|
||||
CollectionEnumeratorState state)
|
||||
{
|
||||
_collection = collection;
|
||||
|
||||
// TODO: this will break if we allow shows and seasons
|
||||
_sortedMediaItems = collection.CollectionItems
|
||||
.OrderBy(ci => ci.CustomIndex)
|
||||
|
||||
@@ -213,6 +213,31 @@ public class PipelineBuilderBaseTests
|
||||
command.Should().Be(
|
||||
"-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -f concat -safe 0 -protocol_whitelist file,http,tcp,https,tcp,tls -probesize 32 -re -stream_loop -1 -i http://localhost:8080/ffmpeg/concat/1 -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -sc_threshold 0 -c copy -map_metadata -1 -metadata service_provider=\"ErsatzTV\" -metadata service_name=\"Some Channel\" -f mpegts -mpegts_flags +initial_discontinuity pipe:1");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Wrap_Segmenter_Test()
|
||||
{
|
||||
var resolution = new FrameSize(1920, 1080);
|
||||
var concatInputFile = new ConcatInputFile("http://localhost:8080/iptv/channel/1.m3u8?mode=segmenter", resolution);
|
||||
|
||||
var builder = new SoftwarePipelineBuilder(
|
||||
HardwareAccelerationMode.None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
"",
|
||||
"",
|
||||
_logger);
|
||||
FFmpegPipeline result = builder.WrapSegmenter(concatInputFile, FFmpegState.Concat(false, "Some Channel"));
|
||||
|
||||
result.PipelineSteps.Should().HaveCountGreaterThan(0);
|
||||
|
||||
string command = PrintCommand(None, None, None, concatInputFile, result);
|
||||
|
||||
command.Should().Be(
|
||||
"-nostdin -threads 1 -hide_banner -loglevel error -nostats -fflags +genpts+discardcorrupt+igndts -re -i http://localhost:8080/iptv/channel/1.m3u8?mode=segmenter -map 0 -c copy -metadata service_provider=\"ErsatzTV\" -metadata service_name=\"Some Channel\" -f mpegts pipe:1");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HlsDirect_Test()
|
||||
|
||||
@@ -97,7 +97,6 @@ public static class CommandGenerator
|
||||
|
||||
foreach (IPipelineStep step in sortedSteps)
|
||||
{
|
||||
|
||||
arguments.AddRange(step.OutputOptions);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
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,21 +104,102 @@ 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 (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.IndexOf(audioInputFile.Path);
|
||||
int inputIndex = distinctPaths.LastIndexOf(audioInputFile.Path);
|
||||
foreach ((int index, _, _) in audioInputFile.Streams)
|
||||
{
|
||||
audioLabel = $"{inputIndex}:{index}";
|
||||
@@ -146,233 +215,18 @@ public class ComplexFilter : IPipelineStep
|
||||
}
|
||||
}
|
||||
|
||||
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)))
|
||||
{
|
||||
watermarkFilterComplex += $"[{inputIndex}:{index}]";
|
||||
watermarkFilterComplex += string.Join(
|
||||
",",
|
||||
watermarkInputFile.FilterSteps.Select(f => f.Filter)
|
||||
.Filter(s => !string.IsNullOrWhiteSpace(s)));
|
||||
watermarkLabel = "[wm]";
|
||||
watermarkFilterComplex += watermarkLabel;
|
||||
}
|
||||
else
|
||||
{
|
||||
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]";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 (subtitleInputFile.FilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter)))
|
||||
{
|
||||
subtitleFilterComplex += $"[{inputIndex}:{index}]";
|
||||
subtitleFilterComplex += string.Join(
|
||||
",",
|
||||
subtitleInputFile.FilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s)));
|
||||
subtitleLabel = "[st]";
|
||||
subtitleFilterComplex += subtitleLabel;
|
||||
}
|
||||
else
|
||||
{
|
||||
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)
|
||||
{
|
||||
_logger.LogDebug("Desired pixel format {PixelFormat}", pixelFormat);
|
||||
|
||||
string tempVideoLabel = videoLabel.StartsWith("[") && videoLabel.EndsWith("]")
|
||||
? videoLabel
|
||||
: $"[{videoLabel}]";
|
||||
|
||||
// normalize pixel format and color params
|
||||
string filter = string.Empty;
|
||||
|
||||
if (!videoStream.ColorParams.IsBt709)
|
||||
{
|
||||
_logger.LogDebug("Adding colorspace filter");
|
||||
var colorspace = new ColorspaceFilter(_currentState, videoStream, pixelFormat);
|
||||
_pipelineSteps.Add(colorspace);
|
||||
filter = colorspace.Filter;
|
||||
}
|
||||
|
||||
if (state.PixelFormat.Map(f => f.FFmpegName) != pixelFormat.FFmpegName)
|
||||
{
|
||||
_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
|
||||
{
|
||||
_logger.LogDebug("Frame data location is HARDWARE");
|
||||
filter = _ffmpegState.EncoderHardwareAccelerationMode switch
|
||||
{
|
||||
HardwareAccelerationMode.Nvenc => $"{filter},scale_cuda=format={pixelFormat.FFmpegName}",
|
||||
_ => filter
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
pixelFormatFilterComplex = $"{tempVideoLabel}{filter}[vpf]";
|
||||
|
||||
// change the mapped label
|
||||
videoLabel = "[vpf]";
|
||||
}
|
||||
}
|
||||
|
||||
var filterComplex = string.Join(
|
||||
";",
|
||||
new[]
|
||||
{
|
||||
audioFilterComplex,
|
||||
videoFilterComplex,
|
||||
watermarkFilterComplex,
|
||||
subtitleFilterComplex,
|
||||
watermarkOverlayFilterComplex,
|
||||
watermarkFilterComplex,
|
||||
subtitleOverlayFilterComplex,
|
||||
watermarkOverlayFilterComplex,
|
||||
pixelFormatFilterComplex
|
||||
}.Where(
|
||||
s => !string.IsNullOrWhiteSpace(s)));
|
||||
}.Where(s => !string.IsNullOrWhiteSpace(s)));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filterComplex))
|
||||
{
|
||||
@@ -393,4 +247,6 @@ public class ComplexFilter : IPipelineStep
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private string ProperLabel(string label) => label.StartsWith("[") ? label : $"[{label}]";
|
||||
}
|
||||
|
||||
@@ -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}]";
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.FFmpeg.Option;
|
||||
|
||||
public class MapAllStreamsOutputOption : OutputOption
|
||||
{
|
||||
public override IList<string> OutputOptions => new[] { "-map", "0" };
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
SetMetadataAudioLanguage(ffmpegState, pipelineSteps);
|
||||
SetOutputFormat(ffmpegState, desiredState, pipelineSteps, videoStream);
|
||||
|
||||
var complexFilter = new NewComplexFilter(
|
||||
var complexFilter = new ComplexFilter(
|
||||
_videoInputFile,
|
||||
_audioInputFile,
|
||||
_watermarkInputFile,
|
||||
@@ -193,7 +218,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
|
||||
return new FFmpegPipeline(pipelineSteps);
|
||||
}
|
||||
|
||||
protected Option<IDecoder> LogUnknownDecoder(
|
||||
private Option<IDecoder> LogUnknownDecoder(
|
||||
HardwareAccelerationMode hardwareAccelerationMode,
|
||||
string videoFormat,
|
||||
string pixelFormat)
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Emby;
|
||||
using ErsatzTV.Core.Interfaces.Emby;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using ErsatzTV.Infrastructure.Emby.Models;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -13,17 +13,14 @@ namespace ErsatzTV.Infrastructure.Emby;
|
||||
public class EmbyApiClient : IEmbyApiClient
|
||||
{
|
||||
private readonly IEmbyPathReplacementService _embyPathReplacementService;
|
||||
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
|
||||
private readonly ILogger<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
|
||||
{
|
||||
MetadataKind = MetadataKind.External,
|
||||
Title = item.Name,
|
||||
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
|
||||
SortTitle = SortTitle.GetSortTitle(item.Name),
|
||||
Plot = item.Overview,
|
||||
Year = item.ProductionYear,
|
||||
Tagline = Optional(item.Taglines).Flatten().HeadOrNone().IfNone(string.Empty),
|
||||
@@ -500,7 +497,7 @@ public class EmbyApiClient : IEmbyApiClient
|
||||
{
|
||||
MetadataKind = MetadataKind.External,
|
||||
Title = item.Name,
|
||||
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
|
||||
SortTitle = SortTitle.GetSortTitle(item.Name),
|
||||
Plot = item.Overview,
|
||||
Year = item.ProductionYear,
|
||||
Tagline = Optional(item.Taglines).Flatten().HeadOrNone().IfNone(string.Empty),
|
||||
@@ -572,7 +569,7 @@ public class EmbyApiClient : IEmbyApiClient
|
||||
{
|
||||
MetadataKind = MetadataKind.External,
|
||||
Title = item.Name,
|
||||
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
|
||||
SortTitle = SortTitle.GetSortTitle(item.Name),
|
||||
Year = item.ProductionYear,
|
||||
DateAdded = dateAdded,
|
||||
Artwork = new List<Artwork>(),
|
||||
@@ -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,
|
||||
|
||||
@@ -3,6 +3,7 @@ using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Jellyfin;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Jellyfin;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using ErsatzTV.Infrastructure.Jellyfin.Models;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -12,19 +13,16 @@ namespace ErsatzTV.Infrastructure.Jellyfin;
|
||||
|
||||
public class JellyfinApiClient : IJellyfinApiClient
|
||||
{
|
||||
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
|
||||
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
|
||||
private readonly ILogger<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
|
||||
{
|
||||
MetadataKind = MetadataKind.External,
|
||||
Title = item.Name,
|
||||
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
|
||||
SortTitle = SortTitle.GetSortTitle(item.Name),
|
||||
Plot = item.Overview,
|
||||
Year = item.ProductionYear,
|
||||
Tagline = Optional(item.Taglines).Flatten().HeadOrNone().IfNone(string.Empty),
|
||||
@@ -558,7 +556,7 @@ public class JellyfinApiClient : IJellyfinApiClient
|
||||
{
|
||||
MetadataKind = MetadataKind.External,
|
||||
Title = item.Name,
|
||||
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
|
||||
SortTitle = SortTitle.GetSortTitle(item.Name),
|
||||
Plot = item.Overview,
|
||||
Year = item.ProductionYear,
|
||||
Tagline = Optional(item.Taglines).Flatten().HeadOrNone().IfNone(string.Empty),
|
||||
@@ -630,7 +628,7 @@ public class JellyfinApiClient : IJellyfinApiClient
|
||||
{
|
||||
MetadataKind = MetadataKind.External,
|
||||
Title = item.Name,
|
||||
SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name),
|
||||
SortTitle = SortTitle.GetSortTitle(item.Name),
|
||||
Year = item.ProductionYear,
|
||||
DateAdded = dateAdded,
|
||||
Artwork = new List<Artwork>(),
|
||||
@@ -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,
|
||||
|
||||
@@ -3,6 +3,7 @@ using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Plex;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using ErsatzTV.Core.Plex;
|
||||
using ErsatzTV.Infrastructure.Plex.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -12,16 +13,11 @@ namespace ErsatzTV.Infrastructure.Plex;
|
||||
|
||||
public class PlexServerApiClient : IPlexServerApiClient
|
||||
{
|
||||
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
|
||||
private readonly ILogger<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
|
||||
{
|
||||
MetadataKind = MetadataKind.External,
|
||||
Title = response.Title,
|
||||
SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title),
|
||||
SortTitle = SortTitle.GetSortTitle(response.Title),
|
||||
Plot = response.Summary,
|
||||
Year = response.Year,
|
||||
Tagline = response.Tagline,
|
||||
@@ -638,7 +634,7 @@ public class PlexServerApiClient : IPlexServerApiClient
|
||||
{
|
||||
MetadataKind = MetadataKind.External,
|
||||
Title = response.Title,
|
||||
SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title),
|
||||
SortTitle = SortTitle.GetSortTitle(response.Title),
|
||||
Plot = response.Summary,
|
||||
Year = response.Year,
|
||||
Tagline = response.Tagline,
|
||||
@@ -734,7 +730,7 @@ public class PlexServerApiClient : IPlexServerApiClient
|
||||
{
|
||||
MetadataKind = MetadataKind.External,
|
||||
Title = response.Title,
|
||||
SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title),
|
||||
SortTitle = SortTitle.GetSortTitle(response.Title),
|
||||
Year = response.Year,
|
||||
DateAdded = dateAdded,
|
||||
DateUpdated = lastWriteTime,
|
||||
@@ -851,7 +847,7 @@ public class PlexServerApiClient : IPlexServerApiClient
|
||||
{
|
||||
MetadataKind = MetadataKind.External,
|
||||
Title = response.Title,
|
||||
SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title),
|
||||
SortTitle = SortTitle.GetSortTitle(response.Title),
|
||||
EpisodeNumber = response.Index,
|
||||
Plot = response.Summary,
|
||||
Year = response.Year,
|
||||
|
||||
+8
-6
@@ -3,6 +3,7 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Bugsnag;
|
||||
using CliWrap;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
@@ -20,6 +21,7 @@ using ErsatzTV.FFmpeg.Format;
|
||||
using ErsatzTV.FFmpeg.Pipeline;
|
||||
using ErsatzTV.FFmpeg.State;
|
||||
using ErsatzTV.Infrastructure.Runtime;
|
||||
using ErsatzTV.Scanner.Core.Metadata;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -28,7 +30,7 @@ using NUnit.Framework;
|
||||
using Serilog;
|
||||
using MediaStream = ErsatzTV.Core.Domain.MediaStream;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.FFmpeg;
|
||||
namespace ErsatzTV.Scanner.Tests.Core.FFmpeg;
|
||||
|
||||
[TestFixture]
|
||||
[Explicit]
|
||||
@@ -316,13 +318,13 @@ public class TranscodingTests
|
||||
.Filter(s => s.MediaStreamKind == MediaStreamKind.Subtitle)
|
||||
.ToList();
|
||||
|
||||
var subtitles = new List<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
|
||||
{
|
||||
// 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
|
||||
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) =>
|
||||
+15
-3
@@ -2,13 +2,13 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using ErsatzTV.Scanner.Core.Metadata;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Metadata;
|
||||
namespace ErsatzTV.Scanner.Tests.Core.Metadata;
|
||||
|
||||
[TestFixture]
|
||||
public class LocalStatisticsProviderTests
|
||||
@@ -25,7 +25,19 @@ public class LocalStatisticsProviderTests
|
||||
new Mock<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>());
|
||||
|
||||
+3
-3
@@ -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
|
||||
@@ -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;
|
||||
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
using System.Text;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core.Metadata.Nfo;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Scanner.Core.Metadata.Nfo;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IO;
|
||||
@@ -8,7 +9,7 @@ using Moq;
|
||||
using NUnit.Framework;
|
||||
using Serilog;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Metadata.Nfo;
|
||||
namespace ErsatzTV.Scanner.Tests.Core.Metadata.Nfo;
|
||||
|
||||
[TestFixture]
|
||||
public class ArtistNfoReaderTests
|
||||
+27
-26
@@ -1,6 +1,7 @@
|
||||
using System.Text;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core.Metadata.Nfo;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Scanner.Core.Metadata.Nfo;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IO;
|
||||
@@ -8,7 +9,7 @@ using Moq;
|
||||
using NUnit.Framework;
|
||||
using Serilog;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Metadata.Nfo;
|
||||
namespace ErsatzTV.Scanner.Tests.Core.Metadata.Nfo;
|
||||
|
||||
[TestFixture]
|
||||
public class EpisodeNfoReaderTests
|
||||
@@ -45,10 +46,10 @@ public class EpisodeNfoReaderTests
|
||||
<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
|
||||
<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
|
||||
<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
|
||||
<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
|
||||
<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
|
||||
<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
|
||||
<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
|
||||
</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
|
||||
<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
|
||||
<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
|
||||
<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
|
||||
"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");
|
||||
+4
-3
@@ -1,13 +1,14 @@
|
||||
using System.Text;
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core.Metadata.Nfo;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Scanner.Core.Metadata.Nfo;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.IO;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Metadata.Nfo;
|
||||
namespace ErsatzTV.Scanner.Tests.Core.Metadata.Nfo;
|
||||
|
||||
[TestFixture]
|
||||
public class MovieNfoReaderTests
|
||||
@@ -187,7 +188,7 @@ https://www.themoviedb.org/movie/11-star-wars"));
|
||||
|
||||
nfo.Plot.Should().Be(
|
||||
"Determined to ensure Superman's ultimate sacrifice was not in vain, Bruce Wayne aligns forces with Diana Prince with plans to recruit a team of metahumans to protect the world from an approaching threat of catastrophic proportions.");
|
||||
nfo.Tagline.Should().BeNullOrEmpty();
|
||||
// nfo.Tagline.Should().BeNullOrEmpty();
|
||||
nfo.Genres.Should().BeEquivalentTo(new List<string> { "SuperHero" });
|
||||
nfo.Tags.Should().BeEquivalentTo(new List<string> { "TV Recording" });
|
||||
nfo.Studios.Should().BeEquivalentTo(new List<string> { "Warner Bros. Pictures" });
|
||||
+3
-2
@@ -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
|
||||
+3
-2
@@ -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
|
||||
+18
-17
@@ -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
|
||||
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
|
||||
{
|
||||
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
|
||||
@"<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"));
|
||||
<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"));
|
||||
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"));
|
||||
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"));
|
||||
{
|
||||
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" });
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata;
|
||||
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata;
|
||||
|
||||
public interface ILocalMetadataProvider
|
||||
{
|
||||
+3
-2
@@ -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
|
||||
{
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata;
|
||||
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata;
|
||||
|
||||
public interface ILocalSubtitlesProvider
|
||||
{
|
||||
+3
-2
@@ -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
|
||||
{
|
||||
+3
-2
@@ -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
|
||||
{
|
||||
+3
-2
@@ -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
|
||||
{
|
||||
+3
-2
@@ -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
|
||||
{
|
||||
+3
-2
@@ -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
|
||||
{
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,7 +2,7 @@
|
||||
using System.Text;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
|
||||
namespace ErsatzTV.Core.Metadata;
|
||||
namespace ErsatzTV.Scanner.Core.Metadata;
|
||||
|
||||
public static class FolderEtag
|
||||
{
|
||||
@@ -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;
|
||||
|
||||
+41
-38
@@ -1,13 +1,16 @@
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Metadata.Nfo;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Metadata.Nfo;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using ErsatzTV.Scanner.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo;
|
||||
using ErsatzTV.Scanner.Core.Metadata.Nfo;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Core.Metadata;
|
||||
namespace ErsatzTV.Scanner.Core.Metadata;
|
||||
|
||||
public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
{
|
||||
@@ -28,7 +31,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
private readonly IOtherVideoRepository _otherVideoRepository;
|
||||
private readonly ISongRepository _songRepository;
|
||||
private readonly ITelevisionRepository _televisionRepository;
|
||||
private readonly ITvShowNfoReader _tvShowNfoReader;
|
||||
private readonly IShowNfoReader _showNfoReader;
|
||||
|
||||
public LocalMetadataProvider(
|
||||
IMetadataRepository metadataRepository,
|
||||
@@ -44,7 +47,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
IEpisodeNfoReader episodeNfoReader,
|
||||
IArtistNfoReader artistNfoReader,
|
||||
IMusicVideoNfoReader musicVideoNfoReader,
|
||||
ITvShowNfoReader tvShowNfoReader,
|
||||
IShowNfoReader showNfoReader,
|
||||
IOtherVideoNfoReader otherVideoNfoReader,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
IClient client,
|
||||
@@ -63,7 +66,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
_episodeNfoReader = episodeNfoReader;
|
||||
_artistNfoReader = artistNfoReader;
|
||||
_musicVideoNfoReader = musicVideoNfoReader;
|
||||
_tvShowNfoReader = tvShowNfoReader;
|
||||
_showNfoReader = showNfoReader;
|
||||
_otherVideoNfoReader = otherVideoNfoReader;
|
||||
_localStatisticsProvider = localStatisticsProvider;
|
||||
_client = client;
|
||||
@@ -81,12 +84,12 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
|
||||
foreach (ShowMetadata metadata in maybeMetadata)
|
||||
{
|
||||
metadata.SortTitle = _fallbackMetadataProvider.GetSortTitle(metadata.Title);
|
||||
metadata.SortTitle = SortTitle.GetSortTitle(metadata.Title);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
ShowMetadata fallbackMetadata = _fallbackMetadataProvider.GetFallbackMetadataForShow(showFolder);
|
||||
fallbackMetadata.SortTitle = _fallbackMetadataProvider.GetSortTitle(fallbackMetadata.Title);
|
||||
fallbackMetadata.SortTitle = SortTitle.GetSortTitle(fallbackMetadata.Title);
|
||||
return fallbackMetadata;
|
||||
}
|
||||
|
||||
@@ -101,12 +104,12 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
|
||||
foreach (ArtistMetadata metadata in maybeMetadata)
|
||||
{
|
||||
metadata.SortTitle = _fallbackMetadataProvider.GetSortTitle(metadata.Title);
|
||||
metadata.SortTitle = SortTitle.GetSortTitle(metadata.Title);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
ArtistMetadata fallbackMetadata = _fallbackMetadataProvider.GetFallbackMetadataForArtist(artistFolder);
|
||||
fallbackMetadata.SortTitle = _fallbackMetadataProvider.GetSortTitle(fallbackMetadata.Title);
|
||||
fallbackMetadata.SortTitle = SortTitle.GetSortTitle(fallbackMetadata.Title);
|
||||
return fallbackMetadata;
|
||||
}
|
||||
|
||||
@@ -297,37 +300,37 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
Tags = new List<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
|
||||
foreach (EpisodeMetadata metadata in toAdd)
|
||||
{
|
||||
metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
? SortTitle.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
metadata.EpisodeId = episode.Id;
|
||||
metadata.Episode = episode;
|
||||
@@ -415,7 +418,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
existing.ReleaseDate = metadata.ReleaseDate;
|
||||
existing.Year = metadata.Year;
|
||||
existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
? SortTitle.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
|
||||
updated = await UpdateMetadataCollections(
|
||||
@@ -515,7 +518,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
existing.ReleaseDate = metadata.ReleaseDate;
|
||||
existing.Year = metadata.Year;
|
||||
existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
? SortTitle.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
|
||||
bool updated = await UpdateMetadataCollections(
|
||||
@@ -590,7 +593,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
}
|
||||
|
||||
metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
? SortTitle.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
metadata.MovieId = movie.Id;
|
||||
movie.MovieMetadata = new List<MovieMetadata> { metadata };
|
||||
@@ -620,7 +623,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
existing.ReleaseDate = metadata.ReleaseDate;
|
||||
existing.Year = metadata.Year;
|
||||
existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
? SortTitle.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
|
||||
bool updated = await UpdateMetadataCollections(
|
||||
@@ -655,7 +658,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
}
|
||||
|
||||
metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
? SortTitle.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
metadata.ShowId = show.Id;
|
||||
show.ShowMetadata = new List<ShowMetadata> { metadata };
|
||||
@@ -680,7 +683,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
existing.DateUpdated = metadata.DateUpdated;
|
||||
existing.MetadataKind = metadata.MetadataKind;
|
||||
existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
? SortTitle.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
|
||||
var updated = false;
|
||||
@@ -749,7 +752,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
}
|
||||
|
||||
metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
? SortTitle.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
metadata.ArtistId = artist.Id;
|
||||
artist.ArtistMetadata = new List<ArtistMetadata> { metadata };
|
||||
@@ -778,7 +781,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
existing.OriginalTitle = metadata.OriginalTitle;
|
||||
existing.ReleaseDate = metadata.ReleaseDate;
|
||||
existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
? SortTitle.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
|
||||
bool updated = await UpdateMetadataCollections(
|
||||
@@ -815,7 +818,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
}
|
||||
|
||||
metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
? SortTitle.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
metadata.MusicVideoId = musicVideo.Id;
|
||||
musicVideo.MusicVideoMetadata = new List<MusicVideoMetadata> { metadata };
|
||||
@@ -845,7 +848,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
existing.ReleaseDate = metadata.ReleaseDate;
|
||||
existing.Year = metadata.Year;
|
||||
existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
? SortTitle.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
existing.OriginalTitle = metadata.OriginalTitle;
|
||||
|
||||
@@ -921,7 +924,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
}
|
||||
|
||||
metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
? SortTitle.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
metadata.OtherVideoId = otherVideo.Id;
|
||||
otherVideo.OtherVideoMetadata = new List<OtherVideoMetadata> { metadata };
|
||||
@@ -949,7 +952,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
existing.DateUpdated = metadata.DateUpdated;
|
||||
existing.MetadataKind = metadata.MetadataKind;
|
||||
existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
? SortTitle.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
existing.OriginalTitle = metadata.OriginalTitle;
|
||||
|
||||
@@ -965,7 +968,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
}
|
||||
|
||||
metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
? SortTitle.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
metadata.SongId = song.Id;
|
||||
song.SongMetadata = new List<SongMetadata> { metadata };
|
||||
@@ -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
|
||||
error.ToString());
|
||||
}
|
||||
|
||||
foreach (TvShowNfo nfo in maybeNfo.RightToSeq())
|
||||
foreach (ShowNfo nfo in maybeNfo.RightToSeq())
|
||||
{
|
||||
DateTime dateAdded = DateTime.UtcNow;
|
||||
DateTime dateUpdated = File.GetLastWriteTimeUtc(nfoFileName);
|
||||
@@ -1066,7 +1069,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
{
|
||||
try
|
||||
{
|
||||
Either<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
|
||||
}
|
||||
|
||||
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
|
||||
DateAdded = dateAdded,
|
||||
DateUpdated = dateUpdated,
|
||||
Title = nfo.Title,
|
||||
SortTitle = _fallbackMetadataProvider.GetSortTitle(nfo.Title),
|
||||
SortTitle = SortTitle.GetSortTitle(nfo.Title),
|
||||
EpisodeNumber = nfo.Episode,
|
||||
Year = GetYear(0, nfo.Aired),
|
||||
ReleaseDate = GetAired(0, nfo.Aired),
|
||||
@@ -1169,7 +1172,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
ReleaseDate = releaseDate,
|
||||
Plot = nfo.Plot,
|
||||
Outline = nfo.Outline,
|
||||
Tagline = nfo.Tagline,
|
||||
// Tagline = nfo.Tagline,
|
||||
Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(),
|
||||
Tags = nfo.Tags.Map(t => new Tag { Name = t }).ToList(),
|
||||
Studios = nfo.Studios.Map(s => new Studio { Name = s }).ToList(),
|
||||
@@ -1300,7 +1303,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider
|
||||
Func<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;
|
||||
|
||||
+48
-45
@@ -5,14 +5,16 @@ using System.Text.RegularExpressions;
|
||||
using Bugsnag;
|
||||
using CliWrap;
|
||||
using CliWrap.Buffered;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Scanner.Core.Interfaces.Metadata;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ErsatzTV.Core.Metadata;
|
||||
namespace ErsatzTV.Scanner.Core.Metadata;
|
||||
|
||||
public class LocalStatisticsProvider : ILocalStatisticsProvider
|
||||
{
|
||||
@@ -237,9 +239,8 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
|
||||
return BaseError.New($"FFprobe at {ffprobePath} exited with code {probe.ExitCode}");
|
||||
}
|
||||
|
||||
FFprobe ffprobe = JsonConvert.DeserializeObject<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
|
||||
{
|
||||
string sar = match.Groups[1].Value;
|
||||
string dar = match.Groups[2].Value;
|
||||
foreach (FFprobeStream stream in ffprobe.streams.Where(s => s.codec_type == "video").ToList())
|
||||
foreach (FFprobeStream stream in Optional(ffprobe.streams?.Where(s => s.codec_type == "video")).Flatten())
|
||||
{
|
||||
FFprobeStream replacement = stream with { sample_aspect_ratio = sar, display_aspect_ratio = dar };
|
||||
ffprobe.streams.Remove(stream);
|
||||
ffprobe.streams.Add(replacement);
|
||||
ffprobe.streams?.Remove(stream);
|
||||
ffprobe.streams?.Add(replacement);
|
||||
}
|
||||
}
|
||||
|
||||
return ffprobe;
|
||||
}
|
||||
|
||||
return ffprobe;
|
||||
return BaseError.New("Unable to deserialize ffprobe output");
|
||||
}
|
||||
|
||||
private async Task AnalyzeDuration(string ffmpegPath, string path, MediaVersion version)
|
||||
@@ -322,7 +325,7 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
|
||||
|
||||
internal MediaVersion ProjectToMediaVersion(string path, FFprobe probeOutput) =>
|
||||
Optional(probeOutput)
|
||||
.Filter(json => json?.format != null && json.streams != null)
|
||||
.Filter(json => json is { format: { }, streams: { } })
|
||||
.ToValidation<BaseError>("Unable to parse ffprobe output")
|
||||
.ToEither<FFprobe>()
|
||||
.Match(
|
||||
@@ -337,7 +340,7 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
|
||||
};
|
||||
|
||||
if (double.TryParse(
|
||||
json.format.duration,
|
||||
json.format?.duration,
|
||||
NumberStyles.Number,
|
||||
CultureInfo.InvariantCulture,
|
||||
out double duration))
|
||||
@@ -380,7 +383,7 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
|
||||
version.Streams.Add(stream);
|
||||
}
|
||||
|
||||
FFprobeStream videoStream = json.streams.FirstOrDefault(s => s.codec_type == "video");
|
||||
FFprobeStream? videoStream = json.streams?.FirstOrDefault(s => s.codec_type == "video");
|
||||
if (videoStream != null)
|
||||
{
|
||||
version.SampleAspectRatio = string.IsNullOrWhiteSpace(videoStream.sample_aspect_ratio)
|
||||
@@ -465,7 +468,7 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
|
||||
version.Streams.Add(stream);
|
||||
}
|
||||
|
||||
foreach (FFprobeChapter probedChapter in json.chapters)
|
||||
foreach (FFprobeChapter probedChapter in Optional(json.chapters).Flatten())
|
||||
{
|
||||
if (double.TryParse(
|
||||
probedChapter.start_time,
|
||||
@@ -484,7 +487,7 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
|
||||
ChapterId = probedChapter.id,
|
||||
StartTime = TimeSpan.FromSeconds(startTime),
|
||||
EndTime = TimeSpan.FromSeconds(endTime),
|
||||
Title = probedChapter?.tags?.title
|
||||
Title = probedChapter.tags?.title
|
||||
};
|
||||
|
||||
version.Chapters.Add(chapter);
|
||||
@@ -516,60 +519,60 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
|
||||
Chapters = new List<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);
|
||||
}
|
||||
+9
-8
@@ -3,9 +3,10 @@ using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Scanner.Core.Interfaces.Metadata;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Core.Metadata;
|
||||
namespace ErsatzTV.Scanner.Core.Metadata;
|
||||
|
||||
public class LocalSubtitlesProvider : ILocalSubtitlesProvider
|
||||
{
|
||||
@@ -50,12 +51,12 @@ public class LocalSubtitlesProvider : ILocalSubtitlesProvider
|
||||
return false;
|
||||
}
|
||||
|
||||
Option<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
|
||||
.GetType().Name);
|
||||
}
|
||||
|
||||
foreach (Domain.Metadata metadata in maybeMetadata)
|
||||
foreach (ErsatzTV.Core.Domain.Metadata metadata in maybeMetadata)
|
||||
{
|
||||
MediaVersion version = mediaItem.GetHeadVersion();
|
||||
var subtitleStreams = version.Streams
|
||||
@@ -96,7 +97,7 @@ public class LocalSubtitlesProvider : ILocalSubtitlesProvider
|
||||
{
|
||||
var result = new List<Subtitle>();
|
||||
|
||||
string folder = Path.GetDirectoryName(mediaItemPath);
|
||||
string? folder = Path.GetDirectoryName(mediaItemPath);
|
||||
string withoutExtension = Path.GetFileNameWithoutExtension(mediaItemPath);
|
||||
foreach (string file in _localFileSystem.ListFiles(folder, $"{withoutExtension}*"))
|
||||
{
|
||||
+5
-3
@@ -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
|
||||
|
||||
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;
|
||||
+5
-3
@@ -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>
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace ErsatzTV.Core.Metadata;
|
||||
namespace ErsatzTV.Scanner.Core.Metadata;
|
||||
|
||||
public static class MetadataSongTag
|
||||
{
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
+5
-9
@@ -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
|
||||
|
||||
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
|
||||
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);
|
||||
@@ -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; }
|
||||
}
|
||||
+9
-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
|
||||
_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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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;
|
||||
@@ -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; }
|
||||
}
|
||||
+5
-13
@@ -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
|
||||
|
||||
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
|
||||
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);
|
||||
@@ -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
Reference in New Issue
Block a user