mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* 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 segmenterpull/1083/head
118 changed files with 926 additions and 2973 deletions
@ -1,3 +0,0 @@
@@ -1,3 +0,0 @@
|
||||
namespace ErsatzTV.Application.MediaSources; |
||||
|
||||
public record LocalMediaSourceViewModel(int Id) : MediaSourceViewModel(Id, "Local"); |
||||
@ -1,777 +0,0 @@
@@ -1,777 +0,0 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.FFmpeg; |
||||
using ErsatzTV.FFmpeg.State; |
||||
using FluentAssertions; |
||||
using NUnit.Framework; |
||||
|
||||
namespace ErsatzTV.Core.Tests.FFmpeg; |
||||
|
||||
[TestFixture] |
||||
public class FFmpegComplexFilterBuilderTests |
||||
{ |
||||
[TestFixture] |
||||
public class Build |
||||
{ |
||||
[Test] |
||||
public void Should_Return_None_With_No_Filters() |
||||
{ |
||||
var builder = new FFmpegComplexFilterBuilder(); |
||||
|
||||
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false); |
||||
|
||||
result.IsNone.Should().BeTrue(); |
||||
} |
||||
|
||||
[Test] |
||||
public void Should_Return_Audio_Filter_With_AudioDuration() |
||||
{ |
||||
var duration = TimeSpan.FromMilliseconds(1000.1); |
||||
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() |
||||
.WithAlignedAudio(duration); |
||||
|
||||
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false); |
||||
|
||||
result.IsSome.Should().BeTrue(); |
||||
result.IfSome( |
||||
filter => |
||||
{ |
||||
filter.ComplexFilter.Should().Be("[0:1]apad=whole_dur=1000.1ms[a]"); |
||||
filter.AudioLabel.Should().Be("[a]"); |
||||
filter.VideoLabel.Should().Be("0:0"); |
||||
}); |
||||
} |
||||
|
||||
[Test] |
||||
// this needs to be a culture where '.' is a group separator
|
||||
[SetCulture("it-IT")] |
||||
public void Should_Return_Audio_Filter_With_AudioDuration_Decimal() |
||||
{ |
||||
var duration = TimeSpan.FromMilliseconds(1000.1); |
||||
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() |
||||
.WithAlignedAudio(duration); |
||||
|
||||
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false); |
||||
|
||||
result.IsSome.Should().BeTrue(); |
||||
result.IfSome( |
||||
filter => |
||||
{ |
||||
filter.ComplexFilter.Should().Be("[0:1]apad=whole_dur=1000.1ms[a]"); |
||||
filter.AudioLabel.Should().Be("[a]"); |
||||
filter.VideoLabel.Should().Be("0:0"); |
||||
}); |
||||
} |
||||
|
||||
[Test] |
||||
public void Should_Return_Audio_And_Video_Filter() |
||||
{ |
||||
var duration = TimeSpan.FromMinutes(54); |
||||
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() |
||||
.WithAlignedAudio(duration) |
||||
.WithDeinterlace(true); |
||||
|
||||
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false); |
||||
|
||||
result.IsSome.Should().BeTrue(); |
||||
result.IfSome( |
||||
filter => |
||||
{ |
||||
filter.ComplexFilter.Should().Be( |
||||
$"[0:1]apad=whole_dur={duration.TotalMilliseconds}ms[a];[0:0]yadif=1[v]"); |
||||
filter.AudioLabel.Should().Be("[a]"); |
||||
filter.VideoLabel.Should().Be("[v]"); |
||||
}); |
||||
} |
||||
|
||||
[Test] |
||||
[TestCase(true, false, false, "[0:0]yadif=1[v]", "[v]")]
|
||||
[TestCase(true, true, false, "[0:0]yadif=1,scale=1920:1000:flags=fast_bilinear,setsar=1[v]", "[v]")]
|
||||
[TestCase(true, false, true, "[0:0]yadif=1,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", "[v]")]
|
||||
[TestCase( |
||||
true, |
||||
true, |
||||
true, |
||||
"[0:0]yadif=1,scale=1920:1000:flags=fast_bilinear,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", |
||||
"[v]")] |
||||
[TestCase(false, true, false, "[0:0]scale=1920:1000:flags=fast_bilinear,setsar=1[v]", "[v]")]
|
||||
[TestCase(false, false, true, "[0:0]setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", "[v]")]
|
||||
[TestCase( |
||||
false, |
||||
true, |
||||
true, |
||||
"[0:0]scale=1920:1000:flags=fast_bilinear,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", |
||||
"[v]")] |
||||
public void Should_Return_Software_Video_Filter( |
||||
bool deinterlace, |
||||
bool scale, |
||||
bool pad, |
||||
string expectedVideoFilter, |
||||
string expectedVideoLabel) |
||||
{ |
||||
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() |
||||
.WithDeinterlace(deinterlace); |
||||
|
||||
if (scale) |
||||
{ |
||||
builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 }); |
||||
} |
||||
|
||||
if (pad) |
||||
{ |
||||
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); |
||||
} |
||||
|
||||
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false); |
||||
|
||||
result.IsSome.Should().BeTrue(); |
||||
result.IfSome( |
||||
filter => |
||||
{ |
||||
filter.ComplexFilter.Should().Be(expectedVideoFilter); |
||||
filter.AudioLabel.Should().Be("0:1"); |
||||
filter.VideoLabel.Should().Be(expectedVideoLabel); |
||||
}); |
||||
} |
||||
|
||||
[Test] |
||||
[TestCase( |
||||
false, |
||||
false, |
||||
false, |
||||
WatermarkLocation.BottomLeft, |
||||
false, |
||||
100, |
||||
"[0:0][1:v]overlay=x=134:y=H-h-54[v]", |
||||
"0:1", |
||||
"[v]")] |
||||
[TestCase( |
||||
false, |
||||
false, |
||||
false, |
||||
WatermarkLocation.BottomRight, |
||||
false, |
||||
100, |
||||
"[0:0][1:v]overlay=x=W-w-134:y=H-h-54[v]", |
||||
"0:1", |
||||
"[v]")] |
||||
[TestCase( |
||||
false, |
||||
false, |
||||
false, |
||||
WatermarkLocation.TopLeft, |
||||
false, |
||||
100, |
||||
"[0:0][1:v]overlay=x=134:y=54[v]", |
||||
"0:1", |
||||
"[v]")] |
||||
[TestCase( |
||||
false, |
||||
false, |
||||
false, |
||||
WatermarkLocation.TopRight, |
||||
false, |
||||
100, |
||||
"[0:0][1:v]overlay=x=W-w-134:y=54[v]", |
||||
"0:1", |
||||
"[v]")] |
||||
[TestCase( |
||||
false, |
||||
false, |
||||
true, |
||||
WatermarkLocation.TopLeft, |
||||
false, |
||||
100, |
||||
"[1:v]format=yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)'[wmp];[0:0][wmp]overlay=x=134:y=54,format=nv12[v]", |
||||
"0:1", |
||||
"[v]")] |
||||
[TestCase( |
||||
false, |
||||
false, |
||||
false, |
||||
WatermarkLocation.TopLeft, |
||||
true, |
||||
100, |
||||
"[1:v]scale=384:-1[wmp];[0:0][wmp]overlay=x=134:y=54[v]", |
||||
"0:1", |
||||
"[v]")] |
||||
[TestCase( |
||||
false, |
||||
false, |
||||
false, |
||||
WatermarkLocation.TopLeft, |
||||
false, |
||||
90, |
||||
"[1:v]format=yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8,colorchannelmixer=aa=0.90[wmp];[0:0][wmp]overlay=x=134:y=54[v]", |
||||
"0:1", |
||||
"[v]")] |
||||
[TestCase( |
||||
false, |
||||
true, |
||||
false, |
||||
WatermarkLocation.TopLeft, |
||||
false, |
||||
100, |
||||
"[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]", |
||||
"0:1", |
||||
"[v]")] |
||||
[TestCase( |
||||
false, |
||||
true, |
||||
false, |
||||
WatermarkLocation.TopLeft, |
||||
true, |
||||
100, |
||||
"[0:0]yadif=1[vt];[1:v]scale=384:-1[wmp];[vt][wmp]overlay=x=134:y=54[v]", |
||||
"0:1", |
||||
"[v]")] |
||||
[TestCase( |
||||
true, |
||||
true, |
||||
false, |
||||
WatermarkLocation.TopLeft, |
||||
false, |
||||
100, |
||||
"[0:1]apad=whole_dur=3300000ms[a];[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]", |
||||
"[a]", |
||||
"[v]")] |
||||
[TestCase( |
||||
true, |
||||
false, |
||||
false, |
||||
WatermarkLocation.TopLeft, |
||||
false, |
||||
100, |
||||
"[0:1]apad=whole_dur=3300000ms[a];[0:0][1:v]overlay=x=134:y=54[v]", |
||||
"[a]", |
||||
"[v]")] |
||||
public void Should_Return_Watermark( |
||||
bool alignAudio, |
||||
bool deinterlace, |
||||
bool intermittent, |
||||
WatermarkLocation location, |
||||
bool scaled, |
||||
int opacity, |
||||
string expectedVideoFilter, |
||||
string expectedAudioLabel, |
||||
string expectedVideoLabel) |
||||
{ |
||||
var watermark = new ChannelWatermark |
||||
{ |
||||
Mode = intermittent |
||||
? ChannelWatermarkMode.Intermittent |
||||
: ChannelWatermarkMode.Permanent, |
||||
DurationSeconds = intermittent ? 15 : 0, |
||||
FrequencyMinutes = intermittent ? 10 : 0, |
||||
Location = location, |
||||
Size = scaled ? WatermarkSize.Scaled : WatermarkSize.ActualSize, |
||||
WidthPercent = scaled ? 20 : 0, |
||||
Opacity = opacity, |
||||
HorizontalMarginPercent = 7, |
||||
VerticalMarginPercent = 5 |
||||
}; |
||||
|
||||
Option<List<FadePoint>> maybeFadePoints = watermark.Mode == ChannelWatermarkMode.Intermittent |
||||
? Some( |
||||
WatermarkCalculator.CalculateFadePoints( |
||||
new DateTimeOffset(2022, 01, 31, 12, 25, 0, TimeSpan.FromHours(-5)), |
||||
TimeSpan.Zero, |
||||
TimeSpan.FromMinutes(55), |
||||
TimeSpan.Zero, |
||||
watermark.FrequencyMinutes, |
||||
watermark.DurationSeconds)) |
||||
: None; |
||||
|
||||
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() |
||||
.WithWatermark( |
||||
Some(watermark), |
||||
maybeFadePoints, |
||||
new Resolution { Width = 1920, Height = 1080 }, |
||||
None) |
||||
.WithDeinterlace(deinterlace) |
||||
.WithAlignedAudio(alignAudio ? Some(TimeSpan.FromMinutes(55)) : None); |
||||
|
||||
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false); |
||||
|
||||
result.IsSome.Should().BeTrue(); |
||||
result.IfSome( |
||||
filter => |
||||
{ |
||||
filter.ComplexFilter.Should().Be(expectedVideoFilter); |
||||
filter.AudioLabel.Should().Be(expectedAudioLabel); |
||||
filter.VideoLabel.Should().Be(expectedVideoLabel); |
||||
}); |
||||
} |
||||
|
||||
[Test] |
||||
[TestCase( |
||||
false, |
||||
false, |
||||
false, |
||||
WatermarkLocation.BottomLeft, |
||||
false, |
||||
100, |
||||
"[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=H-h-54[v]", |
||||
"0:1", |
||||
"[v]", |
||||
false)] |
||||
[TestCase( |
||||
false, |
||||
false, |
||||
false, |
||||
WatermarkLocation.BottomLeft, |
||||
false, |
||||
100, |
||||
"[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=H-h-54,hwupload[v]", |
||||
"0:1", |
||||
"[v]", |
||||
true)] |
||||
[TestCase( |
||||
false, |
||||
false, |
||||
true, |
||||
WatermarkLocation.TopLeft, |
||||
false, |
||||
100, |
||||
"[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)',hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]", |
||||
"0:1", |
||||
"[v]", |
||||
false)] |
||||
[TestCase( |
||||
false, |
||||
false, |
||||
true, |
||||
WatermarkLocation.TopLeft, |
||||
false, |
||||
100, |
||||
"[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)',hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]", |
||||
"0:1", |
||||
"[v]", |
||||
true)] |
||||
[TestCase( |
||||
false, |
||||
false, |
||||
false, |
||||
WatermarkLocation.TopLeft, |
||||
true, |
||||
100, |
||||
"[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,scale=384:-1,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]", |
||||
"0:1", |
||||
"[v]", |
||||
false)] |
||||
[TestCase( |
||||
false, |
||||
false, |
||||
false, |
||||
WatermarkLocation.TopLeft, |
||||
true, |
||||
100, |
||||
"[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,scale=384:-1,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]", |
||||
"0:1", |
||||
"[v]", |
||||
true)] |
||||
[TestCase( |
||||
false, |
||||
false, |
||||
false, |
||||
WatermarkLocation.TopLeft, |
||||
false, |
||||
90, |
||||
"[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,colorchannelmixer=aa=0.90,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]", |
||||
"0:1", |
||||
"[v]", |
||||
false)] |
||||
[TestCase( |
||||
false, |
||||
false, |
||||
false, |
||||
WatermarkLocation.TopLeft, |
||||
false, |
||||
90, |
||||
"[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,colorchannelmixer=aa=0.90,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]", |
||||
"0:1", |
||||
"[v]", |
||||
true)] |
||||
// TODO: do we need these anymore? interlaced content that isn't handled by mpeg2_cuvid?
|
||||
// [TestCase(
|
||||
// false,
|
||||
// true,
|
||||
// false,
|
||||
// WatermarkLocation.TopLeft,
|
||||
// false,
|
||||
// 100,
|
||||
// "[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]",
|
||||
// "0:1",
|
||||
// "[v]")]
|
||||
// [TestCase(
|
||||
// false,
|
||||
// true,
|
||||
// false,
|
||||
// WatermarkLocation.TopLeft,
|
||||
// true,
|
||||
// 100,
|
||||
// "[0:0]yadif=1[vt];[1:v]scale=384:-1[wmp];[vt][wmp]overlay=x=134:y=54[v]",
|
||||
// "0:1",
|
||||
// "[v]")]
|
||||
// [TestCase(
|
||||
// true,
|
||||
// true,
|
||||
// false,
|
||||
// WatermarkLocation.TopLeft,
|
||||
// false,
|
||||
// 100,
|
||||
// "[0:1]apad=whole_dur=3300000ms[a];[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]",
|
||||
// "[a]",
|
||||
// "[v]")]
|
||||
[TestCase( |
||||
true, |
||||
false, |
||||
false, |
||||
WatermarkLocation.TopLeft, |
||||
false, |
||||
100, |
||||
"[0:1]apad=whole_dur=3300000ms[a];[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]", |
||||
"[a]", |
||||
"[v]", |
||||
false)] |
||||
[TestCase( |
||||
true, |
||||
false, |
||||
false, |
||||
WatermarkLocation.TopLeft, |
||||
false, |
||||
100, |
||||
"[0:1]apad=whole_dur=3300000ms[a];[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]", |
||||
"[a]", |
||||
"[v]", |
||||
true)] |
||||
public void Should_Return_NVENC_Watermark( |
||||
bool alignAudio, |
||||
bool deinterlace, |
||||
bool intermittent, |
||||
WatermarkLocation location, |
||||
bool scaled, |
||||
int opacity, |
||||
string expectedVideoFilter, |
||||
string expectedAudioLabel, |
||||
string expectedVideoLabel, |
||||
bool scaledSource) |
||||
{ |
||||
var watermark = new ChannelWatermark |
||||
{ |
||||
Mode = intermittent |
||||
? ChannelWatermarkMode.Intermittent |
||||
: ChannelWatermarkMode.Permanent, |
||||
DurationSeconds = intermittent ? 15 : 0, |
||||
FrequencyMinutes = intermittent ? 10 : 0, |
||||
Location = location, |
||||
Size = scaled ? WatermarkSize.Scaled : WatermarkSize.ActualSize, |
||||
WidthPercent = scaled ? 20 : 0, |
||||
Opacity = opacity, |
||||
HorizontalMarginPercent = 7, |
||||
VerticalMarginPercent = 5 |
||||
}; |
||||
|
||||
Option<List<FadePoint>> maybeFadePoints = watermark.Mode == ChannelWatermarkMode.Intermittent |
||||
? Some( |
||||
WatermarkCalculator.CalculateFadePoints( |
||||
new DateTimeOffset(2022, 01, 31, 12, 25, 0, TimeSpan.FromHours(-5)), |
||||
TimeSpan.Zero, |
||||
TimeSpan.FromMinutes(55), |
||||
TimeSpan.Zero, |
||||
watermark.FrequencyMinutes, |
||||
watermark.DurationSeconds)) |
||||
: None; |
||||
|
||||
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() |
||||
.WithHardwareAcceleration(HardwareAccelerationKind.Nvenc) |
||||
.WithWatermark( |
||||
Some(watermark), |
||||
maybeFadePoints, |
||||
new Resolution { Width = 1920, Height = 1080 }, |
||||
None) |
||||
.WithDeinterlace(deinterlace) |
||||
.WithAlignedAudio(alignAudio ? Some(TimeSpan.FromMinutes(55)) : None); |
||||
|
||||
if (scaledSource) |
||||
{ |
||||
builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1080 }); |
||||
} |
||||
|
||||
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false); |
||||
|
||||
result.IsSome.Should().BeTrue(); |
||||
result.IfSome( |
||||
filter => |
||||
{ |
||||
filter.ComplexFilter.Should().Be(expectedVideoFilter); |
||||
filter.AudioLabel.Should().Be(expectedAudioLabel); |
||||
filter.VideoLabel.Should().Be(expectedVideoLabel); |
||||
}); |
||||
} |
||||
|
||||
[Test] |
||||
[TestCase(true, false, false, "[0:0]deinterlace_qsv[v]", "[v]")]
|
||||
[TestCase( |
||||
true, |
||||
true, |
||||
false, |
||||
"[0:0]deinterlace_qsv,scale_qsv=w=1920:h=1000,setsar=1[v]", |
||||
"[v]")] |
||||
[TestCase( |
||||
true, |
||||
false, |
||||
true, |
||||
"[0:0]deinterlace_qsv,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]", |
||||
"[v]")] |
||||
[TestCase( |
||||
true, |
||||
true, |
||||
true, |
||||
"[0:0]deinterlace_qsv,scale_qsv=w=1920:h=1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]", |
||||
"[v]")] |
||||
[TestCase( |
||||
false, |
||||
true, |
||||
false, |
||||
"[0:0]scale_qsv=w=1920:h=1000,setsar=1[v]", |
||||
"[v]")] |
||||
[TestCase( |
||||
false, |
||||
false, |
||||
true, |
||||
"[0:0]setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]", |
||||
"[v]")] |
||||
[TestCase( |
||||
false, |
||||
true, |
||||
true, |
||||
"[0:0]scale_qsv=w=1920:h=1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]", |
||||
"[v]")] |
||||
public void Should_Return_QSV_Video_Filter( |
||||
bool deinterlace, |
||||
bool scale, |
||||
bool pad, |
||||
string expectedVideoFilter, |
||||
string expectedVideoLabel) |
||||
{ |
||||
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() |
||||
.WithHardwareAcceleration(HardwareAccelerationKind.Qsv) |
||||
.WithDeinterlace(deinterlace); |
||||
|
||||
if (scale) |
||||
{ |
||||
builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 }); |
||||
} |
||||
|
||||
if (pad) |
||||
{ |
||||
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); |
||||
} |
||||
|
||||
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false); |
||||
|
||||
result.IsSome.Should().BeTrue(); |
||||
result.IfSome( |
||||
filter => |
||||
{ |
||||
filter.ComplexFilter.Should().Be(expectedVideoFilter); |
||||
filter.AudioLabel.Should().Be("0:1"); |
||||
filter.VideoLabel.Should().Be(expectedVideoLabel); |
||||
}); |
||||
} |
||||
|
||||
[Test] |
||||
[TestCase(true, false, false, "[0:0]yadif_cuda[v]", "[v]")]
|
||||
[TestCase( |
||||
true, |
||||
true, |
||||
false, |
||||
"[0:0]yadif_cuda,scale_cuda=1920:1000,setsar=1[v]", |
||||
"[v]")] |
||||
[TestCase( |
||||
true, |
||||
false, |
||||
true, |
||||
"[0:0]yadif_cuda,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", |
||||
"[v]")] |
||||
[TestCase( |
||||
true, |
||||
true, |
||||
true, |
||||
"[0:0]yadif_cuda,scale_cuda=1920:1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", |
||||
"[v]")] |
||||
[TestCase( |
||||
false, |
||||
true, |
||||
false, |
||||
"[0:0]scale_cuda=1920:1000,setsar=1[v]", |
||||
"[v]")] |
||||
[TestCase( |
||||
false, |
||||
false, |
||||
true, |
||||
"[0:0]setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", |
||||
"[v]")] |
||||
[TestCase( |
||||
false, |
||||
true, |
||||
true, |
||||
"[0:0]scale_cuda=1920:1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", |
||||
"[v]")] |
||||
public void Should_Return_NVENC_Video_Filter( |
||||
bool deinterlace, |
||||
bool scale, |
||||
bool pad, |
||||
string expectedVideoFilter, |
||||
string expectedVideoLabel) |
||||
{ |
||||
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() |
||||
.WithHardwareAcceleration(HardwareAccelerationKind.Nvenc) |
||||
.WithDeinterlace(deinterlace) |
||||
.WithInputPixelFormat("h264"); |
||||
|
||||
if (scale) |
||||
{ |
||||
builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 }); |
||||
} |
||||
|
||||
if (pad) |
||||
{ |
||||
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); |
||||
} |
||||
|
||||
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false); |
||||
|
||||
result.IsSome.Should().BeTrue(); |
||||
result.IfSome( |
||||
filter => |
||||
{ |
||||
filter.ComplexFilter.Should().Be(expectedVideoFilter); |
||||
filter.AudioLabel.Should().Be("0:1"); |
||||
filter.VideoLabel.Should().Be(expectedVideoLabel); |
||||
}); |
||||
} |
||||
|
||||
[Test] |
||||
[TestCase("h264", true, false, false, "[0:0]deinterlace_vaapi[v]", "[v]")]
|
||||
[TestCase( |
||||
"h264", |
||||
true, |
||||
true, |
||||
false, |
||||
"[0:0]deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]", |
||||
"[v]")] |
||||
[TestCase( |
||||
"h264", |
||||
true, |
||||
false, |
||||
true, |
||||
"[0:0]deinterlace_vaapi,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", |
||||
"[v]")] |
||||
[TestCase( |
||||
"h264", |
||||
true, |
||||
true, |
||||
true, |
||||
"[0:0]deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", |
||||
"[v]")] |
||||
[TestCase( |
||||
"h264", |
||||
false, |
||||
true, |
||||
false, |
||||
"[0:0]scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]", |
||||
"[v]")] |
||||
[TestCase( |
||||
"h264", |
||||
false, |
||||
false, |
||||
true, |
||||
"[0:0]setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", |
||||
"[v]")] |
||||
[TestCase( |
||||
"h264", |
||||
false, |
||||
true, |
||||
true, |
||||
"[0:0]scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", |
||||
"[v]")] |
||||
[TestCase("mpeg4", true, false, false, "[0:0]hwupload,deinterlace_vaapi[v]", "[v]")]
|
||||
[TestCase( |
||||
"mpeg4", |
||||
true, |
||||
true, |
||||
false, |
||||
"[0:0]hwupload,deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]", |
||||
"[v]")] |
||||
[TestCase( |
||||
"mpeg4", |
||||
true, |
||||
false, |
||||
true, |
||||
"[0:0]hwupload,deinterlace_vaapi,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", |
||||
"[v]")] |
||||
[TestCase( |
||||
"mpeg4", |
||||
true, |
||||
true, |
||||
true, |
||||
"[0:0]hwupload,deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", |
||||
"[v]")] |
||||
[TestCase( |
||||
"mpeg4", |
||||
false, |
||||
true, |
||||
false, |
||||
"[0:0]hwupload,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]", |
||||
"[v]")] |
||||
[TestCase( |
||||
"mpeg4", |
||||
false, |
||||
false, |
||||
true, |
||||
"[0:0]setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", |
||||
"[v]")] |
||||
[TestCase( |
||||
"mpeg4", |
||||
false, |
||||
true, |
||||
true, |
||||
"[0:0]hwupload,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", |
||||
"[v]")] |
||||
public void Should_Return_VAAPI_Video_Filter( |
||||
string codec, |
||||
bool deinterlace, |
||||
bool scale, |
||||
bool pad, |
||||
string expectedVideoFilter, |
||||
string expectedVideoLabel) |
||||
{ |
||||
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() |
||||
.WithHardwareAcceleration(HardwareAccelerationKind.Vaapi) |
||||
.WithInputCodec(codec) |
||||
.WithDeinterlace(deinterlace); |
||||
|
||||
if (scale) |
||||
{ |
||||
builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 }); |
||||
} |
||||
|
||||
if (pad) |
||||
{ |
||||
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); |
||||
} |
||||
|
||||
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false); |
||||
|
||||
result.IsSome.Should().BeTrue(); |
||||
result.IfSome( |
||||
filter => |
||||
{ |
||||
filter.ComplexFilter.Should().Be(expectedVideoFilter); |
||||
filter.AudioLabel.Should().Be("0:1"); |
||||
filter.VideoLabel.Should().Be(expectedVideoLabel); |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
@ -1,8 +0,0 @@
@@ -1,8 +0,0 @@
|
||||
using ErsatzTV.Core.Metadata.Nfo; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata.Nfo; |
||||
|
||||
public interface IArtistNfoReader |
||||
{ |
||||
Task<Either<BaseError, ArtistNfo>> ReadFromFile(string fileName); |
||||
} |
||||
@ -1,8 +0,0 @@
@@ -1,8 +0,0 @@
|
||||
using ErsatzTV.Core.Metadata.Nfo; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata.Nfo; |
||||
|
||||
public interface IEpisodeNfoReader |
||||
{ |
||||
Task<Either<BaseError, List<TvShowEpisodeNfo>>> ReadFromFile(string fileName); |
||||
} |
||||
@ -1,8 +0,0 @@
@@ -1,8 +0,0 @@
|
||||
using ErsatzTV.Core.Metadata.Nfo; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata.Nfo; |
||||
|
||||
public interface IMovieNfoReader |
||||
{ |
||||
Task<Either<BaseError, MovieNfo>> ReadFromFile(string fileName); |
||||
} |
||||
@ -1,8 +0,0 @@
@@ -1,8 +0,0 @@
|
||||
using ErsatzTV.Core.Metadata.Nfo; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata.Nfo; |
||||
|
||||
public interface IMusicVideoNfoReader |
||||
{ |
||||
Task<Either<BaseError, MusicVideoNfo>> ReadFromFile(string fileName); |
||||
} |
||||
@ -1,8 +0,0 @@
@@ -1,8 +0,0 @@
|
||||
using ErsatzTV.Core.Metadata.Nfo; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata.Nfo; |
||||
|
||||
public interface IOtherVideoNfoReader |
||||
{ |
||||
Task<Either<BaseError, OtherVideoNfo>> ReadFromFile(string fileName); |
||||
} |
||||
@ -1,8 +0,0 @@
@@ -1,8 +0,0 @@
|
||||
using ErsatzTV.Core.Metadata.Nfo; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata.Nfo; |
||||
|
||||
public interface ITvShowNfoReader |
||||
{ |
||||
Task<Either<BaseError, TvShowNfo>> ReadFromFile(string fileName); |
||||
} |
||||
@ -1,18 +0,0 @@
@@ -1,18 +0,0 @@
|
||||
using System.Xml.Serialization; |
||||
|
||||
namespace ErsatzTV.Core.Metadata.Nfo; |
||||
|
||||
public class ActorNfo |
||||
{ |
||||
[XmlElement("name")] |
||||
public string Name { get; set; } |
||||
|
||||
[XmlElement("role")] |
||||
public string Role { get; set; } |
||||
|
||||
[XmlElement("order")] |
||||
public int? Order { get; set; } |
||||
|
||||
[XmlElement("thumb")] |
||||
public string Thumb { get; set; } |
||||
} |
||||
@ -1,25 +0,0 @@
@@ -1,25 +0,0 @@
|
||||
using System.Xml.Serialization; |
||||
|
||||
namespace ErsatzTV.Core.Metadata.Nfo; |
||||
|
||||
[XmlRoot("artist")] |
||||
public class ArtistNfo |
||||
{ |
||||
[XmlElement("name")] |
||||
public string Name { get; set; } |
||||
|
||||
[XmlElement("disambiguation")] |
||||
public string Disambiguation { get; set; } |
||||
|
||||
[XmlElement("genre")] |
||||
public List<string> Genres { get; set; } |
||||
|
||||
[XmlElement("style")] |
||||
public List<string> Styles { get; set; } |
||||
|
||||
[XmlElement("mood")] |
||||
public List<string> Moods { get; set; } |
||||
|
||||
[XmlElement("biography")] |
||||
public string Biography { get; set; } |
||||
} |
||||
@ -1,52 +0,0 @@
@@ -1,52 +0,0 @@
|
||||
using System.Xml.Serialization; |
||||
|
||||
namespace ErsatzTV.Core.Metadata.Nfo; |
||||
|
||||
[XmlRoot("movie")] |
||||
public class MovieNfo |
||||
{ |
||||
[XmlElement("title")] |
||||
public string Title { get; set; } |
||||
|
||||
[XmlElement("sorttitle")] |
||||
public string SortTitle { get; set; } |
||||
|
||||
[XmlElement("outline")] |
||||
public string Outline { get; set; } |
||||
|
||||
[XmlElement("year")] |
||||
public int Year { get; set; } |
||||
|
||||
[XmlElement("mpaa")] |
||||
public string ContentRating { get; set; } |
||||
|
||||
[XmlElement("premiered")] |
||||
public Option<DateTime> Premiered { get; set; } |
||||
|
||||
[XmlElement("plot")] |
||||
public string Plot { get; set; } |
||||
|
||||
[XmlElement("tagline")] |
||||
public string Tagline { get; set; } |
||||
|
||||
[XmlElement("genre")] |
||||
public List<string> Genres { get; set; } |
||||
|
||||
[XmlElement("tag")] |
||||
public List<string> Tags { get; set; } |
||||
|
||||
[XmlElement("studio")] |
||||
public List<string> Studios { get; set; } |
||||
|
||||
[XmlElement("actor")] |
||||
public List<ActorNfo> Actors { get; set; } |
||||
|
||||
[XmlElement("credits")] |
||||
public List<string> Writers { get; set; } |
||||
|
||||
[XmlElement("director")] |
||||
public List<string> Directors { get; set; } |
||||
|
||||
[XmlElement("uniqueid")] |
||||
public List<UniqueIdNfo> UniqueIds { get; set; } |
||||
} |
||||
@ -1,37 +0,0 @@
@@ -1,37 +0,0 @@
|
||||
using System.Xml.Serialization; |
||||
|
||||
namespace ErsatzTV.Core.Metadata.Nfo; |
||||
|
||||
[XmlRoot("musicvideo")] |
||||
public class MusicVideoNfo |
||||
{ |
||||
[XmlElement("artist")] |
||||
public List<string> Artists { get; set; } |
||||
|
||||
[XmlElement("title")] |
||||
public string Title { get; set; } |
||||
|
||||
[XmlElement("album")] |
||||
public string Album { get; set; } |
||||
|
||||
[XmlElement("plot")] |
||||
public string Plot { get; set; } |
||||
|
||||
[XmlElement("track")] |
||||
public int Track { get; set; } |
||||
|
||||
[XmlElement("aired")] |
||||
public Option<DateTime> Aired { get; set; } |
||||
|
||||
[XmlElement("year")] |
||||
public int Year { get; set; } |
||||
|
||||
[XmlElement("genre")] |
||||
public List<string> Genres { get; set; } |
||||
|
||||
[XmlElement("tag")] |
||||
public List<string> Tags { get; set; } |
||||
|
||||
[XmlElement("studio")] |
||||
public List<string> Studios { get; set; } |
||||
} |
||||
@ -1,52 +0,0 @@
@@ -1,52 +0,0 @@
|
||||
using System.Xml.Serialization; |
||||
|
||||
namespace ErsatzTV.Core.Metadata.Nfo; |
||||
|
||||
[XmlRoot("movie")] |
||||
public class OtherVideoNfo |
||||
{ |
||||
[XmlElement("title")] |
||||
public string Title { get; set; } |
||||
|
||||
[XmlElement("sorttitle")] |
||||
public string SortTitle { get; set; } |
||||
|
||||
[XmlElement("outline")] |
||||
public string Outline { get; set; } |
||||
|
||||
[XmlElement("year")] |
||||
public int Year { get; set; } |
||||
|
||||
[XmlElement("mpaa")] |
||||
public string ContentRating { get; set; } |
||||
|
||||
[XmlElement("premiered")] |
||||
public Option<DateTime> Premiered { get; set; } |
||||
|
||||
[XmlElement("plot")] |
||||
public string Plot { get; set; } |
||||
|
||||
[XmlElement("tagline")] |
||||
public string Tagline { get; set; } |
||||
|
||||
[XmlElement("genre")] |
||||
public List<string> Genres { get; set; } |
||||
|
||||
[XmlElement("tag")] |
||||
public List<string> Tags { get; set; } |
||||
|
||||
[XmlElement("studio")] |
||||
public List<string> Studios { get; set; } |
||||
|
||||
[XmlElement("actor")] |
||||
public List<ActorNfo> Actors { get; set; } |
||||
|
||||
[XmlElement("credits")] |
||||
public List<string> Writers { get; set; } |
||||
|
||||
[XmlElement("director")] |
||||
public List<string> Directors { get; set; } |
||||
|
||||
[XmlElement("uniqueid")] |
||||
public List<UniqueIdNfo> UniqueIds { get; set; } |
||||
} |
||||
@ -1,40 +0,0 @@
@@ -1,40 +0,0 @@
|
||||
using System.Xml.Serialization; |
||||
|
||||
namespace ErsatzTV.Core.Metadata.Nfo; |
||||
|
||||
[XmlRoot("episodedetails")] |
||||
public class TvShowEpisodeNfo |
||||
{ |
||||
[XmlElement("showtitle")] |
||||
public string ShowTitle { get; set; } |
||||
|
||||
[XmlElement("title")] |
||||
public string Title { get; set; } |
||||
|
||||
[XmlElement("episode")] |
||||
public int Episode { get; set; } |
||||
|
||||
[XmlElement("season")] |
||||
public int Season { get; set; } |
||||
|
||||
[XmlElement("mpaa")] |
||||
public string ContentRating { get; set; } |
||||
|
||||
[XmlElement("aired")] |
||||
public Option<DateTime> Aired { get; set; } |
||||
|
||||
[XmlElement("plot")] |
||||
public string Plot { get; set; } |
||||
|
||||
[XmlElement("actor")] |
||||
public List<ActorNfo> Actors { get; set; } |
||||
|
||||
[XmlElement("credits")] |
||||
public List<string> Writers { get; set; } |
||||
|
||||
[XmlElement("director")] |
||||
public List<string> Directors { get; set; } |
||||
|
||||
[XmlElement("uniqueid")] |
||||
public List<UniqueIdNfo> UniqueIds { get; set; } |
||||
} |
||||
@ -1,50 +0,0 @@
@@ -1,50 +0,0 @@
|
||||
using System.Xml.Serialization; |
||||
|
||||
namespace ErsatzTV.Core.Metadata.Nfo; |
||||
|
||||
[XmlRoot("tvshow")] |
||||
public class TvShowNfo |
||||
{ |
||||
[XmlElement("title")] |
||||
public string Title { get; set; } |
||||
|
||||
[XmlIgnore] |
||||
public int? Year { get; set; } |
||||
|
||||
[XmlElement("year")] |
||||
public string YearAsText |
||||
{ |
||||
get => Year.HasValue ? Year.ToString() : null; |
||||
set => Year = !string.IsNullOrWhiteSpace(value) ? int.Parse(value) : default(int?); |
||||
} |
||||
|
||||
[XmlElement("plot")] |
||||
public string Plot { get; set; } |
||||
|
||||
[XmlElement("outline")] |
||||
public string Outline { get; set; } |
||||
|
||||
[XmlElement("tagline")] |
||||
public string Tagline { get; set; } |
||||
|
||||
[XmlElement("mpaa")] |
||||
public string ContentRating { get; set; } |
||||
|
||||
[XmlElement("premiered")] |
||||
public Option<DateTime> Premiered { get; set; } |
||||
|
||||
[XmlElement("genre")] |
||||
public List<string> Genres { get; set; } |
||||
|
||||
[XmlElement("tag")] |
||||
public List<string> Tags { get; set; } |
||||
|
||||
[XmlElement("studio")] |
||||
public List<string> Studios { get; set; } |
||||
|
||||
[XmlElement("actor")] |
||||
public List<ActorNfo> Actors { get; set; } |
||||
|
||||
[XmlElement("uniqueid")] |
||||
public List<UniqueIdNfo> UniqueIds { get; set; } |
||||
} |
||||
@ -1,15 +0,0 @@
@@ -1,15 +0,0 @@
|
||||
using System.Xml.Serialization; |
||||
|
||||
namespace ErsatzTV.Core.Metadata.Nfo; |
||||
|
||||
public class UniqueIdNfo |
||||
{ |
||||
[XmlAttribute("default")] |
||||
public bool Default { get; set; } |
||||
|
||||
[XmlAttribute("type")] |
||||
public string Type { get; set; } |
||||
|
||||
[XmlText] |
||||
public string Guid { get; set; } |
||||
} |
||||
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
namespace ErsatzTV.Core.Metadata; |
||||
|
||||
public static class SortTitle |
||||
{ |
||||
public static string GetSortTitle(string title) |
||||
{ |
||||
if (string.IsNullOrWhiteSpace(title)) |
||||
{ |
||||
return title; |
||||
} |
||||
|
||||
if (title.StartsWith("the ", StringComparison.OrdinalIgnoreCase)) |
||||
{ |
||||
return title[4..]; |
||||
} |
||||
|
||||
if (title.StartsWith("a ", StringComparison.OrdinalIgnoreCase)) |
||||
{ |
||||
return title[2..]; |
||||
} |
||||
|
||||
if (title.StartsWith("an ", StringComparison.OrdinalIgnoreCase)) |
||||
{ |
||||
return title[3..]; |
||||
} |
||||
|
||||
if (title.StartsWith("Æ")) |
||||
{ |
||||
return title.Replace("Æ", "E"); |
||||
} |
||||
|
||||
return title; |
||||
} |
||||
} |
||||
@ -1,252 +0,0 @@
@@ -1,252 +0,0 @@
|
||||
using ErsatzTV.FFmpeg.Environment; |
||||
using ErsatzTV.FFmpeg.Pipeline; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Filter; |
||||
|
||||
public class NewComplexFilter : IPipelineStep |
||||
{ |
||||
private readonly Option<AudioInputFile> _maybeAudioInputFile; |
||||
private readonly Option<SubtitleInputFile> _maybeSubtitleInputFile; |
||||
private readonly PipelineContext _pipelineContext; |
||||
private readonly FilterChain _filterChain; |
||||
private readonly Option<VideoInputFile> _maybeVideoInputFile; |
||||
private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile; |
||||
private readonly List<string> _outputOptions; |
||||
private readonly IList<string> _arguments; |
||||
|
||||
public NewComplexFilter( |
||||
Option<VideoInputFile> maybeVideoInputFile, |
||||
Option<AudioInputFile> maybeAudioInputFile, |
||||
Option<WatermarkInputFile> maybeWatermarkInputFile, |
||||
Option<SubtitleInputFile> maybeSubtitleInputFile, |
||||
PipelineContext pipelineContext, |
||||
FilterChain filterChain) |
||||
{ |
||||
_maybeVideoInputFile = maybeVideoInputFile; |
||||
_maybeAudioInputFile = maybeAudioInputFile; |
||||
_maybeWatermarkInputFile = maybeWatermarkInputFile; |
||||
_maybeSubtitleInputFile = maybeSubtitleInputFile; |
||||
_pipelineContext = pipelineContext; |
||||
_filterChain = filterChain; |
||||
|
||||
_outputOptions = new List<string>(); |
||||
|
||||
_arguments = Arguments(); |
||||
} |
||||
|
||||
public IList<EnvironmentVariable> EnvironmentVariables => Array.Empty<EnvironmentVariable>(); |
||||
public IList<string> GlobalOptions => Array.Empty<string>(); |
||||
public IList<string> InputOptions(InputFile inputFile) => Array.Empty<string>(); |
||||
public IList<string> FilterOptions => _arguments; |
||||
public IList<string> OutputOptions => _outputOptions; |
||||
|
||||
public FrameState NextState(FrameState currentState) => currentState; |
||||
|
||||
// for testing
|
||||
public FilterChain FilterChain => _filterChain; |
||||
|
||||
private List<string> Arguments() |
||||
{ |
||||
var audioLabel = "0:a"; |
||||
var videoLabel = "0:v"; |
||||
string? watermarkLabel = null; |
||||
string? subtitleLabel = null; |
||||
|
||||
var result = new List<string>(); |
||||
|
||||
string audioFilterComplex = string.Empty; |
||||
string videoFilterComplex = string.Empty; |
||||
string watermarkFilterComplex = string.Empty; |
||||
string watermarkOverlayFilterComplex = string.Empty; |
||||
string subtitleFilterComplex = string.Empty; |
||||
string subtitleOverlayFilterComplex = string.Empty; |
||||
string pixelFormatFilterComplex = string.Empty; |
||||
|
||||
var distinctPaths = new List<string>(); |
||||
foreach ((string path, _) in _maybeVideoInputFile) |
||||
{ |
||||
if (!distinctPaths.Contains(path)) |
||||
{ |
||||
distinctPaths.Add(path); |
||||
} |
||||
} |
||||
|
||||
foreach ((string path, _) in _maybeAudioInputFile) |
||||
{ |
||||
if (!distinctPaths.Contains(path) || |
||||
// use audio as a separate input with vaapi/qsv
|
||||
_pipelineContext.HardwareAccelerationMode is HardwareAccelerationMode.Vaapi |
||||
or HardwareAccelerationMode.Qsv) |
||||
{ |
||||
distinctPaths.Add(path); |
||||
} |
||||
} |
||||
|
||||
foreach ((string path, _) in _maybeWatermarkInputFile) |
||||
{ |
||||
if (!distinctPaths.Contains(path)) |
||||
{ |
||||
distinctPaths.Add(path); |
||||
} |
||||
} |
||||
|
||||
foreach ((string path, _) in _maybeSubtitleInputFile) |
||||
{ |
||||
if (!distinctPaths.Contains(path)) |
||||
{ |
||||
distinctPaths.Add(path); |
||||
} |
||||
} |
||||
|
||||
foreach (VideoInputFile videoInputFile in _maybeVideoInputFile) |
||||
{ |
||||
int inputIndex = distinctPaths.IndexOf(videoInputFile.Path); |
||||
foreach ((int index, _, _) in videoInputFile.Streams) |
||||
{ |
||||
videoLabel = $"{inputIndex}:{index}"; |
||||
if (_filterChain.VideoFilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) |
||||
{ |
||||
videoFilterComplex += $"[{inputIndex}:{index}]"; |
||||
videoFilterComplex += string.Join( |
||||
",", |
||||
_filterChain.VideoFilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); |
||||
videoLabel = "[v]"; |
||||
videoFilterComplex += videoLabel; |
||||
} |
||||
} |
||||
} |
||||
|
||||
foreach (WatermarkInputFile watermarkInputFile in _maybeWatermarkInputFile) |
||||
{ |
||||
int inputIndex = distinctPaths.IndexOf(watermarkInputFile.Path); |
||||
foreach ((int index, _, _) in watermarkInputFile.Streams) |
||||
{ |
||||
watermarkLabel = $"{inputIndex}:{index}"; |
||||
if (_filterChain.WatermarkFilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) |
||||
{ |
||||
watermarkFilterComplex += $"[{inputIndex}:{index}]"; |
||||
watermarkFilterComplex += string.Join( |
||||
",", |
||||
_filterChain.WatermarkFilterSteps.Select(f => f.Filter) |
||||
.Filter(s => !string.IsNullOrWhiteSpace(s))); |
||||
watermarkLabel = "[wm]"; |
||||
watermarkFilterComplex += watermarkLabel; |
||||
} |
||||
else |
||||
{ |
||||
watermarkLabel = $"[{watermarkLabel}]"; |
||||
} |
||||
} |
||||
} |
||||
|
||||
foreach (SubtitleInputFile subtitleInputFile in _maybeSubtitleInputFile.Filter(s => !s.Copy)) |
||||
{ |
||||
int inputIndex = distinctPaths.IndexOf(subtitleInputFile.Path); |
||||
foreach ((int index, _, _) in subtitleInputFile.Streams) |
||||
{ |
||||
subtitleLabel = $"{inputIndex}:{index}"; |
||||
if (_filterChain.SubtitleFilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) |
||||
{ |
||||
subtitleFilterComplex += $"[{inputIndex}:{index}]"; |
||||
subtitleFilterComplex += string.Join( |
||||
",", |
||||
_filterChain.SubtitleFilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); |
||||
subtitleLabel = "[st]"; |
||||
subtitleFilterComplex += subtitleLabel; |
||||
} |
||||
else |
||||
{ |
||||
subtitleLabel = $"[{subtitleLabel}]"; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// overlay subtitle
|
||||
if (!string.IsNullOrWhiteSpace(subtitleLabel) && _filterChain.SubtitleOverlayFilterSteps.Any()) |
||||
{ |
||||
subtitleOverlayFilterComplex += $"{ProperLabel(videoLabel)}{ProperLabel(subtitleLabel)}"; |
||||
subtitleOverlayFilterComplex += string.Join( |
||||
",", |
||||
_filterChain.SubtitleOverlayFilterSteps.Select(f => f.Filter) |
||||
.Filter(s => !string.IsNullOrWhiteSpace(s))); |
||||
videoLabel = "[vst]"; |
||||
subtitleOverlayFilterComplex += videoLabel; |
||||
} |
||||
|
||||
// overlay watermark
|
||||
if (!string.IsNullOrWhiteSpace(watermarkLabel) && _filterChain.WatermarkOverlayFilterSteps.Any()) |
||||
{ |
||||
watermarkOverlayFilterComplex += $"{ProperLabel(videoLabel)}{ProperLabel(watermarkLabel)}"; |
||||
watermarkOverlayFilterComplex += string.Join( |
||||
",", |
||||
_filterChain.WatermarkOverlayFilterSteps.Select(f => f.Filter) |
||||
.Filter(s => !string.IsNullOrWhiteSpace(s))); |
||||
videoLabel = "[vwm]"; |
||||
watermarkOverlayFilterComplex += videoLabel; |
||||
} |
||||
|
||||
// pixel format
|
||||
if (_filterChain.PixelFormatFilterSteps.Any()) |
||||
{ |
||||
pixelFormatFilterComplex += $"{ProperLabel(videoLabel)}"; |
||||
pixelFormatFilterComplex += string.Join( |
||||
",", |
||||
_filterChain.PixelFormatFilterSteps.Select(f => f.Filter) |
||||
.Filter(s => !string.IsNullOrWhiteSpace(s))); |
||||
videoLabel = "[vpf]"; |
||||
pixelFormatFilterComplex += videoLabel; |
||||
} |
||||
|
||||
foreach (AudioInputFile audioInputFile in _maybeAudioInputFile) |
||||
{ |
||||
int inputIndex = distinctPaths.LastIndexOf(audioInputFile.Path); |
||||
foreach ((int index, _, _) in audioInputFile.Streams) |
||||
{ |
||||
audioLabel = $"{inputIndex}:{index}"; |
||||
if (audioInputFile.FilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) |
||||
{ |
||||
audioFilterComplex += $"[{inputIndex}:{index}]"; |
||||
audioFilterComplex += string.Join( |
||||
",", |
||||
audioInputFile.FilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); |
||||
audioLabel = "[a]"; |
||||
audioFilterComplex += audioLabel; |
||||
} |
||||
} |
||||
} |
||||
|
||||
var filterComplex = string.Join( |
||||
";", |
||||
new[] |
||||
{ |
||||
audioFilterComplex, |
||||
videoFilterComplex, |
||||
subtitleFilterComplex, |
||||
watermarkFilterComplex, |
||||
subtitleOverlayFilterComplex, |
||||
watermarkOverlayFilterComplex, |
||||
pixelFormatFilterComplex |
||||
}.Where(s => !string.IsNullOrWhiteSpace(s))); |
||||
|
||||
if (!string.IsNullOrWhiteSpace(filterComplex)) |
||||
{ |
||||
result.AddRange(new[] { "-filter_complex", filterComplex }); |
||||
} |
||||
|
||||
result.AddRange(new[] { "-map", audioLabel, "-map", videoLabel }); |
||||
|
||||
foreach (SubtitleInputFile subtitleInputFile in _maybeSubtitleInputFile.Filter(s => s.Copy)) |
||||
{ |
||||
int inputIndex = distinctPaths.IndexOf(subtitleInputFile.Path); |
||||
foreach ((int index, _, _) in subtitleInputFile.Streams) |
||||
{ |
||||
subtitleLabel = $"{inputIndex}:{index}"; |
||||
result.AddRange(new[] { "-map", subtitleLabel }); |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
private string ProperLabel(string label) => label.StartsWith("[") ? label : $"[{label}]"; |
||||
} |
||||
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.FFmpeg.Option; |
||||
|
||||
public class MapAllStreamsOutputOption : OutputOption |
||||
{ |
||||
public override IList<string> OutputOptions => new[] { "-map", "0" }; |
||||
} |
||||
@ -1,14 +1,14 @@
@@ -1,14 +1,14 @@
|
||||
using System.Globalization; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.Metadata; |
||||
using ErsatzTV.Core.Tests.Fakes; |
||||
using ErsatzTV.Scanner.Core.Metadata; |
||||
using ErsatzTV.Scanner.Tests.Core.Fakes; |
||||
using FluentAssertions; |
||||
using Microsoft.Extensions.Logging; |
||||
using Moq; |
||||
using NUnit.Framework; |
||||
|
||||
namespace ErsatzTV.Core.Tests.Metadata; |
||||
namespace ErsatzTV.Scanner.Tests.Core.Metadata; |
||||
|
||||
[TestFixture] |
||||
public class LocalSubtitlesProviderTests |
||||
@ -1,13 +1,14 @@
@@ -1,13 +1,14 @@
|
||||
using System.Text; |
||||
using Bugsnag; |
||||
using ErsatzTV.Core.Metadata.Nfo; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Scanner.Core.Metadata.Nfo; |
||||
using FluentAssertions; |
||||
using Microsoft.Extensions.Logging.Abstractions; |
||||
using Microsoft.IO; |
||||
using Moq; |
||||
using NUnit.Framework; |
||||
|
||||
namespace ErsatzTV.Core.Tests.Metadata.Nfo; |
||||
namespace ErsatzTV.Scanner.Tests.Core.Metadata.Nfo; |
||||
|
||||
[TestFixture] |
||||
public class MusicVideoNfoReaderTests |
||||
@ -1,13 +1,14 @@
@@ -1,13 +1,14 @@
|
||||
using System.Text; |
||||
using Bugsnag; |
||||
using ErsatzTV.Core.Metadata.Nfo; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Scanner.Core.Metadata.Nfo; |
||||
using FluentAssertions; |
||||
using Microsoft.Extensions.Logging.Abstractions; |
||||
using Microsoft.IO; |
||||
using Moq; |
||||
using NUnit.Framework; |
||||
|
||||
namespace ErsatzTV.Core.Tests.Metadata.Nfo; |
||||
namespace ErsatzTV.Scanner.Tests.Core.Metadata.Nfo; |
||||
|
||||
[TestFixture] |
||||
public class OtherVideoNfoReaderTests |
||||
@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata; |
||||
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata; |
||||
|
||||
public interface ILocalMetadataProvider |
||||
{ |
||||
@ -1,6 +1,7 @@
@@ -1,6 +1,7 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata; |
||||
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata; |
||||
|
||||
public interface ILocalStatisticsProvider |
||||
{ |
||||
@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata; |
||||
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata; |
||||
|
||||
public interface ILocalSubtitlesProvider |
||||
{ |
||||
@ -1,6 +1,7 @@
@@ -1,6 +1,7 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata; |
||||
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata; |
||||
|
||||
public interface IMovieFolderScanner |
||||
{ |
||||
@ -1,6 +1,7 @@
@@ -1,6 +1,7 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata; |
||||
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata; |
||||
|
||||
public interface IMusicVideoFolderScanner |
||||
{ |
||||
@ -1,6 +1,7 @@
@@ -1,6 +1,7 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata; |
||||
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata; |
||||
|
||||
public interface IOtherVideoFolderScanner |
||||
{ |
||||
@ -1,6 +1,7 @@
@@ -1,6 +1,7 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata; |
||||
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata; |
||||
|
||||
public interface ISongFolderScanner |
||||
{ |
||||
@ -1,6 +1,7 @@
@@ -1,6 +1,7 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata; |
||||
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata; |
||||
|
||||
public interface ITelevisionFolderScanner |
||||
{ |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Scanner.Core.Metadata.Nfo; |
||||
|
||||
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; |
||||
|
||||
public interface IArtistNfoReader |
||||
{ |
||||
Task<Either<BaseError, ArtistNfo>> ReadFromFile(string fileName); |
||||
} |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Scanner.Core.Metadata.Nfo; |
||||
|
||||
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; |
||||
|
||||
public interface IEpisodeNfoReader |
||||
{ |
||||
Task<Either<BaseError, List<EpisodeNfo>>> ReadFromFile(string fileName); |
||||
} |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Scanner.Core.Metadata.Nfo; |
||||
|
||||
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; |
||||
|
||||
public interface IMovieNfoReader |
||||
{ |
||||
Task<Either<BaseError, MovieNfo>> ReadFromFile(string fileName); |
||||
} |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Scanner.Core.Metadata.Nfo; |
||||
|
||||
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; |
||||
|
||||
public interface IMusicVideoNfoReader |
||||
{ |
||||
Task<Either<BaseError, MusicVideoNfo>> ReadFromFile(string fileName); |
||||
} |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Scanner.Core.Metadata.Nfo; |
||||
|
||||
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; |
||||
|
||||
public interface IOtherVideoNfoReader |
||||
{ |
||||
Task<Either<BaseError, OtherVideoNfo>> ReadFromFile(string fileName); |
||||
} |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Scanner.Core.Metadata.Nfo; |
||||
|
||||
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata.Nfo; |
||||
|
||||
public interface IShowNfoReader |
||||
{ |
||||
Task<Either<BaseError, ShowNfo>> ReadFromFile(string fileName); |
||||
} |
||||
@ -1,13 +1,15 @@
@@ -1,13 +1,15 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.MediaServer; |
||||
using ErsatzTV.Core.Errors; |
||||
using ErsatzTV.Core.Interfaces.Metadata; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.MediaSources; |
||||
using MediatR; |
||||
using ErsatzTV.Core.Metadata; |
||||
using ErsatzTV.Scanner.Core.Interfaces.Metadata; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.Core.Metadata; |
||||
namespace ErsatzTV.Scanner.Core.Metadata; |
||||
|
||||
public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, TLibrary, TShow, TSeason, TEpisode, |
||||
TEtag> |
||||
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
namespace ErsatzTV.Core.Metadata; |
||||
namespace ErsatzTV.Scanner.Core.Metadata; |
||||
|
||||
public static class MetadataSongTag |
||||
{ |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Scanner.Core.Metadata.Nfo; |
||||
|
||||
public class ActorNfo |
||||
{ |
||||
public string? Name { get; set; } |
||||
public string? Role { get; set; } |
||||
public int? Order { get; set; } |
||||
public string? Thumb { get; set; } |
||||
} |
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
namespace ErsatzTV.Scanner.Core.Metadata.Nfo; |
||||
|
||||
public class ArtistNfo |
||||
{ |
||||
public ArtistNfo() |
||||
{ |
||||
Genres = new List<string>(); |
||||
Styles = new List<string>(); |
||||
Moods = new List<string>(); |
||||
} |
||||
|
||||
public string? Name { get; set; } |
||||
public string? Disambiguation { get; set; } |
||||
public List<string> Genres { get; } |
||||
public List<string> Styles { get; } |
||||
public List<string> Moods { get; } |
||||
public string? Biography { get; set; } |
||||
} |
||||
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
namespace ErsatzTV.Scanner.Core.Metadata.Nfo; |
||||
|
||||
public class EpisodeNfo |
||||
{ |
||||
public EpisodeNfo() |
||||
{ |
||||
Actors = new List<ActorNfo>(); |
||||
Writers = new List<string>(); |
||||
Directors = new List<string>(); |
||||
UniqueIds = new List<UniqueIdNfo>(); |
||||
} |
||||
|
||||
public string? ShowTitle { get; set; } |
||||
public string? Title { get; set; } |
||||
public int Episode { get; set; } |
||||
public int Season { get; set; } |
||||
public string? ContentRating { get; set; } |
||||
public Option<DateTime> Aired { get; set; } |
||||
public string? Plot { get; set; } |
||||
public List<ActorNfo> Actors { get; } |
||||
public List<string> Writers { get; } |
||||
public List<string> Directors { get; } |
||||
public List<UniqueIdNfo> UniqueIds { get; } |
||||
} |
||||
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
namespace ErsatzTV.Scanner.Core.Metadata.Nfo; |
||||
|
||||
public class MovieNfo |
||||
{ |
||||
public MovieNfo() |
||||
{ |
||||
Genres = new List<string>(); |
||||
Tags = new List<string>(); |
||||
Studios = new List<string>(); |
||||
Actors = new List<ActorNfo>(); |
||||
Writers = new List<string>(); |
||||
Directors = new List<string>(); |
||||
UniqueIds = new List<UniqueIdNfo>(); |
||||
} |
||||
|
||||
public string? Title { get; set; } |
||||
public string? SortTitle { get; set; } |
||||
public string? Outline { get; set; } |
||||
public int Year { get; set; } |
||||
public string? ContentRating { get; set; } |
||||
public Option<DateTime> Premiered { get; set; } |
||||
public string? Plot { get; set; } |
||||
// public string? Tagline { get; set; }
|
||||
public List<string> Genres { get; } |
||||
public List<string> Tags { get; } |
||||
public List<string> Studios { get; } |
||||
public List<ActorNfo> Actors { get; } |
||||
public List<string> Writers { get; } |
||||
public List<string> Directors { get; } |
||||
public List<UniqueIdNfo> UniqueIds { get; } |
||||
} |
||||
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
namespace ErsatzTV.Scanner.Core.Metadata.Nfo; |
||||
|
||||
public class MusicVideoNfo |
||||
{ |
||||
public MusicVideoNfo() |
||||
{ |
||||
Artists = new List<string>(); |
||||
Genres = new List<string>(); |
||||
Tags = new List<string>(); |
||||
Studios = new List<string>(); |
||||
} |
||||
|
||||
public List<string> Artists { get; } |
||||
public string? Title { get; set; } |
||||
public string? Album { get; set; } |
||||
public string? Plot { get; set; } |
||||
public int Track { get; set; } |
||||
public Option<DateTime> Aired { get; set; } |
||||
public int Year { get; set; } |
||||
public List<string> Genres { get; } |
||||
public List<string> Tags { get; } |
||||
public List<string> Studios { get; } |
||||
} |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue