scanner refactoring and other cleanup (#1082)

* move subtitles provider into scanner

* move more stuff into scanner

* move nfo into scanner

* add scan subcommand

* fix a bunch of nfo build warnings

* more subcommands

* fix warnings

* cleanup logging

* remove unused code

* cleanup old ffmpeg stuff

* rename complex filter

* refactor wrapped segmenter
This commit is contained in:
Jason Dove
2022-12-31 10:57:20 -06:00
committed by GitHub
parent 6e49ea78ec
commit 9479806cb0
118 changed files with 960 additions and 3007 deletions
@@ -38,21 +38,12 @@
<Content Include="Resources\ErsatzTV.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Resources\Nfo\ArtistInvalidCharacters1.nfo">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Resources\Nfo\ArtistInvalidCharacters2.nfo">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Resources\test.sup">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Resources\test.srt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Resources\Nfo\EpisodeInvalidCharacters.nfo">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
@@ -1,777 +0,0 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.FFmpeg.State;
using FluentAssertions;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.FFmpeg;
[TestFixture]
public class FFmpegComplexFilterBuilderTests
{
[TestFixture]
public class Build
{
[Test]
public void Should_Return_None_With_No_Filters()
{
var builder = new FFmpegComplexFilterBuilder();
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false);
result.IsNone.Should().BeTrue();
}
[Test]
public void Should_Return_Audio_Filter_With_AudioDuration()
{
var duration = TimeSpan.FromMilliseconds(1000.1);
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
.WithAlignedAudio(duration);
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false);
result.IsSome.Should().BeTrue();
result.IfSome(
filter =>
{
filter.ComplexFilter.Should().Be("[0:1]apad=whole_dur=1000.1ms[a]");
filter.AudioLabel.Should().Be("[a]");
filter.VideoLabel.Should().Be("0:0");
});
}
[Test]
// this needs to be a culture where '.' is a group separator
[SetCulture("it-IT")]
public void Should_Return_Audio_Filter_With_AudioDuration_Decimal()
{
var duration = TimeSpan.FromMilliseconds(1000.1);
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
.WithAlignedAudio(duration);
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false);
result.IsSome.Should().BeTrue();
result.IfSome(
filter =>
{
filter.ComplexFilter.Should().Be("[0:1]apad=whole_dur=1000.1ms[a]");
filter.AudioLabel.Should().Be("[a]");
filter.VideoLabel.Should().Be("0:0");
});
}
[Test]
public void Should_Return_Audio_And_Video_Filter()
{
var duration = TimeSpan.FromMinutes(54);
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
.WithAlignedAudio(duration)
.WithDeinterlace(true);
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false);
result.IsSome.Should().BeTrue();
result.IfSome(
filter =>
{
filter.ComplexFilter.Should().Be(
$"[0:1]apad=whole_dur={duration.TotalMilliseconds}ms[a];[0:0]yadif=1[v]");
filter.AudioLabel.Should().Be("[a]");
filter.VideoLabel.Should().Be("[v]");
});
}
[Test]
[TestCase(true, false, false, "[0:0]yadif=1[v]", "[v]")]
[TestCase(true, true, false, "[0:0]yadif=1,scale=1920:1000:flags=fast_bilinear,setsar=1[v]", "[v]")]
[TestCase(true, false, true, "[0:0]yadif=1,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", "[v]")]
[TestCase(
true,
true,
true,
"[0:0]yadif=1,scale=1920:1000:flags=fast_bilinear,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]",
"[v]")]
[TestCase(false, true, false, "[0:0]scale=1920:1000:flags=fast_bilinear,setsar=1[v]", "[v]")]
[TestCase(false, false, true, "[0:0]setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", "[v]")]
[TestCase(
false,
true,
true,
"[0:0]scale=1920:1000:flags=fast_bilinear,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]",
"[v]")]
public void Should_Return_Software_Video_Filter(
bool deinterlace,
bool scale,
bool pad,
string expectedVideoFilter,
string expectedVideoLabel)
{
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
.WithDeinterlace(deinterlace);
if (scale)
{
builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 });
}
if (pad)
{
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 });
}
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false);
result.IsSome.Should().BeTrue();
result.IfSome(
filter =>
{
filter.ComplexFilter.Should().Be(expectedVideoFilter);
filter.AudioLabel.Should().Be("0:1");
filter.VideoLabel.Should().Be(expectedVideoLabel);
});
}
[Test]
[TestCase(
false,
false,
false,
WatermarkLocation.BottomLeft,
false,
100,
"[0:0][1:v]overlay=x=134:y=H-h-54[v]",
"0:1",
"[v]")]
[TestCase(
false,
false,
false,
WatermarkLocation.BottomRight,
false,
100,
"[0:0][1:v]overlay=x=W-w-134:y=H-h-54[v]",
"0:1",
"[v]")]
[TestCase(
false,
false,
false,
WatermarkLocation.TopLeft,
false,
100,
"[0:0][1:v]overlay=x=134:y=54[v]",
"0:1",
"[v]")]
[TestCase(
false,
false,
false,
WatermarkLocation.TopRight,
false,
100,
"[0:0][1:v]overlay=x=W-w-134:y=54[v]",
"0:1",
"[v]")]
[TestCase(
false,
false,
true,
WatermarkLocation.TopLeft,
false,
100,
"[1:v]format=yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)'[wmp];[0:0][wmp]overlay=x=134:y=54,format=nv12[v]",
"0:1",
"[v]")]
[TestCase(
false,
false,
false,
WatermarkLocation.TopLeft,
true,
100,
"[1:v]scale=384:-1[wmp];[0:0][wmp]overlay=x=134:y=54[v]",
"0:1",
"[v]")]
[TestCase(
false,
false,
false,
WatermarkLocation.TopLeft,
false,
90,
"[1:v]format=yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8,colorchannelmixer=aa=0.90[wmp];[0:0][wmp]overlay=x=134:y=54[v]",
"0:1",
"[v]")]
[TestCase(
false,
true,
false,
WatermarkLocation.TopLeft,
false,
100,
"[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]",
"0:1",
"[v]")]
[TestCase(
false,
true,
false,
WatermarkLocation.TopLeft,
true,
100,
"[0:0]yadif=1[vt];[1:v]scale=384:-1[wmp];[vt][wmp]overlay=x=134:y=54[v]",
"0:1",
"[v]")]
[TestCase(
true,
true,
false,
WatermarkLocation.TopLeft,
false,
100,
"[0:1]apad=whole_dur=3300000ms[a];[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]",
"[a]",
"[v]")]
[TestCase(
true,
false,
false,
WatermarkLocation.TopLeft,
false,
100,
"[0:1]apad=whole_dur=3300000ms[a];[0:0][1:v]overlay=x=134:y=54[v]",
"[a]",
"[v]")]
public void Should_Return_Watermark(
bool alignAudio,
bool deinterlace,
bool intermittent,
WatermarkLocation location,
bool scaled,
int opacity,
string expectedVideoFilter,
string expectedAudioLabel,
string expectedVideoLabel)
{
var watermark = new ChannelWatermark
{
Mode = intermittent
? ChannelWatermarkMode.Intermittent
: ChannelWatermarkMode.Permanent,
DurationSeconds = intermittent ? 15 : 0,
FrequencyMinutes = intermittent ? 10 : 0,
Location = location,
Size = scaled ? WatermarkSize.Scaled : WatermarkSize.ActualSize,
WidthPercent = scaled ? 20 : 0,
Opacity = opacity,
HorizontalMarginPercent = 7,
VerticalMarginPercent = 5
};
Option<List<FadePoint>> maybeFadePoints = watermark.Mode == ChannelWatermarkMode.Intermittent
? Some(
WatermarkCalculator.CalculateFadePoints(
new DateTimeOffset(2022, 01, 31, 12, 25, 0, TimeSpan.FromHours(-5)),
TimeSpan.Zero,
TimeSpan.FromMinutes(55),
TimeSpan.Zero,
watermark.FrequencyMinutes,
watermark.DurationSeconds))
: None;
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
.WithWatermark(
Some(watermark),
maybeFadePoints,
new Resolution { Width = 1920, Height = 1080 },
None)
.WithDeinterlace(deinterlace)
.WithAlignedAudio(alignAudio ? Some(TimeSpan.FromMinutes(55)) : None);
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false);
result.IsSome.Should().BeTrue();
result.IfSome(
filter =>
{
filter.ComplexFilter.Should().Be(expectedVideoFilter);
filter.AudioLabel.Should().Be(expectedAudioLabel);
filter.VideoLabel.Should().Be(expectedVideoLabel);
});
}
[Test]
[TestCase(
false,
false,
false,
WatermarkLocation.BottomLeft,
false,
100,
"[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=H-h-54[v]",
"0:1",
"[v]",
false)]
[TestCase(
false,
false,
false,
WatermarkLocation.BottomLeft,
false,
100,
"[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=H-h-54,hwupload[v]",
"0:1",
"[v]",
true)]
[TestCase(
false,
false,
true,
WatermarkLocation.TopLeft,
false,
100,
"[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)',hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]",
"0:1",
"[v]",
false)]
[TestCase(
false,
false,
true,
WatermarkLocation.TopLeft,
false,
100,
"[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)',hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]",
"0:1",
"[v]",
true)]
[TestCase(
false,
false,
false,
WatermarkLocation.TopLeft,
true,
100,
"[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,scale=384:-1,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]",
"0:1",
"[v]",
false)]
[TestCase(
false,
false,
false,
WatermarkLocation.TopLeft,
true,
100,
"[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,scale=384:-1,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]",
"0:1",
"[v]",
true)]
[TestCase(
false,
false,
false,
WatermarkLocation.TopLeft,
false,
90,
"[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,colorchannelmixer=aa=0.90,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]",
"0:1",
"[v]",
false)]
[TestCase(
false,
false,
false,
WatermarkLocation.TopLeft,
false,
90,
"[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,colorchannelmixer=aa=0.90,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]",
"0:1",
"[v]",
true)]
// TODO: do we need these anymore? interlaced content that isn't handled by mpeg2_cuvid?
// [TestCase(
// false,
// true,
// false,
// WatermarkLocation.TopLeft,
// false,
// 100,
// "[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]",
// "0:1",
// "[v]")]
// [TestCase(
// false,
// true,
// false,
// WatermarkLocation.TopLeft,
// true,
// 100,
// "[0:0]yadif=1[vt];[1:v]scale=384:-1[wmp];[vt][wmp]overlay=x=134:y=54[v]",
// "0:1",
// "[v]")]
// [TestCase(
// true,
// true,
// false,
// WatermarkLocation.TopLeft,
// false,
// 100,
// "[0:1]apad=whole_dur=3300000ms[a];[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]",
// "[a]",
// "[v]")]
[TestCase(
true,
false,
false,
WatermarkLocation.TopLeft,
false,
100,
"[0:1]apad=whole_dur=3300000ms[a];[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]",
"[a]",
"[v]",
false)]
[TestCase(
true,
false,
false,
WatermarkLocation.TopLeft,
false,
100,
"[0:1]apad=whole_dur=3300000ms[a];[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]",
"[a]",
"[v]",
true)]
public void Should_Return_NVENC_Watermark(
bool alignAudio,
bool deinterlace,
bool intermittent,
WatermarkLocation location,
bool scaled,
int opacity,
string expectedVideoFilter,
string expectedAudioLabel,
string expectedVideoLabel,
bool scaledSource)
{
var watermark = new ChannelWatermark
{
Mode = intermittent
? ChannelWatermarkMode.Intermittent
: ChannelWatermarkMode.Permanent,
DurationSeconds = intermittent ? 15 : 0,
FrequencyMinutes = intermittent ? 10 : 0,
Location = location,
Size = scaled ? WatermarkSize.Scaled : WatermarkSize.ActualSize,
WidthPercent = scaled ? 20 : 0,
Opacity = opacity,
HorizontalMarginPercent = 7,
VerticalMarginPercent = 5
};
Option<List<FadePoint>> maybeFadePoints = watermark.Mode == ChannelWatermarkMode.Intermittent
? Some(
WatermarkCalculator.CalculateFadePoints(
new DateTimeOffset(2022, 01, 31, 12, 25, 0, TimeSpan.FromHours(-5)),
TimeSpan.Zero,
TimeSpan.FromMinutes(55),
TimeSpan.Zero,
watermark.FrequencyMinutes,
watermark.DurationSeconds))
: None;
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
.WithHardwareAcceleration(HardwareAccelerationKind.Nvenc)
.WithWatermark(
Some(watermark),
maybeFadePoints,
new Resolution { Width = 1920, Height = 1080 },
None)
.WithDeinterlace(deinterlace)
.WithAlignedAudio(alignAudio ? Some(TimeSpan.FromMinutes(55)) : None);
if (scaledSource)
{
builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1080 });
}
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false);
result.IsSome.Should().BeTrue();
result.IfSome(
filter =>
{
filter.ComplexFilter.Should().Be(expectedVideoFilter);
filter.AudioLabel.Should().Be(expectedAudioLabel);
filter.VideoLabel.Should().Be(expectedVideoLabel);
});
}
[Test]
[TestCase(true, false, false, "[0:0]deinterlace_qsv[v]", "[v]")]
[TestCase(
true,
true,
false,
"[0:0]deinterlace_qsv,scale_qsv=w=1920:h=1000,setsar=1[v]",
"[v]")]
[TestCase(
true,
false,
true,
"[0:0]deinterlace_qsv,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
"[v]")]
[TestCase(
true,
true,
true,
"[0:0]deinterlace_qsv,scale_qsv=w=1920:h=1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
"[v]")]
[TestCase(
false,
true,
false,
"[0:0]scale_qsv=w=1920:h=1000,setsar=1[v]",
"[v]")]
[TestCase(
false,
false,
true,
"[0:0]setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
"[v]")]
[TestCase(
false,
true,
true,
"[0:0]scale_qsv=w=1920:h=1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
"[v]")]
public void Should_Return_QSV_Video_Filter(
bool deinterlace,
bool scale,
bool pad,
string expectedVideoFilter,
string expectedVideoLabel)
{
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
.WithHardwareAcceleration(HardwareAccelerationKind.Qsv)
.WithDeinterlace(deinterlace);
if (scale)
{
builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 });
}
if (pad)
{
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 });
}
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false);
result.IsSome.Should().BeTrue();
result.IfSome(
filter =>
{
filter.ComplexFilter.Should().Be(expectedVideoFilter);
filter.AudioLabel.Should().Be("0:1");
filter.VideoLabel.Should().Be(expectedVideoLabel);
});
}
[Test]
[TestCase(true, false, false, "[0:0]yadif_cuda[v]", "[v]")]
[TestCase(
true,
true,
false,
"[0:0]yadif_cuda,scale_cuda=1920:1000,setsar=1[v]",
"[v]")]
[TestCase(
true,
false,
true,
"[0:0]yadif_cuda,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
"[v]")]
[TestCase(
true,
true,
true,
"[0:0]yadif_cuda,scale_cuda=1920:1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
"[v]")]
[TestCase(
false,
true,
false,
"[0:0]scale_cuda=1920:1000,setsar=1[v]",
"[v]")]
[TestCase(
false,
false,
true,
"[0:0]setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
"[v]")]
[TestCase(
false,
true,
true,
"[0:0]scale_cuda=1920:1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
"[v]")]
public void Should_Return_NVENC_Video_Filter(
bool deinterlace,
bool scale,
bool pad,
string expectedVideoFilter,
string expectedVideoLabel)
{
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
.WithHardwareAcceleration(HardwareAccelerationKind.Nvenc)
.WithDeinterlace(deinterlace)
.WithInputPixelFormat("h264");
if (scale)
{
builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 });
}
if (pad)
{
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 });
}
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false);
result.IsSome.Should().BeTrue();
result.IfSome(
filter =>
{
filter.ComplexFilter.Should().Be(expectedVideoFilter);
filter.AudioLabel.Should().Be("0:1");
filter.VideoLabel.Should().Be(expectedVideoLabel);
});
}
[Test]
[TestCase("h264", true, false, false, "[0:0]deinterlace_vaapi[v]", "[v]")]
[TestCase(
"h264",
true,
true,
false,
"[0:0]deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]",
"[v]")]
[TestCase(
"h264",
true,
false,
true,
"[0:0]deinterlace_vaapi,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
"[v]")]
[TestCase(
"h264",
true,
true,
true,
"[0:0]deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
"[v]")]
[TestCase(
"h264",
false,
true,
false,
"[0:0]scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]",
"[v]")]
[TestCase(
"h264",
false,
false,
true,
"[0:0]setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
"[v]")]
[TestCase(
"h264",
false,
true,
true,
"[0:0]scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
"[v]")]
[TestCase("mpeg4", true, false, false, "[0:0]hwupload,deinterlace_vaapi[v]", "[v]")]
[TestCase(
"mpeg4",
true,
true,
false,
"[0:0]hwupload,deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]",
"[v]")]
[TestCase(
"mpeg4",
true,
false,
true,
"[0:0]hwupload,deinterlace_vaapi,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
"[v]")]
[TestCase(
"mpeg4",
true,
true,
true,
"[0:0]hwupload,deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
"[v]")]
[TestCase(
"mpeg4",
false,
true,
false,
"[0:0]hwupload,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]",
"[v]")]
[TestCase(
"mpeg4",
false,
false,
true,
"[0:0]setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
"[v]")]
[TestCase(
"mpeg4",
false,
true,
true,
"[0:0]hwupload,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
"[v]")]
public void Should_Return_VAAPI_Video_Filter(
string codec,
bool deinterlace,
bool scale,
bool pad,
string expectedVideoFilter,
string expectedVideoLabel)
{
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
.WithHardwareAcceleration(HardwareAccelerationKind.Vaapi)
.WithInputCodec(codec)
.WithDeinterlace(deinterlace);
if (scale)
{
builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 });
}
if (pad)
{
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 });
}
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false);
result.IsSome.Should().BeTrue();
result.IfSome(
filter =>
{
filter.ComplexFilter.Should().Be(expectedVideoFilter);
filter.AudioLabel.Should().Be("0:1");
filter.VideoLabel.Should().Be(expectedVideoLabel);
});
}
}
}
@@ -1,813 +0,0 @@
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using Bugsnag;
using CliWrap;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.FFmpeg;
using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.FFmpeg.Filter;
using ErsatzTV.FFmpeg.Filter.Cuda;
using ErsatzTV.FFmpeg.Filter.Qsv;
using ErsatzTV.FFmpeg.Filter.Vaapi;
using ErsatzTV.FFmpeg.Format;
using ErsatzTV.FFmpeg.Pipeline;
using ErsatzTV.FFmpeg.State;
using ErsatzTV.Infrastructure.Runtime;
using FluentAssertions;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Serilog;
using MediaStream = ErsatzTV.Core.Domain.MediaStream;
namespace ErsatzTV.Core.Tests.FFmpeg;
[TestFixture]
[Explicit]
public class TranscodingTests
{
private static readonly ILoggerFactory LoggerFactory;
private static readonly MemoryCache MemoryCache;
static TranscodingTests()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
LoggerFactory = new LoggerFactory().AddSerilog(Log.Logger);
MemoryCache = new MemoryCache(new MemoryCacheOptions());
}
[Test]
[Explicit]
public void DeleteTestVideos()
{
foreach (string file in Directory.GetFiles(TestContext.CurrentContext.TestDirectory, "*.mkv"))
{
File.Delete(file);
}
Assert.Pass();
}
public record InputFormat(
string Encoder,
string PixelFormat,
string ColorRange = "tv",
string ColorSpace = "bt709",
string ColorTransfer = "bt709",
string ColorPrimaries = "bt709");
public enum Padding
{
NoPadding,
WithPadding
}
public enum Watermark
{
None,
PermanentOpaqueScaled,
PermanentOpaqueActualSize,
PermanentTransparentScaled,
PermanentTransparentActualSize,
IntermittentOpaque,
IntermittentTransparent
// TODO: animated vs static
}
public enum Subtitle
{
None,
Picture,
Text
}
private class TestData
{
public static Watermark[] Watermarks =
{
Watermark.None,
// Watermark.PermanentOpaqueScaled,
// Watermark.PermanentOpaqueActualSize,
// Watermark.PermanentTransparentScaled,
// Watermark.PermanentTransparentActualSize
};
public static Subtitle[] Subtitles =
{
Subtitle.None,
// Subtitle.Picture,
// Subtitle.Text
};
public static Padding[] Paddings =
{
Padding.NoPadding,
// Padding.WithPadding
};
public static VideoScanKind[] VideoScanKinds =
{
VideoScanKind.Progressive,
// VideoScanKind.Interlaced
};
public static InputFormat[] InputFormats =
{
// // example format that requires colorspace filter
// new("libx264", "yuv420p", "tv", "smpte170m", "bt709", "smpte170m"),
//
// // example format that requires setparams filter
// new("libx264", "yuv420p", string.Empty, string.Empty, string.Empty, string.Empty),
//
// // new("libx264", "yuvj420p"),
// new("libx264", "yuv420p10le"),
// // new("libx264", "yuv444p10le"),
//
// // new("mpeg1video", "yuv420p"),
// //
// // new("mpeg2video", "yuv420p"),
//
new("libx265", "yuv420p"),
// new("libx265", "yuv420p10le"),
//
// // new("mpeg4", "yuv420p"),
// //
// // new("libvpx-vp9", "yuv420p"),
// //
// // // new("libaom-av1", "yuv420p")
// // // av1 yuv420p10le 51
// //
// // new("msmpeg4v2", "yuv420p"),
// // new("msmpeg4v3", "yuv420p")
//
// // wmv3 yuv420p 1
};
public static Resolution[] Resolutions =
{
new() { Width = 1920, Height = 1080 },
// new() { Width = 1280, Height = 720 }
};
public static FFmpegProfileBitDepth[] BitDepths =
{
FFmpegProfileBitDepth.EightBit,
// FFmpegProfileBitDepth.TenBit
};
public static FFmpegProfileVideoFormat[] VideoFormats =
{
FFmpegProfileVideoFormat.H264,
// FFmpegProfileVideoFormat.Hevc
};
public static HardwareAccelerationKind[] TestAccelerations =
{
// HardwareAccelerationKind.None,
HardwareAccelerationKind.Nvenc,
// HardwareAccelerationKind.Vaapi,
// HardwareAccelerationKind.Qsv,
// HardwareAccelerationKind.VideoToolbox,
// HardwareAccelerationKind.Amf
};
public static string[] FilesToTest => new[] { string.Empty };
}
[Test]
[Combinatorial]
public async Task Transcode(
[ValueSource(typeof(TestData), nameof(TestData.FilesToTest))]
string fileToTest,
[ValueSource(typeof(TestData), nameof(TestData.InputFormats))]
InputFormat inputFormat,
[ValueSource(typeof(TestData), nameof(TestData.Resolutions))]
Resolution profileResolution,
[ValueSource(typeof(TestData), nameof(TestData.BitDepths))]
FFmpegProfileBitDepth profileBitDepth,
[ValueSource(typeof(TestData), nameof(TestData.Paddings))]
Padding padding,
[ValueSource(typeof(TestData), nameof(TestData.VideoScanKinds))]
VideoScanKind videoScanKind,
[ValueSource(typeof(TestData), nameof(TestData.Watermarks))]
Watermark watermark,
[ValueSource(typeof(TestData), nameof(TestData.Subtitles))]
Subtitle subtitle,
[ValueSource(typeof(TestData), nameof(TestData.VideoFormats))]
FFmpegProfileVideoFormat profileVideoFormat,
[ValueSource(typeof(TestData), nameof(TestData.TestAccelerations))] HardwareAccelerationKind profileAcceleration)
{
string file = fileToTest;
if (string.IsNullOrWhiteSpace(file))
{
if (inputFormat.Encoder is "mpeg1video" or "msmpeg4v2" or "msmpeg4v3")
{
if (videoScanKind == VideoScanKind.Interlaced)
{
Assert.Inconclusive($"{inputFormat.Encoder} does not support interlaced content");
return;
}
}
string name = GetStringSha256Hash($"{inputFormat}_{videoScanKind}_{padding}_{subtitle}");
file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv");
if (!File.Exists(file))
{
await GenerateTestFile(inputFormat, padding, videoScanKind, subtitle, file);
}
}
var imageCache = new Mock<IImageCache>();
// always return the static watermark resource
imageCache.Setup(
ic => ic.GetPathForImage(
It.IsAny<string>(),
It.Is<ArtworkKind>(x => x == ArtworkKind.Watermark),
It.IsAny<Option<int>>()))
.Returns(Path.Combine(TestContext.CurrentContext.TestDirectory, "Resources", "ErsatzTV.png"));
var oldService = new FFmpegProcessService(
new FFmpegPlaybackSettingsCalculator(),
new FakeStreamSelector(),
imageCache.Object,
new Mock<ITempFilePool>().Object,
new Mock<IClient>().Object,
MemoryCache,
LoggerFactory.CreateLogger<FFmpegProcessService>());
var service = new FFmpegLibraryProcessService(
oldService,
new FFmpegPlaybackSettingsCalculator(),
new FakeStreamSelector(),
new Mock<ITempFilePool>().Object,
new PipelineBuilderFactory(
new RuntimeInfo(),
//new FakeNvidiaCapabilitiesFactory(),
new HardwareCapabilitiesFactory(
MemoryCache,
LoggerFactory.CreateLogger<HardwareCapabilitiesFactory>()),
LoggerFactory.CreateLogger<PipelineBuilderFactory>()),
LoggerFactory.CreateLogger<FFmpegLibraryProcessService>());
var v = new MediaVersion
{
MediaFiles = new List<MediaFile>
{
new() { Path = file }
},
Streams = new List<MediaStream>()
};
var metadataRepository = new Mock<IMetadataRepository>();
metadataRepository
.Setup(r => r.UpdateLocalStatistics(It.IsAny<MediaItem>(), It.IsAny<MediaVersion>(), It.IsAny<bool>()))
.Callback<MediaItem, MediaVersion, bool>(
(_, version, _) =>
{
version.MediaFiles = v.MediaFiles;
v = version;
});
var localStatisticsProvider = new LocalStatisticsProvider(
metadataRepository.Object,
new LocalFileSystem(new Mock<IClient>().Object, LoggerFactory.CreateLogger<LocalFileSystem>()),
new Mock<IClient>().Object,
LoggerFactory.CreateLogger<LocalStatisticsProvider>());
await localStatisticsProvider.RefreshStatistics(
ExecutableName("ffmpeg"),
ExecutableName("ffprobe"),
new Movie
{
MediaVersions = new List<MediaVersion>
{
new()
{
MediaFiles = new List<MediaFile>
{
new() { Path = file }
}
}
}
});
if (videoScanKind == VideoScanKind.Interlaced)
{
v.VideoScanKind.Should().Be(VideoScanKind.Interlaced, file);
}
var subtitleStreams = v.Streams
.Filter(s => s.MediaStreamKind == MediaStreamKind.Subtitle)
.ToList();
var subtitles = new List<Domain.Subtitle>();
if (subtitle != Subtitle.None)
{
foreach (MediaStream stream in subtitleStreams)
{
var s = new Domain.Subtitle
{
Codec = stream.Codec,
Default = stream.Default,
Forced = stream.Forced,
Language = stream.Language,
StreamIndex = stream.Index,
SubtitleKind = SubtitleKind.Embedded,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow,
Path = "test.srt",
IsExtracted = true
};
subtitles.Add(s);
}
}
DateTimeOffset now = DateTimeOffset.Now;
Option<ChannelWatermark> channelWatermark = Option<ChannelWatermark>.None;
switch (watermark)
{
case Watermark.None:
break;
case Watermark.IntermittentOpaque:
channelWatermark = new ChannelWatermark
{
ImageSource = ChannelWatermarkImageSource.Custom,
Mode = ChannelWatermarkMode.Intermittent,
// TODO: how do we make sure this actually appears
FrequencyMinutes = 1,
DurationSeconds = 2,
Opacity = 100
};
break;
case Watermark.IntermittentTransparent:
channelWatermark = new ChannelWatermark
{
ImageSource = ChannelWatermarkImageSource.Custom,
Mode = ChannelWatermarkMode.Intermittent,
// TODO: how do we make sure this actually appears
FrequencyMinutes = 1,
DurationSeconds = 2,
Opacity = 80
};
break;
case Watermark.PermanentOpaqueScaled:
channelWatermark = new ChannelWatermark
{
ImageSource = ChannelWatermarkImageSource.Custom,
Mode = ChannelWatermarkMode.Permanent,
Opacity = 100,
Size = WatermarkSize.Scaled,
WidthPercent = 15
};
break;
case Watermark.PermanentOpaqueActualSize:
channelWatermark = new ChannelWatermark
{
ImageSource = ChannelWatermarkImageSource.Custom,
Mode = ChannelWatermarkMode.Permanent,
Opacity = 100,
Size = WatermarkSize.ActualSize
};
break;
case Watermark.PermanentTransparentScaled:
channelWatermark = new ChannelWatermark
{
ImageSource = ChannelWatermarkImageSource.Custom,
Mode = ChannelWatermarkMode.Permanent,
Opacity = 80,
Size = WatermarkSize.Scaled,
WidthPercent = 15
};
break;
case Watermark.PermanentTransparentActualSize:
channelWatermark = new ChannelWatermark
{
ImageSource = ChannelWatermarkImageSource.Custom,
Mode = ChannelWatermarkMode.Permanent,
Opacity = 80,
Size = WatermarkSize.ActualSize
};
break;
}
ChannelSubtitleMode subtitleMode = subtitle switch
{
Subtitle.Picture or Subtitle.Text => ChannelSubtitleMode.Any,
_ => ChannelSubtitleMode.None
};
string srtFile = Path.Combine(FileSystemLayout.SubtitleCacheFolder, "test.srt");
if (subtitle == Subtitle.Text && !File.Exists(srtFile))
{
string sourceFile = Path.Combine(TestContext.CurrentContext.TestDirectory, "Resources", "test.srt");
Directory.CreateDirectory(FileSystemLayout.SubtitleCacheFolder);
File.Copy(sourceFile, srtFile, true);
}
void PipelineAction(FFmpegPipeline pipeline)
{
// validate pipeline matches expectations (at a high level)
NewComplexFilter complexFilter = pipeline.PipelineSteps.OfType<NewComplexFilter>().First();
FilterChain filterChain = complexFilter.FilterChain;
if (profileBitDepth == FFmpegProfileBitDepth.TenBit)
// process.Arguments.Contains("=nv12") &&
// !process.Arguments.Contains("format=nv12,format=p010le[") &&
// !process.Arguments.Contains("hwdownload,format=nv12,subtitle") &&
// !process.Arguments.Contains("format=nv12,hwupload_cuda[st]") &&
// !process.Arguments.Contains("format=nv12,hwupload_cuda[wm]"))
{
var videoFilters = string.Join(",", filterChain.VideoFilterSteps.Map(f => f.Filter));
var pixelFormatFilters = string.Join(",", filterChain.PixelFormatFilterSteps.Map(f => f.Filter));
if (videoFilters.Contains("nv12") || (pixelFormatFilters.Contains("nv12") && !pixelFormatFilters.EndsWith("format=nv12,format=p010le")))
{
// Assert.Fail("10-bit shouldn't use NV12!");
}
}
bool hasDeinterlaceFilter = filterChain.VideoFilterSteps.Any(
s => s is YadifFilter or YadifCudaFilter or DeinterlaceQsvFilter or DeinterlaceVaapiFilter);
hasDeinterlaceFilter.Should().Be(videoScanKind == VideoScanKind.Interlaced);
bool hasScaling = filterChain.VideoFilterSteps.Filter(
s => s is ScaleFilter or ScaleCudaFilter or ScaleQsvFilter or ScaleVaapiFilter)
.Filter(s => s is not ScaleCudaFilter cuda || !cuda.Filter.Contains("scale_cuda=format="))
.Any();
// TODO: sometimes scaling is used for pixel format, so this is harder to assert the absence
if (profileResolution.Width != 1920)
{
hasScaling.Should().BeTrue();
}
// TODO: bit depth
bool hasPadding = filterChain.VideoFilterSteps.Any(s => s is PadFilter);
// TODO: optimize out padding
// hasPadding.Should().Be(padding == Padding.WithPadding);
if (padding == Padding.WithPadding)
{
hasPadding.Should().BeTrue();
}
bool hasSubtitleFilters =
filterChain.VideoFilterSteps.Any(s => s is SubtitlesFilter) ||
filterChain.SubtitleOverlayFilterSteps.Any(
s => s is OverlaySubtitleFilter
or OverlaySubtitleCudaFilter
or OverlaySubtitleQsvFilter
or OverlaySubtitleVaapiFilter);
hasSubtitleFilters.Should().Be(subtitle != Subtitle.None);
bool hasWatermarkFilters = filterChain.WatermarkOverlayFilterSteps.Any(
s => s is OverlayWatermarkFilter or OverlayWatermarkCudaFilter or OverlayWatermarkQsvFilter);
hasWatermarkFilters.Should().Be(watermark != Watermark.None);
}
Command process = await service.ForPlayoutItem(
ExecutableName("ffmpeg"),
ExecutableName("ffprobe"),
false,
new Channel(Guid.NewGuid())
{
Number = "1",
FFmpegProfile = FFmpegProfile.New("test", profileResolution) with
{
HardwareAcceleration = profileAcceleration,
VideoFormat = profileVideoFormat,
AudioFormat = FFmpegProfileAudioFormat.Aac,
DeinterlaceVideo = true,
BitDepth = profileBitDepth
},
StreamingMode = StreamingMode.TransportStream,
SubtitleMode = subtitleMode
},
v,
new MediaItemAudioVersion(null, v),
file,
file,
_ => subtitles.AsTask(),
string.Empty,
string.Empty,
string.Empty,
subtitleMode,
now,
now + TimeSpan.FromSeconds(5),
now,
Option<ChannelWatermark>.None,
channelWatermark,
VaapiDriver.Default,
"/dev/dri/renderD128",
Option<int>.None,
false,
FillerKind.None,
TimeSpan.Zero,
TimeSpan.FromSeconds(5),
0,
None,
false,
PipelineAction);
// Console.WriteLine($"ffmpeg arguments {string.Join(" ", process.StartInfo.ArgumentList)}");
string[] unsupportedMessages =
{
"No support for codec",
"No usable",
"Provided device doesn't support",
"Current pixel format is unsupported"
};
var sb = new StringBuilder();
var timeoutSignal = new CancellationTokenSource(TimeSpan.FromSeconds(30));
string tempFile = Path.GetTempFileName();
try
{
CommandResult result;
try
{
result = await process
.WithStandardOutputPipe(PipeTarget.ToFile(tempFile))
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(sb))
.ExecuteAsync(timeoutSignal.Token);
}
catch (OperationCanceledException)
{
var arguments = string.Join(
' ',
process.Arguments.Split(" ").Map(a => a.Contains('[') ? $"\"{a}\"" : a));
Assert.Fail($"Transcode failure (timeout): ffmpeg {arguments}");
return;
}
var error = sb.ToString();
bool isUnsupported = unsupportedMessages.Any(error.Contains);
if (profileAcceleration != HardwareAccelerationKind.None && isUnsupported)
{
result.ExitCode.Should().Be(1, $"Error message with successful exit code? {process.Arguments}");
Assert.Warn($"Unsupported on this hardware: ffmpeg {process.Arguments}");
}
else if (error.Contains("Impossible to convert between"))
{
var arguments = string.Join(
' ',
process.Arguments.Split(" ").Map(a => a.Contains('[') ? $"\"{a}\"" : a));
Assert.Fail($"Transcode failure: ffmpeg {arguments}");
}
else
{
var arguments = string.Join(
' ',
process.Arguments.Split(" ").Map(a => a.Contains('[') ? $"\"{a}\"" : a));
result.ExitCode.Should().Be(0, error + Environment.NewLine + arguments);
if (result.ExitCode == 0)
{
Console.WriteLine(process.Arguments);
}
}
// additional checks on resulting file
await localStatisticsProvider.RefreshStatistics(
ExecutableName("ffmpeg"),
ExecutableName("ffprobe"),
new Movie
{
MediaVersions = new List<MediaVersion>
{
new()
{
MediaFiles = new List<MediaFile>
{
new() { Path = tempFile }
}
}
}
});
// verify de-interlace
v.VideoScanKind.Should().NotBe(VideoScanKind.Interlaced);
// verify resolution
v.Height.Should().Be(profileResolution.Height);
v.Width.Should().Be(profileResolution.Width);
foreach (MediaStream videoStream in v.Streams.Filter(s => s.MediaStreamKind == MediaStreamKind.Video))
{
// verify pixel format
videoStream.PixelFormat.Should().Be(
profileBitDepth == FFmpegProfileBitDepth.TenBit ? PixelFormat.YUV420P10LE : PixelFormat.YUV420P);
// verify colors
var colorParams = new ColorParams(
videoStream.ColorRange,
videoStream.ColorSpace,
videoStream.ColorTransfer,
videoStream.ColorPrimaries);
// AMF doesn't seem to set this metadata properly
if (profileAcceleration != HardwareAccelerationKind.Amf)
{
colorParams.IsBt709.Should().BeTrue($"{colorParams}");
}
}
}
finally
{
if (File.Exists(tempFile))
{
File.Delete(tempFile);
}
}
}
private static async Task GenerateTestFile(
InputFormat inputFormat,
Padding padding,
VideoScanKind videoScanKind,
Subtitle subtitle,
string file)
{
string resolution = padding == Padding.WithPadding ? "1920x1060" : "1920x1080";
string videoFilter = videoScanKind == VideoScanKind.Interlaced
? "-vf interlace=scan=tff:lowpass=complex"
: string.Empty;
string flags = videoScanKind == VideoScanKind.Interlaced ? "-field_order tt -flags +ildct+ilme" : string.Empty;
string colorRange = !string.IsNullOrWhiteSpace(inputFormat.ColorRange)
? $" -color_range {inputFormat.ColorRange}"
: string.Empty;
string colorSpace = !string.IsNullOrWhiteSpace(inputFormat.ColorSpace)
? $" -colorspace {inputFormat.ColorSpace}"
: string.Empty;
string colorTransfer = !string.IsNullOrWhiteSpace(inputFormat.ColorTransfer)
? $" -color_trc {inputFormat.ColorTransfer}"
: string.Empty;
string colorPrimaries = !string.IsNullOrWhiteSpace(inputFormat.ColorPrimaries)
? $" -color_primaries {inputFormat.ColorPrimaries}"
: string.Empty;
string args =
$"-y -f lavfi -i anoisesrc=color=brown -f lavfi -i testsrc=duration=1:size={resolution}:rate=30 {videoFilter} -c:a aac -c:v {inputFormat.Encoder}{colorRange}{colorSpace}{colorTransfer}{colorPrimaries} -shortest -pix_fmt {inputFormat.PixelFormat} -strict -2 {flags} {file}";
var p1 = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = ExecutableName("ffmpeg"),
Arguments = args
}
};
p1.Start();
await p1.WaitForExitAsync();
// ReSharper disable once MethodHasAsyncOverload
p1.WaitForExit();
p1.ExitCode.Should().Be(0);
switch (subtitle)
{
case Subtitle.Text or Subtitle.Picture:
string sourceFile = Path.GetTempFileName() + ".mkv";
File.Move(file, sourceFile, true);
string tempFileName = Path.GetTempFileName() + ".mkv";
string subPath = Path.Combine(
TestContext.CurrentContext.TestDirectory,
"Resources",
subtitle == Subtitle.Picture ? "test.sup" : "test.srt");
var p2 = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = ExecutableName("mkvmerge"),
Arguments =
$"-o {tempFileName} {sourceFile} --field-order 0:{(videoScanKind == VideoScanKind.Interlaced ? '1' : '0')} {subPath}"
}
};
p2.Start();
await p2.WaitForExitAsync();
// ReSharper disable once MethodHasAsyncOverload
p2.WaitForExit();
if (p2.ExitCode != 0)
{
if (File.Exists(sourceFile))
{
File.Delete(sourceFile);
}
if (File.Exists(file))
{
File.Delete(file);
}
}
p2.ExitCode.Should().Be(0);
await SetInterlacedFlag(tempFileName, sourceFile, file, videoScanKind == VideoScanKind.Interlaced);
File.Move(tempFileName, file, true);
break;
}
}
private static async Task SetInterlacedFlag(string tempFileName, string sourceFile, string file, bool interlaced)
{
var p = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = ExecutableName("mkvpropedit"),
Arguments = $"{tempFileName} --edit track:v1 --set interlaced={(interlaced ? '1' : '0')}",
}
};
p.Start();
await p.WaitForExitAsync();
// ReSharper disable once MethodHasAsyncOverload
p.WaitForExit();
if (p.ExitCode != 0)
{
if (File.Exists(sourceFile))
{
File.Delete(sourceFile);
}
if (File.Exists(file))
{
File.Delete(file);
}
}
p.ExitCode.Should().Be(0);
}
private static string GetStringSha256Hash(string text)
{
if (string.IsNullOrEmpty(text))
{
return string.Empty;
}
using var sha = SHA256.Create();
byte[] textData = Encoding.UTF8.GetBytes(text);
byte[] hash = sha.ComputeHash(textData);
return BitConverter.ToString(hash).Replace("-", string.Empty);
}
private class FakeStreamSelector : IFFmpegStreamSelector
{
public Task<MediaStream> SelectVideoStream(MediaVersion version) =>
version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Video).AsTask();
public Task<Option<MediaStream>> SelectAudioStream(
MediaItemAudioVersion version,
StreamingMode streamingMode,
Channel channel,
string preferredAudioLanguage,
string preferredAudioTitle) =>
Optional(version.MediaVersion.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Audio)).AsTask();
public Task<Option<Domain.Subtitle>> SelectSubtitleStream(
List<Domain.Subtitle> subtitles,
Channel channel,
string preferredSubtitleLanguage,
ChannelSubtitleMode subtitleMode) =>
subtitles.HeadOrNone().AsTask();
}
private static string ExecutableName(string baseName) =>
OperatingSystem.IsWindows() ? $"{baseName}.exe" : baseName;
}
@@ -1,36 +0,0 @@
using Bugsnag;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.Metadata;
[TestFixture]
public class LocalStatisticsProviderTests
{
[Test]
// this needs to be a culture where '.' is a group separator
[SetCulture("it-IT")]
public void Test()
{
var provider = new LocalStatisticsProvider(
new Mock<IMetadataRepository>().Object,
new Mock<ILocalFileSystem>().Object,
new Mock<IClient>().Object,
new Mock<ILogger<LocalStatisticsProvider>>().Object);
var input = new LocalStatisticsProvider.FFprobe(
new LocalStatisticsProvider.FFprobeFormat("123.45", null),
new List<LocalStatisticsProvider.FFprobeStream>(),
new List<LocalStatisticsProvider.FFprobeChapter>());
MediaVersion result = provider.ProjectToMediaVersion("test", input);
result.Duration.Should().Be(TimeSpan.FromSeconds(123.45));
}
}
@@ -1,108 +0,0 @@
using System.Globalization;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Core.Tests.Fakes;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.Metadata;
[TestFixture]
public class LocalSubtitlesProviderTests
{
// test cases are from plex's example folder layout here
// https://support.plex.tv/articles/200471133-adding-local-subtitles-to-your-media/
// /Movies
// /Avatar (2009)
// Avatar (2009).mkv
// Avatar (2009).eng.srt
// Avatar (2009).en.forced.ass
// Avatar (2009).en.sdh.srt
// Avatar (2009).de.srt
// Avatar (2009).de.sdh.forced.srt
[Test]
public void Should_Find_All_Languages_Codecs_And_Flags_With_Full_Paths()
{
// normally this will have a full list from the database, but we just need these two for testing
var cultures = new List<CultureInfo>
{
CultureInfo.GetCultureInfo("en-US"),
CultureInfo.GetCultureInfo("de-DE")
};
var fakeFiles = new List<FakeFileEntry>
{
new(@"/Movies/Avatar (2009)/Avatar (2009).mkv"),
new(@"/Movies/Avatar (2009)/Avatar (2009).eng.srt"),
new(@"/Movies/Avatar (2009)/Avatar (2009).en.forced.ass"),
new(@"/Movies/Avatar (2009)/Avatar (2009).en.sdh.srt"),
new(@"/Movies/Avatar (2009)/Avatar (2009).de.srt"),
new(@"/Movies/Avatar (2009)/Avatar (2009).de.sdh.forced.srt")
};
var provider = new LocalSubtitlesProvider(
new Mock<IMediaItemRepository>().Object,
new Mock<IMetadataRepository>().Object,
new FakeLocalFileSystem(fakeFiles),
new Mock<ILogger<LocalSubtitlesProvider>>().Object);
List<Subtitle> result = provider.LocateExternalSubtitles(
cultures,
@"/Movies/Avatar (2009)/Avatar (2009).mkv",
true);
result.Count.Should().Be(5);
result.Count(s => s.Language == "eng").Should().Be(3);
result.Count(s => s.Language == "deu").Should().Be(2);
result.Count(s => s.Forced).Should().Be(2);
result.Count(s => s.SDH).Should().Be(2);
result.Count(s => s.Codec == "subrip").Should().Be(4);
result.Count(s => s.Codec == "ass").Should().Be(1);
result.All(s => s.Path.Contains(@"/Movies/Avatar (2009)/")).Should().BeTrue();
}
[Test]
public void Should_Find_All_Languages_Codecs_And_Flags_With_File_Names()
{
// normally this will have a full list from the database, but we just need these two for testing
var cultures = new List<CultureInfo>
{
CultureInfo.GetCultureInfo("en-US"),
CultureInfo.GetCultureInfo("de-DE")
};
var fakeFiles = new List<FakeFileEntry>
{
new(@"/Movies/Avatar (2009)/Avatar (2009).mkv"),
new(@"/Movies/Avatar (2009)/Avatar (2009).eng.srt"),
new(@"/Movies/Avatar (2009)/Avatar (2009).en.forced.ass"),
new(@"/Movies/Avatar (2009)/Avatar (2009).en.sdh.srt"),
new(@"/Movies/Avatar (2009)/Avatar (2009).de.srt"),
new(@"/Movies/Avatar (2009)/Avatar (2009).de.sdh.forced.srt")
};
var provider = new LocalSubtitlesProvider(
new Mock<IMediaItemRepository>().Object,
new Mock<IMetadataRepository>().Object,
new FakeLocalFileSystem(fakeFiles),
new Mock<ILogger<LocalSubtitlesProvider>>().Object);
List<Subtitle> result = provider.LocateExternalSubtitles(
cultures,
@"/Movies/Avatar (2009)/Avatar (2009).mkv",
false);
result.Count.Should().Be(5);
result.Count(s => s.Language == "eng").Should().Be(3);
result.Count(s => s.Language == "deu").Should().Be(2);
result.Count(s => s.Forced).Should().Be(2);
result.Count(s => s.SDH).Should().Be(2);
result.Count(s => s.Codec == "subrip").Should().Be(4);
result.Count(s => s.Codec == "ass").Should().Be(1);
result.Count(s => s.Path.Contains(@"/Movies/Avatar (2009)/")).Should().Be(0);
}
}
@@ -1,216 +0,0 @@
using System.Text;
using Bugsnag;
using ErsatzTV.Core.Metadata.Nfo;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.IO;
using Moq;
using NUnit.Framework;
using Serilog;
namespace ErsatzTV.Core.Tests.Metadata.Nfo;
[TestFixture]
public class ArtistNfoReaderTests
{
[SetUp]
public void SetUp() => _artistNfoReader = new ArtistNfoReader(
new RecyclableMemoryStreamManager(),
new Mock<IClient>().Object,
_logger);
private readonly ILogger<ArtistNfoReader> _logger;
public ArtistNfoReaderTests()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
ILoggerFactory loggerFactory = new LoggerFactory().AddSerilog(Log.Logger);
_logger = loggerFactory.CreateLogger<ArtistNfoReader>();
}
private ArtistNfoReader _artistNfoReader;
[Test]
public async Task ParsingNfo_Should_Return_Error()
{
await using var stream =
new MemoryStream(Encoding.UTF8.GetBytes(@"https://www.themoviedb.org/movie/11-star-wars"));
Either<BaseError, ArtistNfo> result = await _artistNfoReader.Read(stream);
result.IsLeft.Should().BeTrue();
}
[Test]
public async Task MetadataNfo_Should_Return_Nfo()
{
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"<artist></artist>"));
Either<BaseError, ArtistNfo> result = await _artistNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
}
[Test]
public async Task CombinationNfo_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<artist></artist>
https://www.themoviedb.org/movie/11-star-wars"));
Either<BaseError, ArtistNfo> result = await _artistNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
}
[Test]
public async Task FullSample_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
NormalizeLineEndingsLF(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes"" ?>
<artist>
<name>Billy Joel</name>
<musicBrainzArtistID>64b94289-9474-4d43-8c93-918ccc1920d1</musicBrainzArtistID>
<sortname>Joel, Billy</sortname>
<type>Person</type>
<gender>Male</gender>
<disambiguation></disambiguation>
<genre>Pop/Rock</genre>
<style>Album Rock</style>
<style>Contemporary Pop/Rock</style>
<style>Singer/Songwriter</style>
<style>Soft Rock</style>
<style>Keyboard</style>
<mood>Amiable/Good-Natured</mood>
<mood>Autumnal</mood>
<mood>Nostalgic</mood>
<mood>Refined</mood>
<mood>Acerbic</mood>
<mood>Bittersweet</mood>
<mood>Brash</mood>
<mood>Cynical/Sarcastic</mood>
<mood>Earnest</mood>
<yearsactive>1960s - 2010s</yearsactive>
<born>1949-05-09</born>
<formed>1964</formed>
<biography>William Martin &quot;Billy&quot; Joel (born May 9, 1949, New York, USA) is an American pianist, singer-songwriter, and composer. Since releasing his first hit song, &quot;Piano Man&quot;, in 1973, Joel has become the sixth-best-selling recording artist and the third-best-selling solo artist in the United States, according to the RIAA. His compilation album Greatest Hits Vol. 1 &amp; 2 is the third-best-selling album in the United States by discs shipped.&#x0A;Joel had Top 40 hits in the 1970s, 1980s, and 1990s, achieving 33 Top 40 hits in the United States, all of which he wrote himself. He is also a six-time Grammy Award winner, a 23-time Grammy nominee and one of the world&apos;s best-selling artists of all time, having sold over 150 million records worldwide. He was inducted into the Songwriter&apos;s Hall of Fame (1992), the Rock and Roll Hall of Fame (1999), and the Long Island Music Hall of Fame (2006). In 2008, Billboard magazine released a list of the Hot 100 All-Time Top Artists to celebrate the US singles chart&apos;s 50th anniversary, with Billy Joel positioned at No. 23. With the exception of the 2007 songs &quot;All My Life&quot; and &quot;Christmas in Fallujah&quot;, Joel stopped writing and recording popular music after 1993&apos;s River of Dreams, but he continued to tour extensively until 2010. Joel was born in the Bronx, May 9, 1949 and raised in Hicksville, New York in a Levitt home. His father, Howard (born Helmuth), was born in Germany, the son of German merchant and manufacturer Karl Amson Joel, who, after the advent of the Nazi regime, emigrated to Switzerland and later to the United States. Billy Joel&apos;s mother, Rosalind Nyman, was born in England to Philip and Rebecca Nyman. Both of Joel&apos;s parents were Jewish. They divorced in 1960, and his father moved to Vienna, Austria. Billy has a sister, Judith Joel, and a half-brother, Alexander Joel, who is an acclaimed classical conductor in Europe and currently chief musical director of the Staatstheater Braunschweig.&#x0A;Joel&apos;s father was an accomplished classical pianist. Billy reluctantly began piano lessons at an early age, at his mother&apos;s insistence; his teachers included the noted American pianist Morton Estrin and musician/songwriter Timothy Ford. His interest in music, rather than sports, was a source of teasing and bullying in his early years. (He has said in interviews that his piano instructor also taught ballet. Her name was Frances Neiman, and she was a Juilliard trained musician. She gave both classical piano and ballet lessons in the studio attached to the rear of her house, leading neighborhood bullies to mistakenly assume that he was learning to dance.) As a teenager, Joel took up boxing so that he would be able to defend himself. He boxed successfully on the amateur Golden Gloves circuit for a short time, winning twenty-two bouts, but abandoned the sport shortly after breaking his nose in his twenty-fourth boxing match.&#x0A;Joel attended Hicksville High School in 1967, but he did not graduate with his class. He had been helping his single mother make ends meet by playing at a piano bar, which interfered with his school attendance. At the end of his senior year, Joel did not have enough credits to graduate. Rather than attend summer school to earn his diploma, however, Joel decided to immediately begin a career in music. Joel recounted, &quot;I told them, &apos;To hell with it. If I&apos;m not going to Columbia University, I&apos;m going to Columbia Records, and you don&apos;t need a high school diploma over there&apos;.&quot; Columbia did, in fact, become the label that eventually signed him. In 1992, he submitted essays to the school board and was awarded his diploma at Hicksville High&apos;s annual graduation ceremony, 25 years after he had left.</biography>
<died></died>
<disbanded></disbanded>
<thumb spoof="""" cache="""" aspect=""thumb"" preview=""https://assets.fanart.tv/preview/music/64b94289-9474-4d43-8c93-918ccc1920d1/artistthumb/joel-billy-541603848114c.jpg"">https://assets.fanart.tv/fanart/music/64b94289-9474-4d43-8c93-918ccc1920d1/artistthumb/joel-billy-541603848114c.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""thumb"" preview=""https://www.theaudiodb.com/images/media/artist/thumb/ttsxwr1425765041.jpg/preview"">https://www.theaudiodb.com/images/media/artist/thumb/ttsxwr1425765041.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""thumb"" preview=""https://rovimusic.rovicorp.com/image.jpg?c=73pC-Gp0OovlmiQL7Wp5Yd_M69_UI9rrJSVvWL2-yAg=&amp;f=2"">https://rovimusic.rovicorp.com/image.jpg?c=73pC-Gp0OovlmiQL7Wp5Yd_M69_UI9rrJSVvWL2-yAg=&amp;f=0</thumb>
<thumb spoof="""" cache="""" aspect=""thumb"" preview=""https://img.discogs.com/J3bqAiLmdr2gXsetNgSQF2W-f6M=/150x150/smart/filters:strip_icc():format(jpeg):mode_rgb():quality(40)/discogs-images/A-137418-1143052539.jpeg.jpg"">https://img.discogs.com/u7cfC3lZo9JGRdukSttJTZKr9Go=/350x255/smart/filters:strip_icc():format(jpeg):mode_rgb():quality(90)/discogs-images/A-137418-1143052539.jpeg.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""clearlogo"" preview=""https://www.theaudiodb.com/images/media/artist/logo/tvqpys1367246337.png/preview"">https://www.theaudiodb.com/images/media/artist/logo/tvqpys1367246337.png</thumb>
<thumb spoof="""" cache="""" aspect=""clearart"" preview=""https://www.theaudiodb.com/images/media/artist/clearart/yqpsuq1523892204.png/preview"">https://www.theaudiodb.com/images/media/artist/clearart/yqpsuq1523892204.png</thumb>
<thumb spoof="""" cache="""" aspect=""landscape"" preview=""https://www.theaudiodb.com/images/media/artist/widethumb/tywpqx1530815867.jpg/preview"">https://www.theaudiodb.com/images/media/artist/widethumb/tywpqx1530815867.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""banner"" preview=""https://assets.fanart.tv/preview/music/64b94289-9474-4d43-8c93-918ccc1920d1/musicbanner/joel-billy-5914e7759bfcd.jpg"">https://assets.fanart.tv/fanart/music/64b94289-9474-4d43-8c93-918ccc1920d1/musicbanner/joel-billy-5914e7759bfcd.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""clearlogo"" preview=""https://assets.fanart.tv/preview/music/64b94289-9474-4d43-8c93-918ccc1920d1/hdmusiclogo/joel-billy-550b259604412.png"">https://assets.fanart.tv/fanart/music/64b94289-9474-4d43-8c93-918ccc1920d1/hdmusiclogo/joel-billy-550b259604412.png</thumb>
<thumb spoof="""" cache="""" aspect=""fanart"" preview=""https://assets.fanart.tv/preview/music/64b94289-9474-4d43-8c93-918ccc1920d1/artistbackground/joel-billy-4fc0c2dad9ab7.jpg"">https://assets.fanart.tv/fanart/music/64b94289-9474-4d43-8c93-918ccc1920d1/artistbackground/joel-billy-4fc0c2dad9ab7.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""fanart"" preview=""https://www.theaudiodb.com/images/media/artist/fanart/uwqtup1521206367.jpg/preview"">https://www.theaudiodb.com/images/media/artist/fanart/uwqtup1521206367.jpg</thumb>
<path>F:\Music\ArtistInfoKodi\Billy Joel</path>
</artist>")));
Either<BaseError, ArtistNfo> result = await _artistNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (ArtistNfo nfo in result.RightToSeq())
{
nfo.Name.Should().Be("Billy Joel");
nfo.Disambiguation.Should().BeNullOrEmpty();
nfo.Genres.Should().BeEquivalentTo(new List<string> { "Pop/Rock" });
nfo.Styles.Should().BeEquivalentTo(
new List<string>
{
"Album Rock",
"Contemporary Pop/Rock",
"Singer/Songwriter",
"Soft Rock",
"Keyboard"
});
nfo.Moods.Should().BeEquivalentTo(
new List<string>
{
"Amiable/Good-Natured",
"Autumnal",
"Nostalgic",
"Refined",
"Acerbic",
"Bittersweet",
"Brash",
"Cynical/Sarcastic",
"Earnest"
});
nfo.Biography.Should().Be(
NormalizeLineEndingsLF(
@"William Martin ""Billy"" Joel (born May 9, 1949, New York, USA) is an American pianist, singer-songwriter, and composer. Since releasing his first hit song, ""Piano Man"", in 1973, Joel has become the sixth-best-selling recording artist and the third-best-selling solo artist in the United States, according to the RIAA. His compilation album Greatest Hits Vol. 1 & 2 is the third-best-selling album in the United States by discs shipped.
Joel had Top 40 hits in the 1970s, 1980s, and 1990s, achieving 33 Top 40 hits in the United States, all of which he wrote himself. He is also a six-time Grammy Award winner, a 23-time Grammy nominee and one of the world's best-selling artists of all time, having sold over 150 million records worldwide. He was inducted into the Songwriter's Hall of Fame (1992), the Rock and Roll Hall of Fame (1999), and the Long Island Music Hall of Fame (2006). In 2008, Billboard magazine released a list of the Hot 100 All-Time Top Artists to celebrate the US singles chart's 50th anniversary, with Billy Joel positioned at No. 23. With the exception of the 2007 songs ""All My Life"" and ""Christmas in Fallujah"", Joel stopped writing and recording popular music after 1993's River of Dreams, but he continued to tour extensively until 2010. Joel was born in the Bronx, May 9, 1949 and raised in Hicksville, New York in a Levitt home. His father, Howard (born Helmuth), was born in Germany, the son of German merchant and manufacturer Karl Amson Joel, who, after the advent of the Nazi regime, emigrated to Switzerland and later to the United States. Billy Joel's mother, Rosalind Nyman, was born in England to Philip and Rebecca Nyman. Both of Joel's parents were Jewish. They divorced in 1960, and his father moved to Vienna, Austria. Billy has a sister, Judith Joel, and a half-brother, Alexander Joel, who is an acclaimed classical conductor in Europe and currently chief musical director of the Staatstheater Braunschweig.
Joel's father was an accomplished classical pianist. Billy reluctantly began piano lessons at an early age, at his mother's insistence; his teachers included the noted American pianist Morton Estrin and musician/songwriter Timothy Ford. His interest in music, rather than sports, was a source of teasing and bullying in his early years. (He has said in interviews that his piano instructor also taught ballet. Her name was Frances Neiman, and she was a Juilliard trained musician. She gave both classical piano and ballet lessons in the studio attached to the rear of her house, leading neighborhood bullies to mistakenly assume that he was learning to dance.) As a teenager, Joel took up boxing so that he would be able to defend himself. He boxed successfully on the amateur Golden Gloves circuit for a short time, winning twenty-two bouts, but abandoned the sport shortly after breaking his nose in his twenty-fourth boxing match.
Joel attended Hicksville High School in 1967, but he did not graduate with his class. He had been helping his single mother make ends meet by playing at a piano bar, which interfered with his school attendance. At the end of his senior year, Joel did not have enough credits to graduate. Rather than attend summer school to earn his diploma, however, Joel decided to immediately begin a career in music. Joel recounted, ""I told them, 'To hell with it. If I'm not going to Columbia University, I'm going to Columbia Records, and you don't need a high school diploma over there'."" Columbia did, in fact, become the label that eventually signed him. In 1992, he submitted essays to the school board and was awarded his diploma at Hicksville High's annual graduation ceremony, 25 years after he had left."));
}
}
[Test]
public async Task MetadataNfo_With_Disambiguation_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(@"<artist><disambiguation>Test Disambiguation</disambiguation></artist>"));
Either<BaseError, ArtistNfo> result = await _artistNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (ArtistNfo nfo in result.RightToSeq())
{
nfo.Disambiguation.Should().Be("Test Disambiguation");
}
}
[Test]
public async Task Invalid_Characters_End_Should_Abort_And_Return_Nfo()
{
string sourceFile = Path.Combine(
TestContext.CurrentContext.TestDirectory,
"Resources",
"Nfo",
"ArtistInvalidCharacters1.nfo");
Either<BaseError, ArtistNfo> result = await _artistNfoReader.ReadFromFile(sourceFile);
result.IsRight.Should().BeTrue();
foreach (ArtistNfo nfo in result.RightToSeq())
{
nfo.Name.Should().Be("Test Name");
}
}
[Test]
public async Task Invalid_Characters_Middle_Should_Continue_And_Return_Nfo()
{
string sourceFile = Path.Combine(
TestContext.CurrentContext.TestDirectory,
"Resources",
"Nfo",
"ArtistInvalidCharacters2.nfo");
Either<BaseError, ArtistNfo> result = await _artistNfoReader.ReadFromFile(sourceFile);
result.IsRight.Should().BeTrue();
foreach (ArtistNfo nfo in result.RightToSeq())
{
nfo.Name.Should().Be("Test Name");
nfo.Moods.Should().BeEquivalentTo(new List<string> { "Test Mood" });
nfo.Styles.Count.Should().Be(1);
}
}
private static string NormalizeLineEndingsLF(string str) =>
str
.Replace("\r\n", "\n")
.Replace("\r", "\n");
}
@@ -1,445 +0,0 @@
using System.Text;
using Bugsnag;
using ErsatzTV.Core.Metadata.Nfo;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.IO;
using Moq;
using NUnit.Framework;
using Serilog;
namespace ErsatzTV.Core.Tests.Metadata.Nfo;
[TestFixture]
public class EpisodeNfoReaderTests
{
[SetUp]
public void SetUp() => _episodeNfoReader = new EpisodeNfoReader(
new RecyclableMemoryStreamManager(),
new Mock<IClient>().Object,
_logger);
private readonly ILogger<EpisodeNfoReader> _logger;
public EpisodeNfoReaderTests()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
ILoggerFactory loggerFactory = new LoggerFactory().AddSerilog(Log.Logger);
_logger = loggerFactory.CreateLogger<EpisodeNfoReader>();
}
private EpisodeNfoReader _episodeNfoReader;
[Test]
public async Task One()
{
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<!--created on whatever - comment-->
<episodedetails>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
}
}
[Test]
public async Task Two()
{
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<!--created on whatever - comment-->
<episodedetails>
<showtitle>show</showtitle>
<title>episode-one</title>
<episode>1</episode>
<season>1</season>
</episodedetails>
<episodedetails>
<showtitle>show</showtitle>
<title>episode-two</title>
<episode>2</episode>
<season>1</season>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(2);
list.All(nfo => nfo.ShowTitle == "show").Should().BeTrue();
list.All(nfo => nfo.Season == 1).Should().BeTrue();
list.Count(nfo => nfo.Title == "episode-one" && nfo.Episode == 1).Should().Be(1);
list.Count(nfo => nfo.Title == "episode-two" && nfo.Episode == 2).Should().Be(1);
}
}
[Test]
public async Task UniqueIds()
{
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<!--created on whatever - comment-->
<episodedetails>
<uniqueid default=""true"" type=""tvdb"">12345</uniqueid>
<uniqueid default=""false"" type=""imdb"">tt54321</uniqueid>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
list[0].UniqueIds.Count.Should().Be(2);
list[0].UniqueIds.Count(id => id.Default && id.Type == "tvdb" && id.Guid == "12345").Should().Be(1);
list[0].UniqueIds.Count(id => !id.Default && id.Type == "imdb" && id.Guid == "tt54321").Should().Be(1);
}
}
[Test]
public async Task No_ContentRating()
{
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<!--created on whatever - comment-->
<episodedetails>
<mpaa/>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
list[0].ContentRating.Should().BeNullOrEmpty();
}
}
[Test]
public async Task ContentRating()
{
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<!--created on whatever - comment-->
<episodedetails>
<mpaa>US:Something</mpaa>
</episodedetails>
<episodedetails>
<mpaa>US:Something / US:SomethingElse</mpaa>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(2);
list.Count(nfo => nfo.ContentRating == "US:Something").Should().Be(1);
list.Count(nfo => nfo.ContentRating == "US:Something / US:SomethingElse").Should().Be(1);
}
}
[Test]
public async Task No_Plot()
{
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<!--created on whatever - comment-->
<episodedetails>
<plot/>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
list[0].Plot.Should().BeNullOrEmpty();
}
}
[Test]
public async Task Plot()
{
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<!--created on whatever - comment-->
<episodedetails>
<plot>Some Plot</plot>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
list[0].Plot.Should().Be("Some Plot");
}
}
[Test]
public async Task Actors()
{
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<!--created on whatever - comment-->
<episodedetails>
<actor>
<name>Name 1</name>
<role>Role 1</role>
<thumb>Thumb 1</thumb>
</actor>
<actor>
<name>Name 2</name>
<role>Role 2</role>
<thumb>Thumb 2</thumb>
</actor>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
list[0].Actors.Count.Should().Be(2);
list[0].Actors.Count(a => a.Name == "Name 1" && a.Role == "Role 1" && a.Thumb == "Thumb 1")
.Should().Be(1);
list[0].Actors.Count(a => a.Name == "Name 2" && a.Role == "Role 2" && a.Thumb == "Thumb 2")
.Should().Be(1);
}
}
[Test]
public async Task Writers()
{
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<!--created on whatever - comment-->
<episodedetails>
<credits>Writer 1</credits>
</episodedetails>
<episodedetails>
<credits>Writer 2</credits>
<credits>Writer 3</credits>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(2);
list.Count(nfo => nfo.Writers.Count == 1 && nfo.Writers[0] == "Writer 1").Should().Be(1);
list.Count(nfo => nfo.Writers.Count == 2 && nfo.Writers[0] == "Writer 2" && nfo.Writers[1] == "Writer 3")
.Should().Be(1);
}
}
[Test]
public async Task Directors()
{
var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<!--created on whatever - comment-->
<episodedetails>
<director>Director 1</director>
</episodedetails>
<episodedetails>
<director>Director 2</director>
<director>Director 3</director>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(2);
list.Count(nfo => nfo.Directors.Count == 1 && nfo.Directors[0] == "Director 1").Should().Be(1);
list.Count(
nfo => nfo.Directors.Count == 2 && nfo.Directors[0] == "Director 2" &&
nfo.Directors[1] == "Director 3")
.Should().Be(1);
}
}
[Test]
public async Task FullSample_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes"" ?>
<episodedetails>
<title>Filmed Before a Live Studio Audience</title>
<showtitle>WandaVision</showtitle>
<ratings>
<rating name=""imdb"" max=""10"" default=""true"">
<value>7.500000</value>
<votes>18766</votes>
</rating>
<rating name=""tmdb"" max=""10"">
<value>7.500000</value>
<votes>42</votes>
</rating>
<rating name=""trakt"" max=""10"">
<value>6.952780</value>
<votes>3621</votes>
</rating>
</ratings>
<userrating>0</userrating>
<top250>0</top250>
<season>1</season>
<episode>1</episode>
<displayseason>-1</displayseason>
<displayepisode>-1</displayepisode>
<outline></outline>
<plot>Wanda and Vision struggle to conceal their powers during dinner with Vision’s boss and his wife.</plot>
<tagline></tagline>
<runtime>26</runtime>
<thumb spoof="""" cache="""" aspect=""thumb"" preview=""https://image.tmdb.org/t/p/w780/cbe8l0Hnbvu07ePgoOopyWYrcdL.jpg"">https://image.tmdb.org/t/p/original/cbe8l0Hnbvu07ePgoOopyWYrcdL.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""thumb"" preview=""https://image.tmdb.org/t/p/w780/oNCzeCXFanVEWNpzRzyffhLLfZs.jpg"">https://image.tmdb.org/t/p/original/oNCzeCXFanVEWNpzRzyffhLLfZs.jpg</thumb>
<mpaa>Australia:TV-14</mpaa>
<playcount>1</playcount>
<lastplayed>2021-03-27</lastplayed>
<id>1830976</id>
<uniqueid type=""imdb"">tt9601584</uniqueid>
<uniqueid type=""tmdb"" default=""true"">1830976</uniqueid>
<uniqueid type=""tvdb"">8042515</uniqueid>
<genre>Sci-Fi &amp; Fantasy</genre>
<genre>Mystery</genre>
<genre>Drama</genre>
<credits>Jac Schaeffer</credits>
<director>Matt Shakman</director>
<premiered>2021-01-15</premiered>
<year>2021</year>
<status></status>
<code></code>
<aired>2021-01-15</aired>
<studio>Disney+ (US)</studio>
<trailer></trailer>
<fileinfo>
<streamdetails>
<video>
<codec>h264</codec>
<aspect>1.777778</aspect>
<width>1280</width>
<height>720</height>
<durationinseconds>1593</durationinseconds>
<stereomode></stereomode>
</video>
<audio>
<codec>aac</codec>
<language>eng</language>
<channels>2</channels>
</audio>
</streamdetails>
</fileinfo>
<actor>
<name>Randall Park</name>
<role>Jimmy Woo</role>
<order>4</order>
<thumb>https://image.tmdb.org/t/p/original/1QJ4cBQZoOaLR8Hc3V2NgBLvB0f.jpg</thumb>
</actor>
<actor>
<name>Kat Dennings</name>
<role>Darcy Lewis / The Escape Artist</role>
<order>5</order>
<thumb>https://image.tmdb.org/t/p/original/rrfyo9z1wW5nY9ZsFlj1Ozfj9g2.jpg</thumb>
</actor>
<resume>
<position>0.000000</position>
<total>0.000000</total>
</resume>
<dateadded>2021-02-02 11:57:44</dateadded>
</episodedetails>"));
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (TvShowEpisodeNfo nfo in result.RightToSeq().Flatten())
{
nfo.ShowTitle.Should().Be("WandaVision");
nfo.Title.Should().Be("Filmed Before a Live Studio Audience");
nfo.Episode.Should().Be(1);
nfo.Season.Should().Be(1);
nfo.ContentRating.Should().Be("Australia:TV-14");
nfo.Aired.IsSome.Should().BeTrue();
foreach (DateTime aired in nfo.Aired)
{
aired.Should().Be(new DateTime(2021, 01, 15));
}
nfo.Plot.Should().Be(
"Wanda and Vision struggle to conceal their powers during dinner with Vision’s boss and his wife.");
nfo.Actors.Should().BeEquivalentTo(
new List<ActorNfo>
{
new()
{
Name = "Randall Park", Order = 4, Role = "Jimmy Woo",
Thumb = "https://image.tmdb.org/t/p/original/1QJ4cBQZoOaLR8Hc3V2NgBLvB0f.jpg"
},
new()
{
Name = "Kat Dennings", Order = 5, Role = "Darcy Lewis / The Escape Artist",
Thumb = "https://image.tmdb.org/t/p/original/rrfyo9z1wW5nY9ZsFlj1Ozfj9g2.jpg"
}
});
nfo.Writers.Should().BeEquivalentTo(new List<string> { "Jac Schaeffer" });
nfo.Directors.Should().BeEquivalentTo(new List<string> { "Matt Shakman" });
nfo.UniqueIds.Should().BeEquivalentTo(
new List<UniqueIdNfo>
{
new() { Type = "imdb", Guid = "tt9601584", Default = false },
new() { Type = "tmdb", Guid = "1830976", Default = true },
new() { Type = "tvdb", Guid = "8042515", Default = false }
});
}
}
[Test]
public async Task Invalid_Characters_Should_Abort_And_Return_Nfo()
{
string sourceFile = Path.Combine(
TestContext.CurrentContext.TestDirectory,
"Resources",
"Nfo",
"EpisodeInvalidCharacters.nfo");
Either<BaseError, List<TvShowEpisodeNfo>> result = await _episodeNfoReader.ReadFromFile(sourceFile);
result.IsRight.Should().BeTrue();
foreach (List<TvShowEpisodeNfo> list in result.RightToSeq())
{
list.Count.Should().Be(1);
list[0].Title.Should().Be("Test Title");
}
}
}
@@ -1,252 +0,0 @@
using System.Text;
using Bugsnag;
using ErsatzTV.Core.Metadata.Nfo;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.IO;
using Moq;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.Metadata.Nfo;
[TestFixture]
public class MovieNfoReaderTests
{
[SetUp]
public void SetUp() => _movieNfoReader = new MovieNfoReader(
new RecyclableMemoryStreamManager(),
new Mock<IClient>().Object,
new NullLogger<MovieNfoReader>());
private MovieNfoReader _movieNfoReader;
[Test]
public async Task ParsingNfo_Should_Return_Error()
{
await using var stream =
new MemoryStream(Encoding.UTF8.GetBytes(@"https://www.themoviedb.org/movie/11-star-wars"));
Either<BaseError, MovieNfo> result = await _movieNfoReader.Read(stream);
result.IsLeft.Should().BeTrue();
}
[Test]
public async Task MetadataNfo_Should_Return_Nfo()
{
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"<movie></movie>"));
Either<BaseError, MovieNfo> result = await _movieNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
}
[Test]
public async Task CombinationNfo_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<movie></movie>
https://www.themoviedb.org/movie/11-star-wars"));
Either<BaseError, MovieNfo> result = await _movieNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
}
[Test]
public async Task FullSample_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes"" ?>
<movie>
<title>Zack Snyder&apos;s Justice League</title>
<originaltitle>Zack Snyder&apos;s Justice League</originaltitle>
<sorttitle>Justice League 2</sorttitle>
<ratings>
<rating name=""imdb"" max=""10"" default=""true"">
<value>8.300000</value>
<votes>197786</votes>
</rating>
<rating name=""themoviedb"" max=""10"">
<value>8.700000</value>
<votes>3461</votes>
</rating>
<rating name=""trakt"" max=""10"">
<value>8.195670</value>
<votes>4247</votes>
</rating>
</ratings>
<userrating>0</userrating>
<top250>140</top250>
<outline></outline>
<plot>Determined to ensure Superman&apos;s ultimate sacrifice was not in vain, Bruce Wayne aligns forces with Diana Prince with plans to recruit a team of metahumans to protect the world from an approaching threat of catastrophic proportions.</plot>
<tagline></tagline>
<runtime>242</runtime>
<thumb spoof="""" cache="""" aspect=""poster"" preview="""">https://assets.fanart.tv/fanart/movies/791373/movieposter/zack-snyders-justice-league-603fdb873f474.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""poster"" preview="""">https://image.tmdb.org/t/p/original/tnAuB8q5vv7Ax9UAEje5Xi4BXik.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""landscape"" preview="""">https://assets.fanart.tv/fanart/movies/791373/moviethumb/zack-snyders-justice-league-6050310135cf6.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""landscape"" preview="""">https://image.tmdb.org/t/p/original/wcYBuOZDP6Vi8Ye4qax3Zx9dCan.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""keyart"" preview="""">https://assets.fanart.tv/fanart/movies/791373/movieposter/zack-snyders-justice-league-603fdba9bdd16.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""clearlogo"" preview="""">https://assets.fanart.tv/fanart/movies/791373/hdmovielogo/zack-snyders-justice-league-5ed3f2e4952e9.png</thumb>
<thumb spoof="""" cache="""" aspect=""banner"" preview="""">https://assets.fanart.tv/fanart/movies/791373/moviebanner/zack-snyders-justice-league-6050049514d4c.jpg</thumb>
<fanart>
<thumb colors="""" preview=""https://assets.fanart.tv/preview/movies/791373/moviebackground/zack-snyders-justice-league-5fee5b9fe0e0d.jpg"">https://assets.fanart.tv/fanart/movies/791373/moviebackground/zack-snyders-justice-league-5fee5b9fe0e0d.jpg</thumb>
<thumb colors="""" preview=""https://image.tmdb.org/t/p/w780/43NwryODVEsbBDC0jK3wYfVyb5q.jpg"">https://image.tmdb.org/t/p/original/43NwryODVEsbBDC0jK3wYfVyb5q.jpg</thumb>
</fanart>
<mpaa>Australia:M</mpaa>
<playcount>0</playcount>
<lastplayed></lastplayed>
<id>791373</id>
<uniqueid type=""imdb"">tt12361974</uniqueid>
<uniqueid type=""tmdb"" default=""true"">791373</uniqueid>
<genre>SuperHero</genre>
<tag>TV Recording</tag>
<set>
<name>Justice League Collection</name>
<overview>Based on the DC Comics superhero team</overview>
</set>
<country>USA</country>
<credits>Chris Terrio</credits>
<director>Zack Snyder</director>
<premiered>2021-03-18</premiered>
<year>2021</year>
<status></status>
<code></code>
<aired></aired>
<studio>Warner Bros. Pictures</studio>
<trailer></trailer>
<fileinfo>
<streamdetails>
<video>
<codec>hevc</codec>
<aspect>1.777778</aspect>
<width>1920</width>
<height>1080</height>
<durationinseconds>14528</durationinseconds>
<stereomode></stereomode>
</video>
<audio>
<codec>ac3</codec>
<language>eng</language>
<channels>6</channels>
</audio>
<audio>
<codec>ac3</codec>
<language>fre</language>
<channels>6</channels>
</audio>
<subtitle>
<language>eng</language>
</subtitle>
</streamdetails>
</fileinfo>
<actor>
<name>Ben Affleck</name>
<role>Bruce Wayne / Batman</role>
<order>0</order>
<thumb>https://image.tmdb.org/t/p/original/u525jeDOzg9hVdvYfeehTGnw7Aa.jpg</thumb>
</actor>
<actor>
<name>Henry Cavill</name>
<role>Clark Kent / Superman / Kal-El</role>
<order>1</order>
<thumb>https://image.tmdb.org/t/p/original/hErUwonrQgY5Y7RfxOfv8Fq11MB.jpg</thumb>
</actor>
<actor>
<name>Gal Gadot</name>
<role>Diana Prince / Wonder Woman</role>
<order>2</order>
<thumb>https://image.tmdb.org/t/p/original/fysvehTvU6bE3JgxaOTRfvQJzJ4.jpg</thumb>
</actor>
<resume>
<position>0.000000</position>
<total>0.000000</total>
</resume>
<dateadded>2021-03-26 11:35:50</dateadded>
</movie>"));
Either<BaseError, MovieNfo> result = await _movieNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (MovieNfo nfo in result.RightToSeq())
{
nfo.Title.Should().Be("Zack Snyder's Justice League");
nfo.SortTitle.Should().Be("Justice League 2");
nfo.Outline.Should().BeNullOrEmpty();
nfo.Year.Should().Be(2021);
nfo.ContentRating.Should().Be("Australia:M");
nfo.Premiered.IsSome.Should().BeTrue();
foreach (DateTime premiered in nfo.Premiered)
{
premiered.Should().Be(new DateTime(2021, 03, 18));
}
nfo.Plot.Should().Be(
"Determined to ensure Superman's ultimate sacrifice was not in vain, Bruce Wayne aligns forces with Diana Prince with plans to recruit a team of metahumans to protect the world from an approaching threat of catastrophic proportions.");
nfo.Tagline.Should().BeNullOrEmpty();
nfo.Genres.Should().BeEquivalentTo(new List<string> { "SuperHero" });
nfo.Tags.Should().BeEquivalentTo(new List<string> { "TV Recording" });
nfo.Studios.Should().BeEquivalentTo(new List<string> { "Warner Bros. Pictures" });
nfo.Actors.Should().BeEquivalentTo(
new List<ActorNfo>
{
new()
{
Name = "Ben Affleck", Order = 0, Role = "Bruce Wayne / Batman",
Thumb = "https://image.tmdb.org/t/p/original/u525jeDOzg9hVdvYfeehTGnw7Aa.jpg"
},
new()
{
Name = "Henry Cavill", Order = 1, Role = "Clark Kent / Superman / Kal-El",
Thumb = "https://image.tmdb.org/t/p/original/hErUwonrQgY5Y7RfxOfv8Fq11MB.jpg"
},
new()
{
Name = "Gal Gadot", Order = 2, Role = "Diana Prince / Wonder Woman",
Thumb = "https://image.tmdb.org/t/p/original/fysvehTvU6bE3JgxaOTRfvQJzJ4.jpg"
}
});
nfo.Writers.Should().BeEquivalentTo(new List<string> { "Chris Terrio" });
nfo.Directors.Should().BeEquivalentTo(new List<string> { "Zack Snyder" });
nfo.UniqueIds.Should().BeEquivalentTo(
new List<UniqueIdNfo>
{
new() { Type = "imdb", Guid = "tt12361974", Default = false },
new() { Type = "tmdb", Guid = "791373", Default = true }
});
}
}
[Test]
public async Task MetadataNfo_With_Tag_Should_Return_Nfo()
{
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"<movie><tag>Test Tag</tag></movie>"));
Either<BaseError, MovieNfo> result = await _movieNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (MovieNfo nfo in result.RightToSeq())
{
nfo.Tags.Should().BeEquivalentTo(new List<string> { "Test Tag" });
}
}
[Test]
public async Task MetadataNfo_With_Outline_Should_Return_Nfo()
{
await using var stream =
new MemoryStream(Encoding.UTF8.GetBytes(@"<movie><outline>Test Outline</outline></movie>"));
Either<BaseError, MovieNfo> result = await _movieNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (MovieNfo nfo in result.RightToSeq())
{
nfo.Outline.Should().Be("Test Outline");
}
}
}
@@ -1,189 +0,0 @@
using System.Text;
using Bugsnag;
using ErsatzTV.Core.Metadata.Nfo;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.IO;
using Moq;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.Metadata.Nfo;
[TestFixture]
public class MusicVideoNfoReaderTests
{
[SetUp]
public void SetUp() => _musicVideoNfoReader = new MusicVideoNfoReader(
new RecyclableMemoryStreamManager(),
new Mock<IClient>().Object,
new NullLogger<MusicVideoNfoReader>());
private MusicVideoNfoReader _musicVideoNfoReader;
[Test]
public async Task ParsingNfo_Should_Return_Error()
{
await using var stream =
new MemoryStream(Encoding.UTF8.GetBytes(@"https://www.themoviedb.org/movie/11-star-wars"));
Either<BaseError, MusicVideoNfo> result = await _musicVideoNfoReader.Read(stream);
result.IsLeft.Should().BeTrue();
}
[Test]
public async Task MetadataNfo_Should_Return_Nfo()
{
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"<musicvideo></musicvideo>"));
Either<BaseError, MusicVideoNfo> result = await _musicVideoNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
}
[Test]
public async Task CombinationNfo_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<musicvideo></musicvideo>
https://www.themoviedb.org/movie/11-star-wars"));
Either<BaseError, MusicVideoNfo> result = await _musicVideoNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
}
[Test]
public async Task FullSample_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
NormalizeLineEndingsLF(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes"" ?>
<musicvideo>
<title>Dancing Queen</title>
<userrating>0</userrating>
<top250>0</top250>
<track>-1</track>
<album>Arrival</album>
<outline></outline>
<plot>Dancing Queen est un des tubes emblématiques de l&apos;ère disco produits par le groupe suédois ABBA en 1976. Ce tube connaît un regain de popularité en 1994 lors de la sortie de Priscilla, folle du désert, et fait « presque » partie de la distribution du film Muriel.&#x0A;Le groupe a également enregistré une version espagnole de ce titre, La reina del baile, pour le marché d&apos;Amérique latine. On peut retrouver ces versions en espagnol des succès de ABBA sur l&apos;abum Oro. Le 18 juin 1976, ABBA a interprété cette chanson lors d&apos;un spectacle télévisé organisé en l&apos;honneur du roi Charles XVI Gustave de Suède, qui venait de se marier. Le titre sera repris en 2011 par Glee dans la saison 2, épisode 20.</plot>
<tagline></tagline>
<runtime>2</runtime>
<thumb preview=""https://www.theaudiodb.com/images/media/album/thumb/arrival-4ee244732bbde.jpg/preview"">https://www.theaudiodb.com/images/media/album/thumb/arrival-4ee244732bbde.jpg</thumb>
<thumb preview=""https://assets.fanart.tv/preview/music/d87e52c5-bb8d-4da8-b941-9f4928627dc8/albumcover/arrival-548ab7a698b49.jpg"">https://assets.fanart.tv/fanart/music/d87e52c5-bb8d-4da8-b941-9f4928627dc8/albumcover/arrival-548ab7a698b49.jpg</thumb>
<mpaa></mpaa>
<playcount>0</playcount>
<lastplayed></lastplayed>
<id></id>
<genre>Pop</genre>
<year>1976</year>
<status></status>
<director>Director 1</director>
<director>Director 2</director>
<director>Director 3</director>
<director>Director 4</director>
<code></code>
<aired></aired>
<trailer></trailer>
<fileinfo>
<streamdetails>
<video>
<codec>hevc</codec>
<aspect>1.792230</aspect>
<width>716</width>
<height>568</height>
<durationinseconds>143</durationinseconds>
<stereomode></stereomode>
</video>
<audio>
<codec>ac3</codec>
<language>eng</language>
<channels>2</channels>
</audio>
</streamdetails>
</fileinfo>
<artist>ABBA</artist>
<resume>
<position>0.000000</position>
<total>0.000000</total>
</resume>
<dateadded>2018-09-10 09:46:06</dateadded>
</musicvideo>")));
Either<BaseError, MusicVideoNfo> result = await _musicVideoNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (MusicVideoNfo nfo in result.RightToSeq())
{
nfo.Artists.Should().BeEquivalentTo(new List<string> { "ABBA" });
nfo.Title.Should().Be("Dancing Queen");
nfo.Album.Should().Be("Arrival");
nfo.Plot.Should().Be(
NormalizeLineEndingsLF(
@"Dancing Queen est un des tubes emblématiques de l'ère disco produits par le groupe suédois ABBA en 1976. Ce tube connaît un regain de popularité en 1994 lors de la sortie de Priscilla, folle du désert, et fait « presque » partie de la distribution du film Muriel.
Le groupe a également enregistré une version espagnole de ce titre, La reina del baile, pour le marché d'Amérique latine. On peut retrouver ces versions en espagnol des succès de ABBA sur l'abum Oro. Le 18 juin 1976, ABBA a interprété cette chanson lors d'un spectacle télévisé organisé en l'honneur du roi Charles XVI Gustave de Suède, qui venait de se marier. Le titre sera repris en 2011 par Glee dans la saison 2, épisode 20."));
nfo.Year.Should().Be(1976);
nfo.Aired.IsNone.Should().BeTrue();
nfo.Genres.Should().BeEquivalentTo(new List<string> { "Pop" });
nfo.Track.Should().Be(-1);
}
}
[Test]
public async Task MetadataNfo_With_Tags_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(@"<musicvideo><tag>Test Tag</tag></musicvideo>"));
Either<BaseError, MusicVideoNfo> result = await _musicVideoNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (MusicVideoNfo nfo in result.RightToSeq())
{
nfo.Tags.Should().BeEquivalentTo(new List<string> { "Test Tag" });
}
}
[Test]
public async Task MetadataNfo_With_Aired_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(@"<musicvideo><aired>2022-02-03</aired></musicvideo>"));
Either<BaseError, MusicVideoNfo> result = await _musicVideoNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (MusicVideoNfo nfo in result.RightToSeq())
{
nfo.Aired.IsSome.Should().BeTrue();
foreach (DateTime aired in nfo.Aired)
{
aired.Should().Be(new DateTime(2022, 02, 03));
}
}
}
[Test]
public async Task MetadataNfo_With_Studios_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(@"<musicvideo><studio>Test Studio</studio></musicvideo>"));
Either<BaseError, MusicVideoNfo> result = await _musicVideoNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (MusicVideoNfo nfo in result.RightToSeq())
{
nfo.Studios.Should().BeEquivalentTo(new List<string> { "Test Studio" });
}
}
private static string NormalizeLineEndingsLF(string str) =>
str
.Replace("\r\n", "\n")
.Replace("\r", "\n");
}
@@ -1,252 +0,0 @@
using System.Text;
using Bugsnag;
using ErsatzTV.Core.Metadata.Nfo;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.IO;
using Moq;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.Metadata.Nfo;
[TestFixture]
public class OtherVideoNfoReaderTests
{
[SetUp]
public void SetUp() => _otherVideoNfoReader = new OtherVideoNfoReader(
new RecyclableMemoryStreamManager(),
new Mock<IClient>().Object,
new NullLogger<OtherVideoNfoReader>());
private OtherVideoNfoReader _otherVideoNfoReader;
[Test]
public async Task ParsingNfo_Should_Return_Error()
{
await using var stream =
new MemoryStream(Encoding.UTF8.GetBytes(@"https://www.themoviedb.org/movie/11-star-wars"));
Either<BaseError, OtherVideoNfo> result = await _otherVideoNfoReader.Read(stream);
result.IsLeft.Should().BeTrue();
}
[Test]
public async Task MetadataNfo_Should_Return_Nfo()
{
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"<movie></movie>"));
Either<BaseError, OtherVideoNfo> result = await _otherVideoNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
}
[Test]
public async Task CombinationNfo_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<movie></movie>
https://www.themoviedb.org/movie/11-star-wars"));
Either<BaseError, OtherVideoNfo> result = await _otherVideoNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
}
[Test]
public async Task FullSample_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes"" ?>
<movie>
<title>Zack Snyder&apos;s Justice League</title>
<originaltitle>Zack Snyder&apos;s Justice League</originaltitle>
<sorttitle>Justice League 2</sorttitle>
<ratings>
<rating name=""imdb"" max=""10"" default=""true"">
<value>8.300000</value>
<votes>197786</votes>
</rating>
<rating name=""themoviedb"" max=""10"">
<value>8.700000</value>
<votes>3461</votes>
</rating>
<rating name=""trakt"" max=""10"">
<value>8.195670</value>
<votes>4247</votes>
</rating>
</ratings>
<userrating>0</userrating>
<top250>140</top250>
<outline></outline>
<plot>Determined to ensure Superman&apos;s ultimate sacrifice was not in vain, Bruce Wayne aligns forces with Diana Prince with plans to recruit a team of metahumans to protect the world from an approaching threat of catastrophic proportions.</plot>
<tagline></tagline>
<runtime>242</runtime>
<thumb spoof="""" cache="""" aspect=""poster"" preview="""">https://assets.fanart.tv/fanart/movies/791373/movieposter/zack-snyders-justice-league-603fdb873f474.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""poster"" preview="""">https://image.tmdb.org/t/p/original/tnAuB8q5vv7Ax9UAEje5Xi4BXik.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""landscape"" preview="""">https://assets.fanart.tv/fanart/movies/791373/moviethumb/zack-snyders-justice-league-6050310135cf6.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""landscape"" preview="""">https://image.tmdb.org/t/p/original/wcYBuOZDP6Vi8Ye4qax3Zx9dCan.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""keyart"" preview="""">https://assets.fanart.tv/fanart/movies/791373/movieposter/zack-snyders-justice-league-603fdba9bdd16.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""clearlogo"" preview="""">https://assets.fanart.tv/fanart/movies/791373/hdmovielogo/zack-snyders-justice-league-5ed3f2e4952e9.png</thumb>
<thumb spoof="""" cache="""" aspect=""banner"" preview="""">https://assets.fanart.tv/fanart/movies/791373/moviebanner/zack-snyders-justice-league-6050049514d4c.jpg</thumb>
<fanart>
<thumb colors="""" preview=""https://assets.fanart.tv/preview/movies/791373/moviebackground/zack-snyders-justice-league-5fee5b9fe0e0d.jpg"">https://assets.fanart.tv/fanart/movies/791373/moviebackground/zack-snyders-justice-league-5fee5b9fe0e0d.jpg</thumb>
<thumb colors="""" preview=""https://image.tmdb.org/t/p/w780/43NwryODVEsbBDC0jK3wYfVyb5q.jpg"">https://image.tmdb.org/t/p/original/43NwryODVEsbBDC0jK3wYfVyb5q.jpg</thumb>
</fanart>
<mpaa>Australia:M</mpaa>
<playcount>0</playcount>
<lastplayed></lastplayed>
<id>791373</id>
<uniqueid type=""imdb"">tt12361974</uniqueid>
<uniqueid type=""tmdb"" default=""true"">791373</uniqueid>
<genre>SuperHero</genre>
<tag>TV Recording</tag>
<set>
<name>Justice League Collection</name>
<overview>Based on the DC Comics superhero team</overview>
</set>
<country>USA</country>
<credits>Chris Terrio</credits>
<director>Zack Snyder</director>
<premiered>2021-03-18</premiered>
<year>2021</year>
<status></status>
<code></code>
<aired></aired>
<studio>Warner Bros. Pictures</studio>
<trailer></trailer>
<fileinfo>
<streamdetails>
<video>
<codec>hevc</codec>
<aspect>1.777778</aspect>
<width>1920</width>
<height>1080</height>
<durationinseconds>14528</durationinseconds>
<stereomode></stereomode>
</video>
<audio>
<codec>ac3</codec>
<language>eng</language>
<channels>6</channels>
</audio>
<audio>
<codec>ac3</codec>
<language>fre</language>
<channels>6</channels>
</audio>
<subtitle>
<language>eng</language>
</subtitle>
</streamdetails>
</fileinfo>
<actor>
<name>Ben Affleck</name>
<role>Bruce Wayne / Batman</role>
<order>0</order>
<thumb>https://image.tmdb.org/t/p/original/u525jeDOzg9hVdvYfeehTGnw7Aa.jpg</thumb>
</actor>
<actor>
<name>Henry Cavill</name>
<role>Clark Kent / Superman / Kal-El</role>
<order>1</order>
<thumb>https://image.tmdb.org/t/p/original/hErUwonrQgY5Y7RfxOfv8Fq11MB.jpg</thumb>
</actor>
<actor>
<name>Gal Gadot</name>
<role>Diana Prince / Wonder Woman</role>
<order>2</order>
<thumb>https://image.tmdb.org/t/p/original/fysvehTvU6bE3JgxaOTRfvQJzJ4.jpg</thumb>
</actor>
<resume>
<position>0.000000</position>
<total>0.000000</total>
</resume>
<dateadded>2021-03-26 11:35:50</dateadded>
</movie>"));
Either<BaseError, OtherVideoNfo> result = await _otherVideoNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (OtherVideoNfo nfo in result.RightToSeq())
{
nfo.Title.Should().Be("Zack Snyder's Justice League");
nfo.SortTitle.Should().Be("Justice League 2");
nfo.Outline.Should().BeNullOrEmpty();
nfo.Year.Should().Be(2021);
nfo.ContentRating.Should().Be("Australia:M");
nfo.Premiered.IsSome.Should().BeTrue();
foreach (DateTime premiered in nfo.Premiered)
{
premiered.Should().Be(new DateTime(2021, 03, 18));
}
nfo.Plot.Should().Be(
"Determined to ensure Superman's ultimate sacrifice was not in vain, Bruce Wayne aligns forces with Diana Prince with plans to recruit a team of metahumans to protect the world from an approaching threat of catastrophic proportions.");
nfo.Tagline.Should().BeNullOrEmpty();
nfo.Genres.Should().BeEquivalentTo(new List<string> { "SuperHero" });
nfo.Tags.Should().BeEquivalentTo(new List<string> { "TV Recording" });
nfo.Studios.Should().BeEquivalentTo(new List<string> { "Warner Bros. Pictures" });
nfo.Actors.Should().BeEquivalentTo(
new List<ActorNfo>
{
new()
{
Name = "Ben Affleck", Order = 0, Role = "Bruce Wayne / Batman",
Thumb = "https://image.tmdb.org/t/p/original/u525jeDOzg9hVdvYfeehTGnw7Aa.jpg"
},
new()
{
Name = "Henry Cavill", Order = 1, Role = "Clark Kent / Superman / Kal-El",
Thumb = "https://image.tmdb.org/t/p/original/hErUwonrQgY5Y7RfxOfv8Fq11MB.jpg"
},
new()
{
Name = "Gal Gadot", Order = 2, Role = "Diana Prince / Wonder Woman",
Thumb = "https://image.tmdb.org/t/p/original/fysvehTvU6bE3JgxaOTRfvQJzJ4.jpg"
}
});
nfo.Writers.Should().BeEquivalentTo(new List<string> { "Chris Terrio" });
nfo.Directors.Should().BeEquivalentTo(new List<string> { "Zack Snyder" });
nfo.UniqueIds.Should().BeEquivalentTo(
new List<UniqueIdNfo>
{
new() { Type = "imdb", Guid = "tt12361974", Default = false },
new() { Type = "tmdb", Guid = "791373", Default = true }
});
}
}
[Test]
public async Task MetadataNfo_With_Tag_Should_Return_Nfo()
{
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"<movie><tag>Test Tag</tag></movie>"));
Either<BaseError, OtherVideoNfo> result = await _otherVideoNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (OtherVideoNfo nfo in result.RightToSeq())
{
nfo.Tags.Should().BeEquivalentTo(new List<string> { "Test Tag" });
}
}
[Test]
public async Task MetadataNfo_With_Outline_Should_Return_Nfo()
{
await using var stream =
new MemoryStream(Encoding.UTF8.GetBytes(@"<movie><outline>Test Outline</outline></movie>"));
Either<BaseError, OtherVideoNfo> result = await _otherVideoNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (OtherVideoNfo nfo in result.RightToSeq())
{
nfo.Outline.Should().Be("Test Outline");
}
}
}
@@ -1,221 +0,0 @@
using System.Text;
using Bugsnag;
using ErsatzTV.Core.Metadata.Nfo;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.IO;
using Moq;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.Metadata.Nfo;
[TestFixture]
public class TvShowNfoReaderTests
{
[SetUp]
public void SetUp() => _tvShowNfoReader = new TvShowNfoReader(
new RecyclableMemoryStreamManager(),
new Mock<IClient>().Object,
new NullLogger<TvShowNfoReader>());
private TvShowNfoReader _tvShowNfoReader;
[Test]
public async Task ParsingNfo_Should_Return_Error()
{
await using var stream =
new MemoryStream(Encoding.UTF8.GetBytes(@"https://www.themoviedb.org/movie/11-star-wars"));
Either<BaseError, TvShowNfo> result = await _tvShowNfoReader.Read(stream);
result.IsLeft.Should().BeTrue();
}
[Test]
public async Task MetadataNfo_Should_Return_Nfo()
{
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"<tvshow></tvshow>"));
Either<BaseError, TvShowNfo> result = await _tvShowNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
}
[Test]
public async Task CombinationNfo_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<tvshow></tvshow>
https://www.themoviedb.org/movie/11-star-wars"));
Either<BaseError, TvShowNfo> result = await _tvShowNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
}
[Test]
public async Task FullSample_Should_Return_Nfo()
{
await using var stream = new MemoryStream(
Encoding.UTF8.GetBytes(
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes"" ?>
<tvshow>
<title>WandaVision</title>
<originaltitle>WandaVision</originaltitle>
<showtitle>WandaVision</showtitle>
<ratings>
<rating name=""imdb"" max=""10"" default=""true"">
<value>8.200000</value>
<votes>105359</votes>
</rating>
<rating name=""tmdb"" max=""10"">
<value>8.500000</value>
<votes>7230</votes>
</rating>
<rating name=""trakt"" max=""10"">
<value>8.077950</value>
<votes>3284</votes>
</rating>
</ratings>
<userrating>0</userrating>
<top250>0</top250>
<season>1</season>
<episode>9</episode>
<displayseason>-1</displayseason>
<displayepisode>-1</displayepisode>
<outline></outline>
<plot>Wanda Maximoff and Vision—two super-powered beings living idealized suburban lives—begin to suspect that everything is not as it seems.</plot>
<tagline></tagline>
<runtime>0</runtime>
<thumb spoof="""" cache="""" aspect=""landscape"" preview=""https://image.tmdb.org/t/p/w780/dUWto4NaeJFrGx7jm8m3KLymUGf.jpg"">https://image.tmdb.org/t/p/original/dUWto4NaeJFrGx7jm8m3KLymUGf.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""poster"" preview=""https://image.tmdb.org/t/p/w780/8UsAB1hgwnd80eI2ociyppB6UXL.jpg"">https://image.tmdb.org/t/p/original/8UsAB1hgwnd80eI2ociyppB6UXL.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""poster"" preview=""https://assets.fanart.tv/preview/tv/362392/tvposter/wandavision-6009571d1ed1f.jpg"">https://assets.fanart.tv/fanart/tv/362392/tvposter/wandavision-6009571d1ed1f.jpg</thumb>
<thumb spoof="""" cache="""" aspect=""clearlogo"" preview=""https://assets.fanart.tv/preview/tv/362392/hdtvlogo/marvels-wandavision-5f6ac3b1e9458.png"">https://assets.fanart.tv/fanart/tv/362392/hdtvlogo/marvels-wandavision-5f6ac3b1e9458.png</thumb>
<thumb spoof="""" cache="""" aspect=""clearart"" preview=""https://assets.fanart.tv/preview/tv/362392/hdclearart/wandavision-6009b6875a285.png"">https://assets.fanart.tv/fanart/tv/362392/hdclearart/wandavision-6009b6875a285.png</thumb>
<thumb spoof="""" cache="""" aspect=""landscape"" preview=""https://assets.fanart.tv/preview/tv/362392/tvthumb/wandavision-603032a5349b9.jpg"">https://assets.fanart.tv/fanart/tv/362392/tvthumb/wandavision-603032a5349b9.jpg</thumb>
<thumb spoof="""" cache="""" season=""1"" type=""season"" aspect=""poster"" preview=""https://image.tmdb.org/t/p/w780/7u443QI5xNIfLgNzEsV43CYZCWX.jpg"">https://image.tmdb.org/t/p/original/7u443QI5xNIfLgNzEsV43CYZCWX.jpg</thumb>
<fanart>
<thumb colors="""" preview="""">https://image.tmdb.org/t/p/original/57vVjteucIF3bGnZj6PmaoJRScw.jpg</thumb>
<thumb colors="""" preview="""">https://assets.fanart.tv/fanart/tv/362392/showbackground/marvels-wandavision-5ff4fef387a43.jpg</thumb>
</fanart>
<mpaa>Australia:M</mpaa>
<playcount>0</playcount>
<lastplayed>2021-03-29</lastplayed>
<id>85271</id>
<uniqueid type=""imdb"">tt9140560</uniqueid>
<uniqueid type=""tmdb"" default=""true"">85271</uniqueid>
<uniqueid type=""tvdb"">362392</uniqueid>
<genre>SuperHero</genre>
<premiered>2021-01-15</premiered>
<year>2021</year>
<status>Ended</status>
<code></code>
<aired></aired>
<studio>Disney+</studio>
<actor>
<name>Elizabeth Olsen</name>
<role>Wanda Maximoff / The Scarlet Witch</role>
<order>0</order>
<thumb>https://image.tmdb.org/t/p/original/wIU675y4dofIDVuhaNWPizJNtep.jpg</thumb>
</actor>
<actor>
<name>Paul Bettany</name>
<role>Vision / The Vision</role>
<order>1</order>
<thumb>https://image.tmdb.org/t/p/original/vcAVrAOZrpqmi37qjFdztRAv1u9.jpg</thumb>
</actor>
<namedseason number=""1"">Season 1</namedseason>
<resume>
<position>0.000000</position>
<total>0.000000</total>
</resume>
<dateadded>2021-03-12 06:15:51</dateadded>
</tvshow>"));
Either<BaseError, TvShowNfo> result = await _tvShowNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (TvShowNfo nfo in result.RightToSeq())
{
nfo.Title.Should().Be("WandaVision");
nfo.Year.Should().Be(2021);
nfo.Plot.Should().Be(
"Wanda Maximoff and Vision—two super-powered beings living idealized suburban lives—begin to suspect that everything is not as it seems.");
nfo.ContentRating.Should().Be("Australia:M");
nfo.Genres.Should().BeEquivalentTo(new List<string> { "SuperHero" });
nfo.Studios.Should().BeEquivalentTo(new List<string> { "Disney+" });
nfo.Actors.Should().BeEquivalentTo(
new List<ActorNfo>
{
new()
{
Name = "Elizabeth Olsen", Order = 0, Role = "Wanda Maximoff / The Scarlet Witch",
Thumb = "https://image.tmdb.org/t/p/original/wIU675y4dofIDVuhaNWPizJNtep.jpg"
},
new()
{
Name = "Paul Bettany", Order = 1, Role = "Vision / The Vision",
Thumb = "https://image.tmdb.org/t/p/original/vcAVrAOZrpqmi37qjFdztRAv1u9.jpg"
}
});
nfo.UniqueIds.Should().BeEquivalentTo(
new List<UniqueIdNfo>
{
new() { Type = "imdb", Guid = "tt9140560", Default = false },
new() { Type = "tmdb", Guid = "85271", Default = true },
new() { Type = "tvdb", Guid = "362392", Default = false }
});
nfo.Premiered.IsSome.Should().BeTrue();
foreach (DateTime premiered in nfo.Premiered)
{
premiered.Should().Be(new DateTime(2021, 1, 15));
}
}
}
[Test]
public async Task MetadataNfo_With_Outline_Should_Return_Nfo()
{
await using var stream =
new MemoryStream(Encoding.UTF8.GetBytes(@"<tvshow><outline>Test Outline</outline></tvshow>"));
Either<BaseError, TvShowNfo> result = await _tvShowNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (TvShowNfo nfo in result.RightToSeq())
{
nfo.Outline.Should().Be("Test Outline");
}
}
[Test]
public async Task MetadataNfo_With_Tagline_Should_Return_Nfo()
{
await using var stream =
new MemoryStream(Encoding.UTF8.GetBytes(@"<tvshow><tagline>Test Tagline</tagline></tvshow>"));
Either<BaseError, TvShowNfo> result = await _tvShowNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (TvShowNfo nfo in result.RightToSeq())
{
nfo.Tagline.Should().Be("Test Tagline");
}
}
[Test]
public async Task MetadataNfo_With_Tag_Should_Return_Nfo()
{
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"<tvshow><tag>Test Tag</tag></tvshow>"));
Either<BaseError, TvShowNfo> result = await _tvShowNfoReader.Read(stream);
result.IsRight.Should().BeTrue();
foreach (TvShowNfo nfo in result.RightToSeq())
{
nfo.Tags.Should().BeEquivalentTo(new List<string> { "Test Tag" });
}
}
}
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--created on whatever - comment-->
<artist>
<name>Test Name</name>
</artist>
ÐPS½NÞ5Þ*˜¡¡ã·Ýq×ÍâeVk—¯¬}É
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--created on whatever - comment-->
<artist>
<name>Test Name</name>
<style>ÐPS½NÞ5Þ*˜¡¡ã·Ýq×ÍâeVk—¯¬}É</style>
<mood>Test Mood</mood>
</artist>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--created on whatever - comment-->
<episodedetails>
<title>Test Title</title>
</episodedetails>
ÐPS½NÞ5Þ*˜¡¡ã·Ýq×ÍâeVk—¯¬}É