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 @@ |
|||||||
namespace ErsatzTV.Application.MediaSources; |
|
||||||
|
|
||||||
public record LocalMediaSourceViewModel(int Id) : MediaSourceViewModel(Id, "Local"); |
|
||||||
@ -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 @@ |
|||||||
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); |
|
||||||
} |
|
||||||
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
@ -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" }; |
||||||
|
} |
||||||
@ -1,14 +1,14 @@ |
|||||||
using System.Globalization; |
using System.Globalization; |
||||||
using ErsatzTV.Core.Domain; |
using ErsatzTV.Core.Domain; |
||||||
using ErsatzTV.Core.Interfaces.Repositories; |
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
using ErsatzTV.Core.Metadata; |
using ErsatzTV.Scanner.Core.Metadata; |
||||||
using ErsatzTV.Core.Tests.Fakes; |
using ErsatzTV.Scanner.Tests.Core.Fakes; |
||||||
using FluentAssertions; |
using FluentAssertions; |
||||||
using Microsoft.Extensions.Logging; |
using Microsoft.Extensions.Logging; |
||||||
using Moq; |
using Moq; |
||||||
using NUnit.Framework; |
using NUnit.Framework; |
||||||
|
|
||||||
namespace ErsatzTV.Core.Tests.Metadata; |
namespace ErsatzTV.Scanner.Tests.Core.Metadata; |
||||||
|
|
||||||
[TestFixture] |
[TestFixture] |
||||||
public class LocalSubtitlesProviderTests |
public class LocalSubtitlesProviderTests |
||||||
@ -1,13 +1,14 @@ |
|||||||
using System.Text; |
using System.Text; |
||||||
using Bugsnag; |
using Bugsnag; |
||||||
using ErsatzTV.Core.Metadata.Nfo; |
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Scanner.Core.Metadata.Nfo; |
||||||
using FluentAssertions; |
using FluentAssertions; |
||||||
using Microsoft.Extensions.Logging.Abstractions; |
using Microsoft.Extensions.Logging.Abstractions; |
||||||
using Microsoft.IO; |
using Microsoft.IO; |
||||||
using Moq; |
using Moq; |
||||||
using NUnit.Framework; |
using NUnit.Framework; |
||||||
|
|
||||||
namespace ErsatzTV.Core.Tests.Metadata.Nfo; |
namespace ErsatzTV.Scanner.Tests.Core.Metadata.Nfo; |
||||||
|
|
||||||
[TestFixture] |
[TestFixture] |
||||||
public class MusicVideoNfoReaderTests |
public class MusicVideoNfoReaderTests |
||||||
@ -1,13 +1,14 @@ |
|||||||
using System.Text; |
using System.Text; |
||||||
using Bugsnag; |
using Bugsnag; |
||||||
using ErsatzTV.Core.Metadata.Nfo; |
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Scanner.Core.Metadata.Nfo; |
||||||
using FluentAssertions; |
using FluentAssertions; |
||||||
using Microsoft.Extensions.Logging.Abstractions; |
using Microsoft.Extensions.Logging.Abstractions; |
||||||
using Microsoft.IO; |
using Microsoft.IO; |
||||||
using Moq; |
using Moq; |
||||||
using NUnit.Framework; |
using NUnit.Framework; |
||||||
|
|
||||||
namespace ErsatzTV.Core.Tests.Metadata.Nfo; |
namespace ErsatzTV.Scanner.Tests.Core.Metadata.Nfo; |
||||||
|
|
||||||
[TestFixture] |
[TestFixture] |
||||||
public class OtherVideoNfoReaderTests |
public class OtherVideoNfoReaderTests |
||||||
@ -1,6 +1,6 @@ |
|||||||
using ErsatzTV.Core.Domain; |
using ErsatzTV.Core.Domain; |
||||||
|
|
||||||
namespace ErsatzTV.Core.Interfaces.Metadata; |
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata; |
||||||
|
|
||||||
public interface ILocalMetadataProvider |
public interface ILocalMetadataProvider |
||||||
{ |
{ |
||||||
@ -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 |
public interface ILocalStatisticsProvider |
||||||
{ |
{ |
||||||
@ -1,6 +1,6 @@ |
|||||||
using ErsatzTV.Core.Domain; |
using ErsatzTV.Core.Domain; |
||||||
|
|
||||||
namespace ErsatzTV.Core.Interfaces.Metadata; |
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata; |
||||||
|
|
||||||
public interface ILocalSubtitlesProvider |
public interface ILocalSubtitlesProvider |
||||||
{ |
{ |
||||||
@ -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 |
public interface IMovieFolderScanner |
||||||
{ |
{ |
||||||
@ -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 |
public interface IMusicVideoFolderScanner |
||||||
{ |
{ |
||||||
@ -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 |
public interface IOtherVideoFolderScanner |
||||||
{ |
{ |
||||||
@ -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 |
public interface ISongFolderScanner |
||||||
{ |
{ |
||||||
@ -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 |
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); |
||||||
|
} |
||||||
@ -1,13 +1,15 @@ |
|||||||
using ErsatzTV.Core.Domain; |
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
using ErsatzTV.Core.Domain.MediaServer; |
using ErsatzTV.Core.Domain.MediaServer; |
||||||
using ErsatzTV.Core.Errors; |
using ErsatzTV.Core.Errors; |
||||||
using ErsatzTV.Core.Interfaces.Metadata; |
using ErsatzTV.Core.Interfaces.Metadata; |
||||||
using ErsatzTV.Core.Interfaces.Repositories; |
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
using ErsatzTV.Core.MediaSources; |
using ErsatzTV.Core.MediaSources; |
||||||
using MediatR; |
using ErsatzTV.Core.Metadata; |
||||||
|
using ErsatzTV.Scanner.Core.Interfaces.Metadata; |
||||||
using Microsoft.Extensions.Logging; |
using Microsoft.Extensions.Logging; |
||||||
|
|
||||||
namespace ErsatzTV.Core.Metadata; |
namespace ErsatzTV.Scanner.Core.Metadata; |
||||||
|
|
||||||
public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, TLibrary, TShow, TSeason, TEpisode, |
public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, TLibrary, TShow, TSeason, TEpisode, |
||||||
TEtag> |
TEtag> |
||||||
@ -1,4 +1,4 @@ |
|||||||
namespace ErsatzTV.Core.Metadata; |
namespace ErsatzTV.Scanner.Core.Metadata; |
||||||
|
|
||||||
public static class MetadataSongTag |
public static class MetadataSongTag |
||||||
{ |
{ |
||||||
@ -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; } |
||||||
|
} |
||||||
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
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